Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,27 @@ jobs:
fail-fast: false
matrix:
node:
- 22
- 24
- "22.x"
- "24.x"
platform:
- ubuntu-latest
- macos-latest
- windows-latest
name: "${{matrix.platform}} w/ Node.js ${{matrix.node}}.x"
include:
- node: "22.14.0"
platform: ubuntu-latest
node_options: "--experimental-strip-types"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the changes in this file are unnecessary

name: "${{matrix.platform}} w/ Node.js ${{matrix.node}}"
runs-on: ${{matrix.platform}}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: "Use Node.js ${{matrix.node}}.x"
- name: "Use Node.js ${{matrix.node}}"
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{matrix.node}}.x
node-version: ${{matrix.node}}
- run: npm install
- run: npm run typecheck
- run: npm run build
- run: npm test
env:
NODE_OPTIONS: ${{matrix.node_options}}
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,14 @@ node --enable-source-maps --import="amaro/transform" file.ts
> Note that the `amaro/transform` loader should be used with `--enable-source-maps`
> to preserve accurate source-mapped stack traces.

#### Programmatic registration with `module.register()`
#### Programmatic loading

If you want TypeScript to "just work" in an existing codebase without passing `--import` every time, create a small bootstrap file and register Amaro once before loading your TS entrypoint.
If you want TypeScript to "just work" in an existing codebase without passing `--import` every time, create a small bootstrap file and import Amaro before loading your TS entrypoint.

```mjs
// bootstrap.mjs
import { register } from "node:module";
import "amaro/strip";

register("amaro/strip", import.meta.url);
await import("./src/index.ts");
```

Expand All @@ -65,7 +64,7 @@ Then start your app through the bootstrap file:
node --watch ./bootstrap.mjs
```

For transform mode, swap `amaro/strip` with `amaro/transform` and run Node with `--enable-source-maps`.
For transform mode, import `amaro/transform` instead and run Node with `--enable-source-maps`.

### Monorepo usage

Expand Down
18 changes: 17 additions & 1 deletion esbuild.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { build } from "esbuild";
import { copy } from "esbuild-plugin-copy";

await build({
entryPoints: ["src/*.ts"],
entryPoints: [
"src/errors.ts",
"src/index.ts",
"src/nodejs.ts",
"src/transform.ts",
],
bundle: false,
outdir: "dist",
outbase: "src",
Expand All @@ -24,3 +29,14 @@ await build({
}),
],
});

await build({
entryPoints: ["src/*-loader.ts"],
bundle: false,
outdir: "dist",
outbase: "src",
outExtension: { ".js": ".mjs" },
platform: "node",
format: "esm",
Comment on lines +38 to +40
target: "node22",
});
9 changes: 7 additions & 2 deletions src/register/register-strip.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { register } from "node:module";
import * as module from "node:module";

register("./strip-loader.js", import.meta.url);
if (typeof module.registerHooks === "function") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can try to import registerHooks and register instead of the whole module

const { loadSync } = await import("./strip-loader.mjs");
module.registerHooks({ load: loadSync });
} else {
module.register("./strip-loader.mjs", import.meta.url);
}
9 changes: 7 additions & 2 deletions src/register/register-transform.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { register } from "node:module";
import * as module from "node:module";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

import { emitWarning, env, execArgv } from "node:process";

const hasSourceMaps =
Expand All @@ -9,4 +9,9 @@ if (!hasSourceMaps) {
emitWarning("Source maps are disabled, stack traces will not be accurate");
}

register("./transform-loader.js", import.meta.url);
if (typeof module.registerHooks === "function") {
const { loadSync } = await import("./transform-loader.mjs");
module.registerHooks({ load: loadSync });
} else {
module.register("./transform-loader.mjs", import.meta.url);
}
65 changes: 40 additions & 25 deletions src/strip-loader.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
import type { LoadFnOutput, LoadHookContext } from "node:module";
import type { LoadFnOutput, LoadHook, LoadHookSync } from "node:module";
import { fileURLToPath } from "node:url";
import { isSwcError, wrapAndReThrowSwcError } from "./errors.js";
import { transformSync } from "./index.js";

export async function load(
function stripTypeScript(
url: string,
context: LoadHookContext,
nextLoad: (
url: string,
context?: LoadHookContext,
) => LoadFnOutput | Promise<LoadFnOutput>,
) {
format: string,
source: LoadFnOutput["source"],
): LoadFnOutput {
// biome-ignore lint/style/noNonNullAssertion: If module exists, it will have a source
const { code } = transformSync(source!.toString(), {
mode: "strip-only",
filename: fileURLToPath(url),
});
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}`,
};
}

export const load: LoadHook = async (url, context, nextLoad) => {
const { format } = context;
if (format?.endsWith("-typescript")) {
try {
const { source } = await nextLoad(url, {
...context,
format,
});
// biome-ignore lint/style/noNonNullAssertion: If module exists, it will have a source
const { code } = transformSync(source!.toString(), {
mode: "strip-only",
filename: fileURLToPath(url),
});
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}`,
};
const { source } = await nextLoad(url, { ...context, format });
return stripTypeScript(url, format, source);
} catch (error: unknown) {
if (isSwcError(error)) {
wrapAndReThrowSwcError(error);
Expand All @@ -39,4 +37,21 @@ export async function load(
}
}
return nextLoad(url, context);
}
};

export const loadSync: LoadHookSync = (url, context, nextLoad) => {
const { format } = context;
if (format?.endsWith("-typescript")) {
try {
const { source } = nextLoad(url, { ...context, format });
return stripTypeScript(url, format, source);
} catch (error: unknown) {
if (isSwcError(error)) {
wrapAndReThrowSwcError(error);
}
// If the error is not an SwcError, rethrow it
throw error;
}
}
return nextLoad(url, context);
};
74 changes: 44 additions & 30 deletions src/transform-loader.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,57 @@
import type { LoadFnOutput, LoadHookContext } from "node:module";
import type { LoadFnOutput, LoadHook, LoadHookSync } from "node:module";
import { fileURLToPath } from "node:url";
import type { Options } from "../lib/wasm";
import { isSwcError, wrapAndReThrowSwcError } from "./errors.js";
import { transformSync } from "./index.js";

export async function load(
function transformTypeScript(
url: string,
context: LoadHookContext,
nextLoad: (
url: string,
context?: LoadHookContext,
) => LoadFnOutput | Promise<LoadFnOutput>,
) {
const { format } = context;
if (format?.endsWith("-typescript")) {
try {
const { source } = await nextLoad(url, {
...context,
format,
});
format: string,
source: LoadFnOutput["source"],
): LoadFnOutput {
// biome-ignore lint/style/noNonNullAssertion: If module exists, it will have a source
const { code, map } = transformSync(source!.toString(), {
mode: "transform",
sourceMap: true,
filename: fileURLToPath(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: fileURLToPath(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}`;
}

return {
format: format.replace("-typescript", ""),
source: `${output}\n\n//# sourceURL=${url}`,
};
}

if (map) {
const base64SourceMap = Buffer.from(map).toString("base64");
output = `${code}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`;
export const load: LoadHook = async (url, context, nextLoad) => {
const { format } = context;
if (format?.endsWith("-typescript")) {
try {
const { source } = await nextLoad(url, { ...context, format });
return transformTypeScript(url, format, source);
} catch (error) {
if (isSwcError(error)) {
wrapAndReThrowSwcError(error);
}
// If the error is not an SwcError, rethrow it
throw error;
}
}
return nextLoad(url, context);
};

return {
format: format.replace("-typescript", ""),
source: `${output}\n\n//# sourceURL=${url}`,
};
export const loadSync: LoadHookSync = (url, context, nextLoad) => {
const { format } = context;
if (format?.endsWith("-typescript")) {
try {
const { source } = nextLoad(url, { ...context, format });
return transformTypeScript(url, format, source);
} catch (error) {
if (isSwcError(error)) {
wrapAndReThrowSwcError(error);
Expand All @@ -47,4 +61,4 @@ export async function load(
}
}
return nextLoad(url, context);
}
};
6 changes: 3 additions & 3 deletions test/loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ const { spawnPromisified, fixturesPath } = require("./util/util.js");
const { test } = require("node:test");
const { match, doesNotMatch, strictEqual } = require("node:assert");

test("should work as a loader", async () => {
test("should work as a loader without warnings", async () => {
const result = await spawnPromisified(process.execPath, [
"--no-warnings",
"--import=./dist/register-strip.mjs",
fixturesPath("hello.ts"),
]);
Expand All @@ -29,7 +28,7 @@ test("should not work with enums", async (t) => {

test("should work with enums", async () => {
const result = await spawnPromisified(process.execPath, [
"--no-warnings",
"--enable-source-maps",
"--import=./dist/register-transform.mjs",
fixturesPath("enum.ts"),
]);
Expand All @@ -47,6 +46,7 @@ test("should warn and inaccurate stracktrace", async () => {

strictEqual(result.stdout, "");
match(result.stderr, /Source maps are disabled/);
doesNotMatch(result.stderr, /DEP0205/);
match(result.stderr, /stacktrace.ts:7:11/); // inaccurate
strictEqual(result.code, 1);
});
Expand Down
Loading