Skip to content

Commit

Permalink
feat: wrap and rethrow swc errors
Browse files Browse the repository at this point in the history
  • Loading branch information
marco-ippolito committed Jan 10, 2025
1 parent 35efcea commit 6dcdfcc
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 50 deletions.
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"deps/**",
"lib/**",
"node_modules/**",
"test/snapshot/**"
"test/snapshot/**",
"test/fixtures/**"
]
}
}
7 changes: 7 additions & 0 deletions esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -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 error;
}
}
50 changes: 28 additions & 22 deletions src/strip-loader.ts
Original file line number Diff line number Diff line change
@@ -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<LoadFnOutput>;

export async function load(
url: string,
context: LoadHookContext,
nextLoad: NextLoad,
nextLoad: (
url: string,
context?: LoadHookContext,
) => LoadFnOutput | Promise<LoadFnOutput>,
) {
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);
}
61 changes: 34 additions & 27 deletions src/transform-loader.ts
Original file line number Diff line number Diff line change
@@ -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<LoadFnOutput>;

export async function load(
url: string,
context: LoadHookContext,
nextLoad: NextLoad,
nextLoad: (
url: string,
context?: LoadHookContext,
) => LoadFnOutput | Promise<LoadFnOutput>,
) {
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);
}
3 changes: 3 additions & 0 deletions test/fixtures/invalid-syntax.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function foo(){
await Promise.resolve();

Check failure on line 2 in test/fixtures/invalid-syntax.ts

View workflow job for this annotation

GitHub Actions / ubuntu-latest w/ Node.js 22.x

'await' expressions are only allowed within async functions and at the top levels of modules.

Check failure on line 2 in test/fixtures/invalid-syntax.ts

View workflow job for this annotation

GitHub Actions / macos-latest w/ Node.js 22.x

'await' expressions are only allowed within async functions and at the top levels of modules.

Check failure on line 2 in test/fixtures/invalid-syntax.ts

View workflow job for this annotation

GitHub Actions / windows-latest w/ Node.js 22.x

'await' expressions are only allowed within async functions and at the top levels of modules.

Check failure on line 2 in test/fixtures/invalid-syntax.ts

View workflow job for this annotation

GitHub Actions / ubuntu-latest w/ Node.js 23.x

'await' expressions are only allowed within async functions and at the top levels of modules.

Check failure on line 2 in test/fixtures/invalid-syntax.ts

View workflow job for this annotation

GitHub Actions / macos-latest w/ Node.js 23.x

'await' expressions are only allowed within async functions and at the top levels of modules.

Check failure on line 2 in test/fixtures/invalid-syntax.ts

View workflow job for this annotation

GitHub Actions / windows-latest w/ Node.js 23.x

'await' expressions are only allowed within async functions and at the top levels of modules.
}
15 changes: 15 additions & 0 deletions test/loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down Expand Up @@ -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);
});

0 comments on commit 6dcdfcc

Please sign in to comment.