@ 261027.7

250113.5 · 87641224c949422f1d1b2734309bc1d907df69de

feat: wrap and rethrow swc errors

reading declarations

biome.json

@@ -19,7 +19,8 @@ 			"deps/**", 			"lib/**", 			"node_modules/**",-			"test/snapshot/**"+			"test/snapshot/**",+			"test/fixtures/**" 		] 	} }

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,

src/errors.ts

new file mode 100644@@ -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);+	}+}

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<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); }

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<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); }

test/fixtures/invalid-syntax.ts

new file mode 100644@@ -0,0 +1,3 @@+function foo(){+    await Promise.resolve();+}

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);+});