From 0d8f59aaac4954c3e68f742bb0965343ec82d771 Mon Sep 17 00:00:00 2001 From: Paolo Insogna Date: Fri, 26 Jan 2024 11:50:07 +0100 Subject: [PATCH] feat: Add `strip-leading-paths` option (#283) --- src/swc/__tests__/options.test.ts | 11 +++++++++++ src/swc/__tests__/util.test.ts | 22 ++++++++++++++++++++++ src/swc/dir.ts | 31 +++++++++++++++++++++++-------- src/swc/dirWorker.ts | 9 ++++++++- src/swc/options.ts | 8 ++++++++ src/swc/util.ts | 21 ++++++++++++++++++++- 6 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 src/swc/__tests__/util.test.ts diff --git a/src/swc/__tests__/options.test.ts b/src/swc/__tests__/options.test.ts index 8e55885..534a96a 100644 --- a/src/swc/__tests__/options.test.ts +++ b/src/swc/__tests__/options.test.ts @@ -35,6 +35,7 @@ const createDefaultResult = (): ParserArgsReturn => ({ outFile: undefined, quiet: false, sourceMapTarget: undefined, + stripLeadingPaths: false, sync: false, watch: false, }, @@ -206,6 +207,14 @@ describe("parserArgs", () => { describe("--config", () => { it("throws with no config", async () => { + let mockConsoleError: jest.SpyInstance; + + mockConsoleError = jest + .spyOn(process.stderr, "write") + .mockImplementation(() => { + return true; + }); + const args = [ "node", "/path/to/node_modules/swc-cli/bin/swc.js", @@ -213,6 +222,8 @@ describe("parserArgs", () => { "-C", ]; expect(() => parserArgs(args)).toThrow(); + + mockConsoleError.mockRestore(); }); it("react development", async () => { diff --git a/src/swc/__tests__/util.test.ts b/src/swc/__tests__/util.test.ts new file mode 100644 index 0000000..49221ca --- /dev/null +++ b/src/swc/__tests__/util.test.ts @@ -0,0 +1,22 @@ +import { join } from "path"; +import { getDest } from "../util"; + +describe("getDest", () => { + it("does not modify the filename by default", () => { + expect( + getDest(join(process.cwd(), "src/path/name.ts"), "foo/bar", false) + ).toEqual("foo/bar/src/path/name.ts"); + }); + + it("when stripLeadingPaths is true, it removes leading paths", () => { + expect( + getDest(join(process.cwd(), "src/path/name.ts"), "foo/bar", true) + ).toEqual("foo/bar/path/name.ts"); + }); + + it("when stripLeadingPaths is true, it also resolves relative paths", () => { + expect( + getDest(join(process.cwd(), "../../path/name.ts"), "foo/bar", true) + ).toEqual("foo/bar/path/name.ts"); + }); +}); diff --git a/src/swc/dir.ts b/src/swc/dir.ts index 7c7e04f..a9a814a 100644 --- a/src/swc/dir.ts +++ b/src/swc/dir.ts @@ -28,8 +28,12 @@ const { mkdir, rmdir, rm, copyFile, unlink } = promises; const recursive = { recursive: true }; -async function handleCopy(filename: string, outDir: string) { - const dest = getDest(filename, outDir); +async function handleCopy( + filename: string, + outDir: string, + stripLeadingPaths: boolean +) { + const dest = getDest(filename, outDir, stripLeadingPaths); const dir = dirname(dest); await mkdir(dir, recursive); @@ -56,6 +60,7 @@ async function initialCompilation(cliOptions: CliOptions, swcOptions: Options) { copyFiles, extensions, outDir, + stripLeadingPaths, sync, quiet, watch, @@ -85,6 +90,7 @@ async function initialCompilation(cliOptions: CliOptions, swcOptions: Options) { filename, outDir, sync, + cliOptions, swcOptions, }); results.set(filename, result); @@ -95,7 +101,7 @@ async function initialCompilation(cliOptions: CliOptions, swcOptions: Options) { } for (const filename of copyable) { try { - const result = await handleCopy(filename, outDir); + const result = await handleCopy(filename, outDir, stripLeadingPaths); results.set(filename, result); } catch (err: any) { console.error(err.message); @@ -118,7 +124,9 @@ async function initialCompilation(cliOptions: CliOptions, swcOptions: Options) { }) ) ), - Promise.allSettled(copyable.map(file => handleCopy(file, outDir))), + Promise.allSettled( + copyable.map(file => handleCopy(file, outDir, stripLeadingPaths)) + ), ]).then(([compiled, copied]) => { compiled.forEach((result, index) => { const filename = compilable[index]; @@ -197,6 +205,7 @@ async function watchCompilation(cliOptions: CliOptions, swcOptions: Options) { copyFiles, extensions, outDir, + stripLeadingPaths, quiet, sync, } = cliOptions; @@ -210,14 +219,19 @@ async function watchCompilation(cliOptions: CliOptions, swcOptions: Options) { watcher.on("unlink", async filename => { try { if (isCompilableExtension(filename, extensions)) { - await unlink(getDest(filename, outDir, ".js")); - const sourcemapPath = getDest(filename, outDir, ".js.map"); + await unlink(getDest(filename, outDir, stripLeadingPaths, ".js")); + const sourcemapPath = getDest( + filename, + outDir, + stripLeadingPaths, + ".js.map" + ); const sourcemapExists = await exists(sourcemapPath); if (sourcemapExists) { await unlink(sourcemapPath); } } else if (copyFiles) { - await unlink(getDest(filename, outDir)); + await unlink(getDest(filename, outDir, stripLeadingPaths)); } } catch (err: any) { if (err.code !== "ENOENT") { @@ -234,6 +248,7 @@ async function watchCompilation(cliOptions: CliOptions, swcOptions: Options) { filename, outDir, sync, + cliOptions, swcOptions, }); if (!quiet && result === CompileStatus.Compiled) { @@ -249,7 +264,7 @@ 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, outDir, stripLeadingPaths); if (!quiet && result === CompileStatus.Copied) { const end = process.hrtime(start); console.log( diff --git a/src/swc/dirWorker.ts b/src/swc/dirWorker.ts index 88f0d3a..102cb33 100644 --- a/src/swc/dirWorker.ts +++ b/src/swc/dirWorker.ts @@ -5,14 +5,21 @@ import { compile, getDest } from "./util"; import { outputResult } from "./compile"; import type { Options } from "@swc/core"; +import type { CliOptions } from "./options"; export default async function handleCompile(opts: { filename: string; outDir: string; sync: boolean; + cliOptions: CliOptions; swcOptions: Options; }) { - const dest = getDest(opts.filename, opts.outDir, ".js"); + const dest = getDest( + opts.filename, + opts.outDir, + opts.cliOptions.stripLeadingPaths, + ".js" + ); const sourceFileName = slash(relative(dirname(dest), opts.filename)); const options = { ...opts.swcOptions, sourceFileName }; diff --git a/src/swc/options.ts b/src/swc/options.ts index 035ee49..b64675a 100644 --- a/src/swc/options.ts +++ b/src/swc/options.ts @@ -85,6 +85,12 @@ export const initProgram = () => { "-D, --copy-files", "When compiling a directory copy over non-compilable files" ); + + program.option( + "--strip-leading-paths", + "Remove the leading directory (including all parent relative paths) when building the final output path" + ); + program.option( "--include-dotfiles", "Include dotfiles when compiling and copying non-compilable files" @@ -158,6 +164,7 @@ function collect( export interface CliOptions { readonly outDir: string; readonly outFile: string; + readonly stripLeadingPaths: boolean; /** * Invoke swc using transformSync. It's useful for debugging. */ @@ -278,6 +285,7 @@ export default function parserArgs(args: string[]) { const cliOptions: CliOptions = { outDir: opts.outDir, outFile: opts.outFile, + stripLeadingPaths: Boolean(opts.stripLeadingPaths), filename: opts.filename, filenames, sync: !!opts.sync, diff --git a/src/swc/util.ts b/src/swc/util.ts index f61b156..28d8cfd 100644 --- a/src/swc/util.ts +++ b/src/swc/util.ts @@ -126,10 +126,29 @@ export function assertCompilationResult( } } +function stripComponents(filename: string) { + const components = filename.split("/").slice(1); + if (!components.length) { + return filename; + } + while (components[0] === "..") { + components.shift(); + } + return components.join("/"); +} + const cwd = process.cwd(); -export function getDest(filename: string, outDir: string, ext?: string) { +export function getDest( + filename: string, + outDir: string, + stripLeadingPaths: boolean, + ext?: string +) { let base = slash(relative(cwd, filename)); + if (stripLeadingPaths) { + base = stripComponents(base); + } if (ext) { base = base.replace(/\.\w*$/, ext); }