From 449f23b032ace0121adf73a76db97549c389a537 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Fri, 10 Jan 2025 11:32:48 +0100 Subject: [PATCH] feat: wrap and rethrow swc errors --- biome.json | 3 +- esbuild.config.mjs | 7 ++++ src/errors.ts | 24 +++++++++++++ src/strip-loader.ts | 50 +++++++++++++++------------ src/transform-loader.ts | 61 ++++++++++++++++++--------------- test/fixtures/invalid-syntax.ts | 3 ++ test/loader.test.js | 15 ++++++++ 7 files changed, 113 insertions(+), 50 deletions(-) create mode 100644 src/errors.ts create mode 100644 test/fixtures/invalid-syntax.ts diff --git a/biome.json b/biome.json index b7f0ef2cd..b7bba5e4c 100644 --- a/biome.json +++ b/biome.json @@ -19,7 +19,8 @@ "deps/**", "lib/**", "node_modules/**", - "test/snapshot/**" + "test/snapshot/**", + "test/fixtures/**" ] } } diff --git a/esbuild.config.mjs b/esbuild.config.mjs index 281464db5..a8fb24679 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -27,6 +27,13 @@ await build({ plugins: [copyPlugin], }); +await build({ + entryPoints: ["src/errors.ts"], + platform: "node", + target: "node22", + outfile: "dist/errors.js", +}); + await build({ entryPoints: ["src/strip-loader.ts"], bundle: false, diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 000000000..6adf7aab1 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,24 @@ +type SwcError = { + code: "UnsupportedSyntax" | "InvalidSyntax"; + message: string; +}; + +// Type guard to check if error is SwcError +export function isSwcError(error: unknown): error is SwcError { + return (error as SwcError).code !== undefined; +} + +// Since swc throw an object, we need to wrap it in a proper error +export function wrapAndReThrowSwcError(error: SwcError): never { + switch (error.code) { + case "UnsupportedSyntax": { + const unsupportedSyntaxError = new Error(error.message); + unsupportedSyntaxError.name = "UnsupportedSyntaxError"; + throw unsupportedSyntaxError; + } + case "InvalidSyntax": + throw new SyntaxError(error.message); + default: + throw new Error(error.message); + } +} diff --git a/src/strip-loader.ts b/src/strip-loader.ts index a185935d3..151ee2083 100644 --- a/src/strip-loader.ts +++ b/src/strip-loader.ts @@ -1,36 +1,42 @@ import type { LoadFnOutput, LoadHookContext } from "node:module"; -import type { Options } from "../lib/wasm"; +import { isSwcError, wrapAndReThrowSwcError } from "./errors.js"; import { transformSync } from "./index.js"; -type NextLoad = ( - url: string, - context?: LoadHookContext, -) => LoadFnOutput | Promise; - export async function load( url: string, context: LoadHookContext, - nextLoad: NextLoad, + nextLoad: ( + url: string, + context?: LoadHookContext, + ) => LoadFnOutput | Promise, ) { const { format } = context; if (format.endsWith("-typescript")) { // Use format 'module' so it returns the source as-is, without stripping the types. // Format 'commonjs' would not return the source for historical reasons. - const { source } = await nextLoad(url, { - ...context, - format: "module", - }); - // biome-ignore lint/style/noNonNullAssertion: If module exists, it will have a source - const { code } = transformSync(source!.toString(), { - mode: "strip-only", - } as Options); - return { - format: format.replace("-typescript", ""), - // Source map is not necessary in strip-only mode. However, to map the source - // file in debuggers to the original TypeScript source, add a sourceURL magic - // comment to hint that it is a generated source. - source: `${code}\n\n//# sourceURL=${url}`, - }; + try { + const { source } = await nextLoad(url, { + ...context, + format: "module", + }); + // biome-ignore lint/style/noNonNullAssertion: If module exists, it will have a source + const { code } = transformSync(source!.toString(), { + mode: "strip-only", + }); + return { + format: format.replace("-typescript", ""), + // Source map is not necessary in strip-only mode. However, to map the source + // file in debuggers to the original TypeScript source, add a sourceURL magic + // comment to hint that it is a generated source. + source: `${code}\n\n//# sourceURL=${url}`, + }; + } catch (error: unknown) { + if (isSwcError(error)) { + wrapAndReThrowSwcError(error); + } + // If the error is not an SwcError, rethrow it + throw error; + } } return nextLoad(url, context); } diff --git a/src/transform-loader.ts b/src/transform-loader.ts index 9f3b73629..89b786d8e 100644 --- a/src/transform-loader.ts +++ b/src/transform-loader.ts @@ -1,44 +1,51 @@ import type { LoadFnOutput, LoadHookContext } from "node:module"; import type { Options } from "../lib/wasm"; +import { isSwcError, wrapAndReThrowSwcError } from "./errors.js"; import { transformSync } from "./index.js"; -type NextLoad = ( - url: string, - context?: LoadHookContext, -) => LoadFnOutput | Promise; - export async function load( url: string, context: LoadHookContext, - nextLoad: NextLoad, + nextLoad: ( + url: string, + context?: LoadHookContext, + ) => LoadFnOutput | Promise, ) { const { format } = context; if (format.endsWith("-typescript")) { - // Use format 'module' so it returns the source as-is, without stripping the types. - // Format 'commonjs' would not return the source for historical reasons. - const { source } = await nextLoad(url, { - ...context, - format: "module", - }); + try { + // Use format 'module' so it returns the source as-is, without stripping the types. + // Format 'commonjs' would not return the source for historical reasons. + const { source } = await nextLoad(url, { + ...context, + format: "module", + }); - // biome-ignore lint/style/noNonNullAssertion: If module exists, it will have a source - const { code, map } = transformSync(source!.toString(), { - mode: "transform", - sourceMap: true, - filename: url, - } as Options); + // biome-ignore lint/style/noNonNullAssertion: If module exists, it will have a source + const { code, map } = transformSync(source!.toString(), { + mode: "transform", + sourceMap: true, + filename: url, + } as Options); - let output = code; + let output = code; - if (map) { - const base64SourceMap = Buffer.from(map).toString("base64"); - output = `${code}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`; - } + if (map) { + const base64SourceMap = Buffer.from(map).toString("base64"); + output = `${code}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`; + } - return { - format: format.replace("-typescript", ""), - source: `${output}\n\n//# sourceURL=${url}`, - }; + return { + format: format.replace("-typescript", ""), + source: `${output}\n\n//# sourceURL=${url}`, + }; + } catch (error) { + if (isSwcError(error)) { + wrapAndReThrowSwcError(error); + } + // If the error is not an SwcError, rethrow it + throw error; + } } return nextLoad(url, context); } diff --git a/test/fixtures/invalid-syntax.ts b/test/fixtures/invalid-syntax.ts new file mode 100644 index 000000000..92c475cb5 --- /dev/null +++ b/test/fixtures/invalid-syntax.ts @@ -0,0 +1,3 @@ +function foo(){ + await Promise.resolve(); +} diff --git a/test/loader.test.js b/test/loader.test.js index 41c4ce657..e56c08c8c 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -24,6 +24,7 @@ test("should not work with enums", async () => { ]); strictEqual(result.stdout, ""); + match(result.stderr, /UnsupportedSyntaxError/); match(result.stderr, /TypeScript enum is not supported in strip-only mode/); strictEqual(result.code, 1); }); @@ -93,3 +94,17 @@ test("should call nextLoader for non-typescript files for transform", async () = match(result.stdout, /Hello, JavaScript!/); strictEqual(result.code, 0); }); + +test("should throw syntax error for invalid typescript", async () => { + const result = await spawnPromisified(process.execPath, [ + "--experimental-strip-types", + "--no-warnings", + "--import=./dist/register-strip.mjs", + fixturesPath("invalid-syntax.ts"), + ]); + + strictEqual(result.stdout, ""); + match(result.stderr, /SyntaxError/); + match(result.stderr, /await isn't allowed in non-async function/); + strictEqual(result.code, 1); +});