From 72d13fed339bfd6fe202a75e880428ccef3eaf1a Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Wed, 7 Aug 2024 14:34:40 +0200 Subject: [PATCH 1/3] feat: create amaro loader --- src/index.ts | 20 ++----------------- src/loader.ts | 14 ++++++++++++++ src/transform.ts | 18 +++++++++++++++++ test/fixtures/enum.ts | 4 ++++ test/fixtures/hello.ts | 1 + test/loader.test.js | 29 ++++++++++++++++++++++++++++ test/util/util.js | 44 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 112 insertions(+), 18 deletions(-) create mode 100644 src/loader.ts create mode 100644 src/transform.ts create mode 100644 test/fixtures/enum.ts create mode 100644 test/fixtures/hello.ts create mode 100644 test/loader.test.js create mode 100644 test/util/util.js diff --git a/src/index.ts b/src/index.ts index 206e3910e..4f96007e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,2 @@ -import type { Options, TransformOutput } from "../lib/wasm"; -import swc from "../lib/wasm.js"; - -const DEFAULT_OPTIONS: Options = { - mode: "strip-only", -}; - -export function transformSync( - source: string, - options: Options, -): TransformOutput { - // Ensure that the source code is a string - const input = `${source ?? ""}`; - return swc.transformSync(input, { - ...DEFAULT_OPTIONS, - ...options, - }); -} +export { transformSync } from "./transform.ts"; +export { load } from "./loader.ts"; diff --git a/src/loader.ts b/src/loader.ts new file mode 100644 index 000000000..78b07b981 --- /dev/null +++ b/src/loader.ts @@ -0,0 +1,14 @@ +import swc from "../lib/wasm.js"; +import type { LoadHook } from "node:module"; +import { readFile } from "node:fs/promises"; + +export const load: LoadHook = async (source, context, nextLoad) => { + if (context.format?.includes("typescript")) { + const data = await readFile(source, "utf8"); + return { + source: swc.transformSync(data).code, + shortCircuit: true, // Skips bundled transpilation + }; + } + return { source: await nextLoad(source, context) }; +}; diff --git a/src/transform.ts b/src/transform.ts new file mode 100644 index 000000000..206e3910e --- /dev/null +++ b/src/transform.ts @@ -0,0 +1,18 @@ +import type { Options, TransformOutput } from "../lib/wasm"; +import swc from "../lib/wasm.js"; + +const DEFAULT_OPTIONS: Options = { + mode: "strip-only", +}; + +export function transformSync( + source: string, + options: Options, +): TransformOutput { + // Ensure that the source code is a string + const input = `${source ?? ""}`; + return swc.transformSync(input, { + ...DEFAULT_OPTIONS, + ...options, + }); +} diff --git a/test/fixtures/enum.ts b/test/fixtures/enum.ts new file mode 100644 index 000000000..0a5d0414c --- /dev/null +++ b/test/fixtures/enum.ts @@ -0,0 +1,4 @@ +enum Foo { + A = "Hello, TypeScript!" +} +console.log(Foo.A); diff --git a/test/fixtures/hello.ts b/test/fixtures/hello.ts new file mode 100644 index 000000000..b0456b2e2 --- /dev/null +++ b/test/fixtures/hello.ts @@ -0,0 +1 @@ +console.log("Hello, TypeScript!"); diff --git a/test/loader.test.js b/test/loader.test.js new file mode 100644 index 000000000..f6d0e68c6 --- /dev/null +++ b/test/loader.test.js @@ -0,0 +1,29 @@ +const { spawnPromisified, fixturesPath } = require("./util/util.js"); +const { test } = require("node:test"); +const { match, strictEqual } = require("node:assert"); + +test("should work as a loader", async () => { + const result = await spawnPromisified(process.execPath, [ + "--experimental-strip-types", + "--no-warnings", + "--import=./dist/index.js", + fixturesPath("hello.ts"), + ]); + + strictEqual(result.stderr, ""); + match(result.stdout, /Hello, TypeScript!/); + strictEqual(result.code, 0); +}); + +test("should work with enums", async () => { + const result = await spawnPromisified(process.execPath, [ + "--experimental-strip-types", + "--no-warnings", + "--import=./dist/index.js", + fixturesPath("enum.ts"), + ]); + + strictEqual(result.stderr, ""); + match(result.stdout, /Hello, TypeScript!/); + strictEqual(result.code, 0); +}); diff --git a/test/util/util.js b/test/util/util.js new file mode 100644 index 000000000..a57ea8653 --- /dev/null +++ b/test/util/util.js @@ -0,0 +1,44 @@ +const { spawn } = require("node:child_process"); +const path = require("node:path"); + +function spawnPromisified(...args) { + let stderr = ""; + let stdout = ""; + + const child = spawn(...args); + child.stderr.setEncoding("utf8"); + child.stderr.on("data", (data) => { + stderr += data; + }); + child.stdout.setEncoding("utf8"); + child.stdout.on("data", (data) => { + stdout += data; + }); + + return new Promise((resolve, reject) => { + child.on("close", (code, signal) => { + resolve({ + code, + signal, + stderr, + stdout, + }); + }); + child.on("error", (code, signal) => { + reject({ + code, + signal, + stderr, + stdout, + }); + }); + }); +} + +const fixturesDir = path.join(__dirname, "..", "fixtures"); + +function fixturesPath(...args) { + return path.join(fixturesDir, ...args); +} + +module.exports = { spawnPromisified, fixturesPath }; From 6c23c6eb4f2f6c91e9d058cae023723d3b65e867 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Zasso?= Date: Wed, 7 Aug 2024 16:17:24 +0200 Subject: [PATCH 2/3] chore: replace rspack with esbuild --- package.json | 10 ++++++---- rspack.config.js | 31 ------------------------------- 2 files changed, 6 insertions(+), 35 deletions(-) delete mode 100644 rspack.config.js diff --git a/package.json b/package.json index 87abda934..f0090da30 100644 --- a/package.json +++ b/package.json @@ -21,18 +21,20 @@ "ci:fix": "biome check --write", "prepack": "npm run build", "postpack": "npm run clean", - "build": "rspack build", + "build": "esbuild --bundle --platform=node --target=node18 --outdir=dist src/index.ts", "typecheck": "tsc --noEmit", "test": "node --test --experimental-test-snapshots \"**/*.test.js\"", "test:regenerate": "node --test --experimental-test-snapshots --test-update-snapshots \"**/*.test.js\"" }, "devDependencies": { "@biomejs/biome": "1.8.3", - "@rspack/cli": "^0.7.5", - "@rspack/core": "^0.7.5", "@types/node": "^20.14.11", + "esbuild": "^0.23.0", "rimraf": "^6.0.1", "typescript": "^5.5.3" }, - "files": ["dist", "LICENSE.md"] + "files": [ + "dist", + "LICENSE.md" + ] } diff --git a/rspack.config.js b/rspack.config.js deleted file mode 100644 index eb0fb23aa..000000000 --- a/rspack.config.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = { - target: "node", - module: { - rules: [ - { - test: /\.ts$/, - exclude: [/node_modules/], - loader: "builtin:swc-loader", - options: { - jsc: { - parser: { - syntax: "typescript", - }, - target: "es2022", - }, - }, - type: "javascript/auto", - }, - ], - }, - output: { - filename: "index.js", - library: { - type: "commonjs2", - }, - }, - optimization: { - minimize: false, - }, - devtool: false, -}; From 429744b9a77d379050a5602a066743d26dd04348 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Wed, 7 Aug 2024 17:42:59 +0200 Subject: [PATCH 3/3] fix: now working as a loader --- README.md | 11 ++++++++++- esbuild.config.js | 18 ++++++++++++++++++ package.json | 12 +++++++----- src/loader.ts | 38 ++++++++++++++++++++++++++++---------- src/register/register.mjs | 3 +++ src/transform.ts | 2 +- test/fixtures/enum.ts | 2 +- test/loader.test.js | 10 +++++----- 8 files changed, 73 insertions(+), 23 deletions(-) create mode 100644 esbuild.config.js create mode 100644 src/register/register.mjs diff --git a/README.md b/README.md index 0a22d289d..12c74d473 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,19 @@ Stack traces are preserved, by replacing removed types with white spaces. ```javascript const amaro = require('amaro'); -const { code } = amaro.transformSync("const foo: string = 'bar';"); +const { code } = amaro.transformSync("const foo: string = 'bar';", { mode: "strip-only" }); console.log(code); // "const foo = 'bar';" ``` +### Loader + +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 +``` + ### How to update SWC To update the SWC version, run: diff --git a/esbuild.config.js b/esbuild.config.js new file mode 100644 index 000000000..72fe6f71a --- /dev/null +++ b/esbuild.config.js @@ -0,0 +1,18 @@ +const { copy } = require("esbuild-plugin-copy"); +const esbuild = 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: ["."], + }, + }), + ], +}); diff --git a/package.json b/package.json index f0090da30..7c77a9ee4 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "ci:fix": "biome check --write", "prepack": "npm run build", "postpack": "npm run clean", - "build": "esbuild --bundle --platform=node --target=node18 --outdir=dist src/index.ts", + "build": "node esbuild.config.js", "typecheck": "tsc --noEmit", "test": "node --test --experimental-test-snapshots \"**/*.test.js\"", "test:regenerate": "node --test --experimental-test-snapshots --test-update-snapshots \"**/*.test.js\"" @@ -30,11 +30,13 @@ "@biomejs/biome": "1.8.3", "@types/node": "^20.14.11", "esbuild": "^0.23.0", + "esbuild-plugin-copy": "^2.1.1", "rimraf": "^6.0.1", "typescript": "^5.5.3" }, - "files": [ - "dist", - "LICENSE.md" - ] + "exports": { + ".": "./dist/index.js", + "./register": "./dist/register.mjs" + }, + "files": ["dist", "LICENSE.md"] } diff --git a/src/loader.ts b/src/loader.ts index 78b07b981..58a1737cd 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -1,14 +1,32 @@ -import swc from "../lib/wasm.js"; -import type { LoadHook } from "node:module"; -import { readFile } from "node:fs/promises"; +import assert from "node:assert"; +import type { LoadFnOutput, LoadHookContext } from "node:module"; +import { transformSync } from "./index.ts"; -export const load: LoadHook = async (source, context, nextLoad) => { - if (context.format?.includes("typescript")) { - const data = await readFile(source, "utf8"); +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 } = transformSync(source.toString(), { mode: "strip-only" }); return { - source: swc.transformSync(data).code, - shortCircuit: true, // Skips bundled transpilation + format: format.replace("-typescript", ""), + source: code, }; } - return { source: await nextLoad(source, context) }; -}; + return nextLoad(url, context); +} diff --git a/src/register/register.mjs b/src/register/register.mjs new file mode 100644 index 000000000..39e7bce12 --- /dev/null +++ b/src/register/register.mjs @@ -0,0 +1,3 @@ +import { register } from "node:module"; + +register("./index.js", import.meta.url); diff --git a/src/transform.ts b/src/transform.ts index 206e3910e..dabf4b718 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -7,7 +7,7 @@ const DEFAULT_OPTIONS: Options = { export function transformSync( source: string, - options: Options, + options?: Options, ): TransformOutput { // Ensure that the source code is a string const input = `${source ?? ""}`; diff --git a/test/fixtures/enum.ts b/test/fixtures/enum.ts index 0a5d0414c..c510a608e 100644 --- a/test/fixtures/enum.ts +++ b/test/fixtures/enum.ts @@ -1,4 +1,4 @@ enum Foo { - A = "Hello, TypeScript!" + A = "Hello, TypeScript!", } console.log(Foo.A); diff --git a/test/loader.test.js b/test/loader.test.js index f6d0e68c6..2ee276981 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/index.js", + "--import=./dist/register.mjs", fixturesPath("hello.ts"), ]); @@ -19,11 +19,11 @@ test("should work with enums", async () => { const result = await spawnPromisified(process.execPath, [ "--experimental-strip-types", "--no-warnings", - "--import=./dist/index.js", + "--import=./dist/register.mjs", fixturesPath("enum.ts"), ]); - strictEqual(result.stderr, ""); - match(result.stdout, /Hello, TypeScript!/); - strictEqual(result.code, 0); + strictEqual(result.stdout, ""); + match(result.stderr, /TypeScript enum is not supported in strip-only mode/); + strictEqual(result.code, 1); });