From a6c4f2a5cac6b8ad80e3d89625c5986ceaae8c91 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Tue, 20 Aug 2024 09:32:07 +0200 Subject: [PATCH] feat: add transform loader --- README.md | 6 ++- esbuild.config.js | 62 +++++++++++++++++++---------- package.json | 10 +++-- src/index.ts | 1 - src/register/register-strip.mjs | 3 ++ src/register/register-transform.mjs | 3 ++ src/register/register.mjs | 3 -- src/{loader.ts => strip-loader.ts} | 13 ++++-- src/transform-loader.ts | 47 ++++++++++++++++++++++ src/transform.ts | 5 +++ test/loader.test.js | 4 +- 11 files changed, 124 insertions(+), 33 deletions(-) create mode 100644 src/register/register-strip.mjs create mode 100644 src/register/register-transform.mjs delete mode 100644 src/register/register.mjs rename src/{loader.ts => strip-loader.ts} (72%) create mode 100644 src/transform-loader.ts diff --git a/README.md b/README.md index 12c74d473..c1102bdc8 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,11 @@ It is possible to use Amaro as an external loader to execute TypeScript files. This allows the installed Amaro to override the Amaro version used by Node.js. ```bash -node --experimental-strip-types --import="amaro/register" script.ts +node --experimental-strip-types --import="amaro/strip" script.ts +``` + +```bash +node --experimental-transform-types --import="amaro/transform" script.ts ``` ### How to update SWC diff --git a/esbuild.config.js b/esbuild.config.js index aa6a08d5d..4e00e2de1 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -1,24 +1,46 @@ const { copy } = require("esbuild-plugin-copy"); -const esbuild = require("esbuild"); +const { build } = require("esbuild"); -esbuild.build({ - entryPoints: ["src/index.ts"], - bundle: true, - platform: "node", - target: "node20", - outdir: "dist", - plugins: [ - copy({ - assets: { - from: ["./src/register/register.mjs"], - to: ["."], - }, - }), - copy({ - assets: { - from: ["./lib/LICENSE", "./lib/package.json"], - to: ["."], - }, - }), +const copyPlugin = copy({ + assets: [ + { + from: ["./src/register/register-strip.mjs"], + to: ["."], + }, + { + from: ["./src/register/register-transform.mjs"], + to: ["."], + }, + { + from: ["./lib/LICENSE", "./lib/package.json"], + to: ["."], + }, ], }); + +(async () => { + await build({ + entryPoints: ["src/index.ts"], + bundle: true, + platform: "node", + target: "node20", + outfile: "dist/index.js", + plugins: [copyPlugin], + }); + + await build({ + entryPoints: ["src/strip-loader.ts"], + bundle: false, + outfile: "dist/strip-loader.js", + platform: "node", + target: "node20", + }); + + await build({ + entryPoints: ["src/transform-loader.ts"], + bundle: false, + outfile: "dist/transform-loader.js", + platform: "node", + target: "node20", + }); +})(); diff --git a/package.json b/package.json index e5f1c2597..c653a84d9 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,11 @@ }, "exports": { ".": "./dist/index.js", - "./register": "./dist/register.mjs" + "./strip": "./dist/register-strip.mjs", + "./transform": "./dist/register-transform.mjs" }, - "files": ["dist", "LICENSE.md"] -} + "files": [ + "dist", + "LICENSE.md" + ] +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 4f96007e2..5e35978f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1 @@ export { transformSync } from "./transform.ts"; -export { load } from "./loader.ts"; diff --git a/src/register/register-strip.mjs b/src/register/register-strip.mjs new file mode 100644 index 000000000..3a6134ba6 --- /dev/null +++ b/src/register/register-strip.mjs @@ -0,0 +1,3 @@ +import { register } from "node:module"; + +register("./strip-loader.js", import.meta.url); diff --git a/src/register/register-transform.mjs b/src/register/register-transform.mjs new file mode 100644 index 000000000..f8c12c34c --- /dev/null +++ b/src/register/register-transform.mjs @@ -0,0 +1,3 @@ +import { register } from "node:module"; + +register("./transform-loader.js", import.meta.url); diff --git a/src/register/register.mjs b/src/register/register.mjs deleted file mode 100644 index 39e7bce12..000000000 --- a/src/register/register.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { register } from "node:module"; - -register("./index.js", import.meta.url); diff --git a/src/loader.ts b/src/strip-loader.ts similarity index 72% rename from src/loader.ts rename to src/strip-loader.ts index 1e4843dcb..1cd0791d6 100644 --- a/src/loader.ts +++ b/src/strip-loader.ts @@ -1,6 +1,6 @@ import type { LoadFnOutput, LoadHookContext } from "node:module"; import type { Options } from "../lib/wasm"; -import { transformSync } from "./index.ts"; +import { transformSync } from "./index.js"; type NextLoad = ( url: string, @@ -20,14 +20,21 @@ export async function load( ...context, format: "module", }); - if (source == null) + + if (source == null) { throw new Error("Source code cannot be null or undefined"); + } + const { code } = transformSync(source.toString(), { mode: "strip-only", } as Options); + return { format: format.replace("-typescript", ""), - source: code, + // 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}`, }; } return nextLoad(url, context); diff --git a/src/transform-loader.ts b/src/transform-loader.ts new file mode 100644 index 000000000..8f1f7748a --- /dev/null +++ b/src/transform-loader.ts @@ -0,0 +1,47 @@ +import type { LoadFnOutput, LoadHookContext } from "node:module"; +import type { Options } from "../lib/wasm"; +import { transformSync } from "./index.js"; + +type NextLoad = ( + url: string, + context?: LoadHookContext, +) => LoadFnOutput | Promise; + +export async function load( + url: string, + context: LoadHookContext, + nextLoad: NextLoad, +) { + 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", + }); + + if (source == null) { + throw new Error("Source code cannot be null or undefined"); + } + + const { code, map } = transformSync(source.toString(), { + mode: "transform", + sourceMap: true, + filename: url, + } as Options); + + let output = code; + + 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, + }; + } + return nextLoad(url, context); +} diff --git a/src/transform.ts b/src/transform.ts index 1a6145b1d..1b09c9f10 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -3,6 +3,11 @@ import swc from "../lib/wasm.js"; const DEFAULT_OPTIONS = { mode: "strip-only", + // default transform will only work when mode is "transform" + transform: { + verbatimModuleSyntax: true, + nativeClassProperties: true, + }, } as Options; export function transformSync( diff --git a/test/loader.test.js b/test/loader.test.js index 2ee276981..895083cda 100644 --- a/test/loader.test.js +++ b/test/loader.test.js @@ -6,7 +6,7 @@ test("should work as a loader", async () => { const result = await spawnPromisified(process.execPath, [ "--experimental-strip-types", "--no-warnings", - "--import=./dist/register.mjs", + "--import=./dist/register-strip.mjs", fixturesPath("hello.ts"), ]); @@ -19,7 +19,7 @@ test("should work with enums", async () => { const result = await spawnPromisified(process.execPath, [ "--experimental-strip-types", "--no-warnings", - "--import=./dist/register.mjs", + "--import=./dist/register-strip.mjs", fixturesPath("enum.ts"), ]);