diff --git a/src/swc/__tests__/sources.test.ts b/src/swc/__tests__/sources.test.ts index 8548f40..c0b3bd2 100644 --- a/src/swc/__tests__/sources.test.ts +++ b/src/swc/__tests__/sources.test.ts @@ -11,14 +11,14 @@ describe("globSources", () => { }); it("exclude dotfiles sources when includeDotfiles=false", async () => { - const files = await globSources([".dotfile"], false); + const [files, _] = await globSources([".dotfile"], false); expect([...files]).toEqual([]); }); it("include dotfiles sources when includeDotfiles=true", async () => { (fs as any).setMockStats({ ".dotfile": { isDirectory: () => false } }); - const files = await globSources([".dotfile"], true); + const [files, _] = await globSources([".dotfile"], true); expect([...files]).toEqual([".dotfile"]); }); @@ -26,7 +26,7 @@ describe("globSources", () => { it("include multiple file sources", async () => { (fs as any).setMockStats({ ".dotfile": { isDirectory: () => false } }); (fs as any).setMockStats({ file: { isDirectory: () => false } }); - const files = await globSources([".dotfile", "file"], true); + const [files, _] = await globSources([".dotfile", "file"], true); expect([...files]).toEqual([".dotfile", "file"]); }); @@ -34,7 +34,7 @@ describe("globSources", () => { it("exclude files that errors on stats", async () => { (fs as any).setMockStats({ ".dotfile": { isDirectory: () => false } }); (fs as any).setMockStats({ file: new Error("Failed stat") }); - const files = await globSources([".dotfile", "file"], true); + const [files, _] = await globSources([".dotfile", "file"], true); expect([...files]).toEqual([".dotfile"]); }); @@ -44,7 +44,7 @@ describe("globSources", () => { (fs as any).setMockStats({ file: { isDirectory: () => false } }); (glob as unknown as jest.Mock).mockResolvedValue(["fileDir1", "fileDir2"]); - const files = await globSources(["file", "directory"], true); + const [files, _] = await globSources(["file", "directory"], true); expect([...files]).toEqual(["file", "fileDir1", "fileDir2"]); }); @@ -54,7 +54,7 @@ describe("globSources", () => { (fs as any).setMockStats({ file: { isDirectory: () => false } }); (glob as unknown as jest.Mock).mockRejectedValue(new Error("Failed")); - const files = await globSources(["file", "directory"], true); + const [files, _] = await globSources(["file", "directory"], true); expect([...files]).toEqual(["file"]); }); diff --git a/src/swc/dir.ts b/src/swc/dir.ts index 0568d5a..b0026eb 100644 --- a/src/swc/dir.ts +++ b/src/swc/dir.ts @@ -1,6 +1,6 @@ import slash from "slash"; import { existsSync, promises } from "fs"; -import { dirname, relative, join } from "path"; +import { dirname, relative, join, isAbsolute, resolve } from "path"; import { CompileStatus } from "./constants"; import { CliOptions } from "./options"; import { compile } from "./util"; @@ -11,6 +11,7 @@ import { slitCompilableAndCopyable, watchSources, } from "./sources"; +import { FileContext } from "./file"; import type { Options } from "@swc/core"; @@ -26,39 +27,33 @@ declare module "fs" { const { mkdir, rmdir, rm, copyFile, unlink } = promises; -const cwd = process.cwd(); const recursive = { recursive: true }; -/** - * Removes the leading directory, including all parent relative paths - */ -function stripComponents(filename: string) { - const components = filename.split("/").slice(1); - if (!components.length) { - return filename; - } - while (components[0] === "..") { - components.shift(); - } - return components.join("/"); -} +function getDest( + filename: string, + sourceLength: number, + cwd: string, + outDir: string, + ext?: string +) { + const outDirAbsolutePath = slash(resolve(cwd, outDir)); + let relativePath = slash(filename.slice(sourceLength)); -function getDest(filename: string, outDir: string, ext?: string) { - const relativePath = slash(relative(cwd, filename)); - let base = stripComponents(relativePath); if (ext) { - base = base.replace(/\.\w*$/, ext); + relativePath = relativePath.replace(/\.\w*$/, ext); } - return join(outDir, base); + return join(outDirAbsolutePath, relativePath); } async function handleCompile( filename: string, + sourceLength: number, + cwd: string, outDir: string, sync: boolean, swcOptions: Options ) { - const dest = getDest(filename, outDir, ".js"); + const dest = getDest(filename, sourceLength, cwd, outDir, ".js"); const sourceFileName = slash(relative(dirname(dest), filename)); const options = { ...swcOptions, sourceFileName }; @@ -73,11 +68,15 @@ async function handleCompile( } } -async function handleCopy(filename: string, outDir: string) { - const dest = getDest(filename, outDir); +async function handleCopy( + filename: string, + sourceLength: number, + cwd: string, + outDir: string +) { + const dest = getDest(filename, sourceLength, cwd, outDir); const dir = dirname(dest); - console.log(filename); await mkdir(dir, recursive); await copyFile(filename, dest); @@ -95,12 +94,29 @@ async function beforeStartCompilation(cliOptions: CliOptions) { } } -async function initialCompilation(cliOptions: CliOptions, swcOptions: Options) { +function absolutePath(filenames: string[], cwd: string): string[] { + return filenames.map((filename: string) => { + if (!isAbsolute(filename)) { + filename = resolve(cwd, filename); + } + return filename; + }); +} + +function resolveCwd(cwd: string): string { + return cwd ?? process.cwd(); +} + +async function initialCompilation( + cliOptions: CliOptions, + swcOptions: Options +): Promise { const { includeDotfiles, filenames, copyFiles, extensions, + cwd, outDir, sync, quiet, @@ -110,7 +126,12 @@ async function initialCompilation(cliOptions: CliOptions, swcOptions: Options) { const results = new Map(); const start = process.hrtime(); - const sourceFiles = await globSources(filenames, includeDotfiles); + const resolvedCwd = resolveCwd(cwd); + const filenamesAbsolutePath = absolutePath(filenames, resolvedCwd); + const [sourceFiles, fileContext] = await globSources( + filenamesAbsolutePath, + includeDotfiles + ); const [compilable, copyable] = slitCompilableAndCopyable( sourceFiles, extensions, @@ -120,7 +141,14 @@ async function initialCompilation(cliOptions: CliOptions, swcOptions: Options) { if (sync) { for (const filename of compilable) { try { - const result = await handleCompile(filename, outDir, sync, swcOptions); + const result = await handleCompile( + filename, + fileContext[filename], + resolvedCwd, + outDir, + sync, + swcOptions + ); results.set(filename, result); } catch (err) { console.error(err.message); @@ -129,7 +157,12 @@ async function initialCompilation(cliOptions: CliOptions, swcOptions: Options) { } for (const filename of copyable) { try { - const result = await handleCopy(filename, outDir); + const result = await handleCopy( + filename, + fileContext[filename], + resolvedCwd, + outDir + ); results.set(filename, result); } catch (err) { console.error(err.message); @@ -140,13 +173,24 @@ async function initialCompilation(cliOptions: CliOptions, swcOptions: Options) { await Promise.all([ Promise.allSettled( compilable.map(file => - handleCompile(file, outDir, sync, swcOptions).catch(err => { + handleCompile( + file, + fileContext[file], + resolvedCwd, + outDir, + sync, + swcOptions + ).catch(err => { console.error(err.message); throw err; }) ) ), - Promise.allSettled(copyable.map(file => handleCopy(file, outDir))), + Promise.allSettled( + copyable.map(file => + handleCopy(file, fileContext[file], resolvedCwd, outDir) + ) + ), ]).then(([compiled, copied]) => { compiled.forEach((result, index) => { const filename = compilable[index]; @@ -212,10 +256,17 @@ async function initialCompilation(cliOptions: CliOptions, swcOptions: Options) { throw new Error("Failed to compile"); } } + + return fileContext; } -async function watchCompilation(cliOptions: CliOptions, swcOptions: Options) { +async function watchCompilation( + cliOptions: CliOptions, + swcOptions: Options, + fileContext: FileContext +) { const { + cwd, includeDotfiles, filenames, copyFiles, @@ -225,7 +276,9 @@ async function watchCompilation(cliOptions: CliOptions, swcOptions: Options) { sync, } = cliOptions; - const watcher = await watchSources(filenames, includeDotfiles); + const resolvedCwd = resolveCwd(cwd); + const filenamesAbsolutePath = absolutePath(filenames, resolvedCwd); + const watcher = await watchSources(filenamesAbsolutePath, includeDotfiles); watcher.on("ready", () => { if (!quiet) { console.info("Watching for file changes."); @@ -234,9 +287,13 @@ async function watchCompilation(cliOptions: CliOptions, swcOptions: Options) { watcher.on("unlink", async filename => { try { if (isCompilableExtension(filename, extensions)) { - await unlink(getDest(filename, outDir, ".js")); + await unlink( + getDest(filename, fileContext[filename], resolvedCwd, outDir, ".js") + ); } else if (copyFiles) { - await unlink(getDest(filename, outDir)); + await unlink( + getDest(filename, fileContext[filename], resolvedCwd, outDir) + ); } } catch (err) { if (err.code !== "ENOENT") { @@ -251,6 +308,8 @@ async function watchCompilation(cliOptions: CliOptions, swcOptions: Options) { const start = process.hrtime(); const result = await handleCompile( filename, + fileContext[filename], + resolvedCwd, outDir, sync, swcOptions @@ -268,7 +327,12 @@ async function watchCompilation(cliOptions: CliOptions, swcOptions: Options) { } else if (copyFiles) { try { const start = process.hrtime(); - const result = await handleCopy(filename, outDir); + const result = await handleCopy( + filename, + fileContext[filename], + resolvedCwd, + outDir + ); if (!quiet && result === CompileStatus.Copied) { const end = process.hrtime(start); console.log( @@ -295,9 +359,9 @@ export default async function dir({ const { watch } = cliOptions; await beforeStartCompilation(cliOptions); - await initialCompilation(cliOptions, swcOptions); + const fileContext = await initialCompilation(cliOptions, swcOptions); if (watch) { - await watchCompilation(cliOptions, swcOptions); + await watchCompilation(cliOptions, swcOptions, fileContext); } } diff --git a/src/swc/file.ts b/src/swc/file.ts index 50de19e..9777e01 100644 --- a/src/swc/file.ts +++ b/src/swc/file.ts @@ -7,6 +7,15 @@ import { CliOptions } from "./options"; import { globSources, isCompilableExtension, watchSources } from "./sources"; import * as util from "./util"; +// export interface File { +// filename: string; +// sourceLength: number; +// } + +export interface FileContext { + [filename: string]: number; +} + export default async function ({ cliOptions, swcOptions, @@ -107,10 +116,11 @@ export default async function ({ ) { const results: typeof previousResults = new Map(); - for (const filename of await globSources( + const [filenames, _] = await globSources( cliOptions.filenames, cliOptions.includeDotfiles - )) { + ); + for (const filename of filenames) { if (isCompilableExtension(filename, cliOptions.extensions)) { results.set(filename, previousResults.get(filename)!); } diff --git a/src/swc/index.ts b/src/swc/index.ts index 86faba4..085a778 100644 --- a/src/swc/index.ts +++ b/src/swc/index.ts @@ -2,7 +2,7 @@ import dirCommand from "./dir"; import fileCommand from "./file"; import parseArgs, { initProgram } from "./options"; -initProgram() +initProgram(); const opts = parseArgs(process.argv); const fn = opts.cliOptions.outDir ? dirCommand : fileCommand; diff --git a/src/swc/options.ts b/src/swc/options.ts index 72b5e6a..6df13d2 100644 --- a/src/swc/options.ts +++ b/src/swc/options.ts @@ -19,6 +19,8 @@ export const initProgram = () => { "filename to use when reading from stdin - this will be used in source-maps, errors etc" ); + program.option("--cwd [path]", "Specify the current working directory"); + program.option("--config-file [path]", "Path to a .swcrc file to use"); program.option( @@ -140,6 +142,7 @@ function collect( } export interface CliOptions { + readonly cwd: string; readonly outDir: string; readonly outFile: string; /** @@ -246,6 +249,7 @@ export default function parserArgs(args: string[]) { } const cliOptions: CliOptions = { + cwd: opts.cwd, outDir: opts.outDir, outFile: opts.outFile, filename: opts.filename, diff --git a/src/swc/sources.ts b/src/swc/sources.ts index 8695b4f..1b04d50 100644 --- a/src/swc/sources.ts +++ b/src/swc/sources.ts @@ -2,6 +2,7 @@ import glob from "fast-glob"; import slash from "slash"; import { stat } from "fs"; import { join, basename, extname } from "path"; +import { FileContext } from "./file"; /** * Find all input files based on source globs @@ -9,12 +10,14 @@ import { join, basename, extname } from "path"; export async function globSources( sources: string[], includeDotfiles = false -): Promise { +): Promise<[string[], FileContext]> { const globConfig = { dot: includeDotfiles, nodir: true, }; + let fileContext: FileContext = {}; + const files = await Promise.all( sources .filter(source => includeDotfiles || !basename(source).startsWith(".")) @@ -26,10 +29,18 @@ export async function globSources( return; } if (!stat.isDirectory()) { + fileContext[source] = basename(source).length; resolve([source]); } else { glob(slash(join(source, "**")), globConfig) - .then(matches => resolve(matches)) + .then(matches => + resolve( + matches.map(match => { + fileContext[match] = source.length; + return match; + }) + ) + ) .catch(() => resolve([])); } }); @@ -37,7 +48,7 @@ export async function globSources( }) ); - return Array.from(new Set(files.flat())); + return [Array.from(new Set(files.flat())), fileContext]; } type Split = [compilable: string[], copyable: string[]];