Skip to content

Commit 355f56a

Browse files
feat: add transform loader
1 parent 9a61f52 commit 355f56a

12 files changed

+182
-41
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ This allows the installed Amaro to override the Amaro version used by Node.js.
3434
node --experimental-strip-types --import="amaro/register" script.ts
3535
```
3636

37+
Or with the alias:
38+
39+
```bash
40+
node --experimental-strip-types --import="amaro/strip" script.ts
41+
```
42+
43+
Enabling TypeScript feature transformation:
44+
45+
```bash
46+
node --experimental-transform-types --import="amaro/transform" script.ts
47+
```
48+
49+
> Note that the "amaro/transform" loader should be used with `--experimental-transform-types` flag, or
50+
> at least with `--enable-source-maps` flag, to preserve the original source maps.
51+
3752
### How to update SWC
3853

3954
To update the SWC version, run:

esbuild.config.js

Lines changed: 0 additions & 24 deletions
This file was deleted.

esbuild.config.mjs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { build } from "esbuild";
2+
import { copy } from "esbuild-plugin-copy";
3+
4+
const copyPlugin = copy({
5+
assets: [
6+
{
7+
from: ["./src/register/register-strip.mjs"],
8+
to: ["."],
9+
},
10+
{
11+
from: ["./src/register/register-transform.mjs"],
12+
to: ["."],
13+
},
14+
{
15+
from: ["./lib/LICENSE", "./lib/package.json"],
16+
to: ["."],
17+
},
18+
],
19+
});
20+
21+
await build({
22+
entryPoints: ["src/index.ts"],
23+
bundle: true,
24+
platform: "node",
25+
target: "node22",
26+
outfile: "dist/index.js",
27+
plugins: [copyPlugin],
28+
});
29+
30+
await build({
31+
entryPoints: ["src/strip-loader.ts"],
32+
bundle: false,
33+
outfile: "dist/strip-loader.js",
34+
platform: "node",
35+
target: "node22",
36+
});
37+
38+
await build({
39+
entryPoints: ["src/transform-loader.ts"],
40+
bundle: false,
41+
outfile: "dist/transform-loader.js",
42+
platform: "node",
43+
target: "node22",
44+
});

package.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,27 @@
2121
"ci:fix": "biome check --write",
2222
"prepack": "npm run build",
2323
"postpack": "npm run clean",
24-
"build": "node esbuild.config.js",
24+
"build": "node esbuild.config.mjs",
2525
"typecheck": "tsc --noEmit",
2626
"test": "node --test --experimental-test-snapshots \"**/*.test.js\"",
2727
"test:regenerate": "node --test --experimental-test-snapshots --test-update-snapshots \"**/*.test.js\""
2828
},
2929
"devDependencies": {
3030
"@biomejs/biome": "1.8.3",
31-
"@types/node": "^20.14.11",
31+
"@types/node": "^22.0.0",
3232
"esbuild": "^0.23.0",
3333
"esbuild-plugin-copy": "^2.1.1",
3434
"rimraf": "^6.0.1",
3535
"typescript": "^5.5.3"
3636
},
3737
"exports": {
3838
".": "./dist/index.js",
39-
"./register": "./dist/register.mjs"
39+
"./register": "./dist/register-strip.mjs",
40+
"./strip": "./dist/register-strip.mjs",
41+
"./transform": "./dist/register-transform.mjs"
4042
},
41-
"files": ["dist", "LICENSE.md"]
43+
"files": ["dist", "LICENSE.md"],
44+
"engines": {
45+
"node": ">=22"
46+
}
4247
}

src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export { transformSync } from "./transform.ts";
2-
export { load } from "./loader.ts";

src/register/register-strip.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { register } from "node:module";
2+
3+
register("./strip-loader.js", import.meta.url);

src/register/register-transform.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { register } from "node:module";
2+
import { emitWarning, env, execArgv } from "node:process";
3+
4+
const hasSourceMaps =
5+
execArgv.includes("--enable-source-maps") ||
6+
env.NODE_OPTIONS?.includes("--enable-source-maps");
7+
8+
if (!hasSourceMaps) {
9+
emitWarning("Source maps are disabled, stack traces will not accurate");
10+
}
11+
12+
register("./transform-loader.js", import.meta.url);

src/register/register.mjs

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/loader.ts renamed to src/strip-loader.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { LoadFnOutput, LoadHookContext } from "node:module";
22
import type { Options } from "../lib/wasm";
3-
import { transformSync } from "./index.ts";
3+
import { transformSync } from "./index.js";
44

55
type NextLoad = (
66
url: string,
@@ -20,14 +20,16 @@ export async function load(
2020
...context,
2121
format: "module",
2222
});
23-
if (source == null)
24-
throw new Error("Source code cannot be null or undefined");
25-
const { code } = transformSync(source.toString(), {
23+
// biome-ignore lint/style/noNonNullAssertion: If module exists, it will have a source
24+
const { code } = transformSync(source!.toString(), {
2625
mode: "strip-only",
2726
} as Options);
2827
return {
2928
format: format.replace("-typescript", ""),
30-
source: code,
29+
// Source map is not necessary in strip-only mode. However, to map the source
30+
// file in debuggers to the original TypeScript source, add a sourceURL magic
31+
// comment to hint that it is a generated source.
32+
source: `${code}\n\n//# sourceURL=${url}`,
3133
};
3234
}
3335
return nextLoad(url, context);

src/transform-loader.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { LoadFnOutput, LoadHookContext } from "node:module";
2+
import type { Options } from "../lib/wasm";
3+
import { transformSync } from "./index.js";
4+
5+
type NextLoad = (
6+
url: string,
7+
context?: LoadHookContext,
8+
) => LoadFnOutput | Promise<LoadFnOutput>;
9+
10+
export async function load(
11+
url: string,
12+
context: LoadHookContext,
13+
nextLoad: NextLoad,
14+
) {
15+
const { format } = context;
16+
if (format.endsWith("-typescript")) {
17+
// Use format 'module' so it returns the source as-is, without stripping the types.
18+
// Format 'commonjs' would not return the source for historical reasons.
19+
const { source } = await nextLoad(url, {
20+
...context,
21+
format: "module",
22+
});
23+
24+
// biome-ignore lint/style/noNonNullAssertion: If module exists, it will have a source
25+
const { code, map } = transformSync(source!.toString(), {
26+
mode: "transform",
27+
sourceMap: true,
28+
filename: url,
29+
} as Options);
30+
31+
let output = code;
32+
33+
if (map) {
34+
const base64SourceMap = Buffer.from(map).toString("base64");
35+
output = `${code}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`;
36+
}
37+
38+
return {
39+
format: format.replace("-typescript", ""),
40+
source: output,
41+
};
42+
}
43+
return nextLoad(url, context);
44+
}

test/fixtures/stacktrace.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
enum Foo {
2+
A = "Hello, TypeScript!",
3+
}
4+
throw new Error(Foo.A);

test/loader.test.js

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
const { spawnPromisified, fixturesPath } = require("./util/util.js");
22
const { test } = require("node:test");
3-
const { match, strictEqual } = require("node:assert");
3+
const { match, doesNotMatch, strictEqual } = require("node:assert");
44

55
test("should work as a loader", async () => {
66
const result = await spawnPromisified(process.execPath, [
77
"--experimental-strip-types",
88
"--no-warnings",
9-
"--import=./dist/register.mjs",
9+
"--import=./dist/register-strip.mjs",
1010
fixturesPath("hello.ts"),
1111
]);
1212

@@ -15,15 +15,55 @@ test("should work as a loader", async () => {
1515
strictEqual(result.code, 0);
1616
});
1717

18-
test("should work with enums", async () => {
18+
test("should not work with enums", async () => {
1919
const result = await spawnPromisified(process.execPath, [
2020
"--experimental-strip-types",
2121
"--no-warnings",
22-
"--import=./dist/register.mjs",
22+
"--import=./dist/register-strip.mjs",
2323
fixturesPath("enum.ts"),
2424
]);
2525

2626
strictEqual(result.stdout, "");
2727
match(result.stderr, /TypeScript enum is not supported in strip-only mode/);
2828
strictEqual(result.code, 1);
2929
});
30+
31+
test("should work with enums", async () => {
32+
const result = await spawnPromisified(process.execPath, [
33+
"--experimental-strip-types",
34+
"--no-warnings",
35+
"--import=./dist/register-transform.mjs",
36+
fixturesPath("enum.ts"),
37+
]);
38+
39+
match(result.stdout, /Hello, TypeScript!/);
40+
strictEqual(result.stderr, "");
41+
strictEqual(result.code, 0);
42+
});
43+
44+
test("should warn and inaccurate stracktrace", async () => {
45+
const result = await spawnPromisified(process.execPath, [
46+
"--experimental-strip-types",
47+
"--import=./dist/register-transform.mjs",
48+
fixturesPath("stacktrace.ts"),
49+
]);
50+
51+
strictEqual(result.stdout, "");
52+
match(result.stderr, /Source maps are disabled/);
53+
match(result.stderr, /stacktrace.ts:5:7/); // inaccurate
54+
strictEqual(result.code, 1);
55+
});
56+
57+
test("should not warn and accurate stracktrace", async () => {
58+
const result = await spawnPromisified(process.execPath, [
59+
"--experimental-strip-types",
60+
"--enable-source-maps",
61+
"--import=./dist/register-transform.mjs",
62+
fixturesPath("stacktrace.ts"),
63+
]);
64+
65+
doesNotMatch(result.stderr, /Source maps are disabled/);
66+
strictEqual(result.stdout, "");
67+
match(result.stderr, /stacktrace.ts:4:7/); // accurate
68+
strictEqual(result.code, 1);
69+
});

0 commit comments

Comments
 (0)