Expose the full Amaro loader API via 'node:module'
modulestrip-types
Node.js bundles Amaro (currently v1.1.10) and uses it for the default
type-stripping loader, but exposes almost none of it. After
[nodejs/node#61803][] removed `--experimental-transform-types`, there is no
way to run TypeScript syntax that requires transformation
(enums, namespaces with runtime code, parameter properties, import aliases),
and TypeScript files under `node_modules` are always rejected with
`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`.
The escape hatch we document today is `npm install amaro` plus
`--import=amaro/transform` — installing from npm the exact same code that
already ships inside the Node.js binary.
This proposal exposes the bundled Amaro loader as a small, runtime-configurable
API on `node:module`, so that full TypeScript support, including files inside
`node_modules`, is 1–3 lines of code away, with no flags and no dependencies:
```mjs
// enable-ts.mjs
import { configureTypeScript } from 'node:module';
configureTypeScript({ mode: 'transform', nodeModules: true });
```
```console
$ node --import ./enable-ts.mjs app.ts
```
## Motivation
1. **The capability already ships in the binary.** `deps/amaro/dist/index.js`
exports `transformSync(source, { mode: 'transform', sourceMap, filename })`,
and `deps/amaro` even contains the ready-made `transform-loader`. Core only
wires up `mode: 'strip-only'`. Telling users to install `amaro` from npm to
unlock functionality that is compiled into their `node` executable is hard
to justify, adds a supply-chain surface, and risks version skew between the
bundled SWC and the npm one.
2. **The removal of `--experimental-transform-types` left a real gap.**
The flag was removed ([nodejs/node#61803][]) rather than stabilized, which
was the right call for a *default*: erasable-only syntax is the direction
the TypeScript ecosystem is heading (`erasableSyntaxOnly`). But existing
codebases full of enums and runtime namespaces can no longer run on Node.js
directly at all. An explicit, in-code opt-in serves those users without
weakening the default.
3. **The `node_modules` ban is policy, not capability.** We refuse to strip
types under `node_modules` to discourage publishing raw TypeScript to npm.
That is sound as a *default*, but it is the application author — not
Node.js — who pays when a dependency (a monorepo workspace package, a git
dependency, an internal registry package) ships `.ts` files. Today their
only options are a build step or a userland loader. An explicit opt-in
keeps the discouragement (packages still cannot *assume* it works) while
unblocking the people who consciously accept the trade-off.
4. **"On the fly" configuration matches the existing hooks model.**
`module.registerHooks()` already lets code synchronously customize
resolution and loading at runtime. TypeScript configuration should be
equally programmatic instead of frozen at process start.
## Current state
| Capability | Status today |
| ----------------------------------- | ---------------------------------------------------- |
| Type stripping (erasable syntax) | On by default (`--strip-types`) |
| Enums, runtime namespaces, etc. | `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`, no opt-in |
| TypeScript under `node_modules` | `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` |
| `module.stripTypeScriptTypes()` | `mode: 'strip'` only (transform removed in #61803) |
| Amaro `transformSync` full options | Bundled, internal-only |
| Amaro transform loader | Bundled, internal-only; users re-install it from npm |
## Proposed API
### 1. `module.configureTypeScript(options)` — the 1-liner
A process-wide (per-thread, see [Worker threads](#worker-threads))
configuration call that reconfigures the **built-in** TypeScript pipeline used
by both the CJS and ESM loaders, `--eval`, and STDIN input.
```js
const { configureTypeScript } = require('node:module');
configureTypeScript({
// 'strip' (default) erases types only, no source maps needed.
// 'transform' enables full TypeScript syntax: enums, namespaces,
// parameter properties, import aliases. Emits inline source maps.
mode: 'transform',
// Allow transpiling TypeScript files located under node_modules.
// Default: false.
nodeModules: true,
// Emit inline source maps in transform mode. Default: true when
// mode is 'transform' (locations change), ignored in strip mode
// (whitespace replacement preserves locations).
sourceMaps: true,
});
```
Returns the previous configuration, so wrappers can save/restore. Calling it
with no arguments returns the current configuration without changing it.
Semantics:
* Takes effect for every module compiled **after** the call. Already-loaded
modules are not retranspiled; entries already in the on-disk compile cache
made with a different configuration are not reused (the cache key includes
the mode, see [Compile cache](#compile-cache)).
* It configures the *default* steps, so it composes correctly with
`module.registerHooks()` and async `module.register()` hooks: user hooks
still run first and can short-circuit; whatever falls through to the default
load uses the configured mode.
* Works with all existing entry points that support TypeScript today:
`.ts`/`.mts`/`.cts` files via `import`/`require`, `--eval`, STDIN.
* Errors keep their current codes: syntax that even `transform` cannot handle
(e.g. decorators pre-TC39-native) still throws
`ERR_INVALID_TYPESCRIPT_SYNTAX` / `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX` with
the Amaro snippet decoration.
Because the entry point itself must be loadable before the call runs, the
recommended pattern is `--import` (which also covers worker threads via
inherited `execArgv`):
```console
$ node --import ./enable-ts.mjs app.ts
```
For projects whose entry point is plain JS (or strip-compatible TS), calling
it at the top of the entry point works too — that's the true 1-line case:
```js
require('node:module').configureTypeScript({ mode: 'transform', nodeModules: true });
```
### 2. `module.stripTypeScriptTypes()` — restore `mode: 'transform'`
Re-extend the existing public transpiler API (reverting the API-surface part
of #61803) so the full Amaro `transformSync` capability is reachable for
tooling that wants direct source-to-source transforms:
```js
const { stripTypeScriptTypes } = require('node:module');
const js = stripTypeScriptTypes(code, {
mode: 'transform', // 'strip' | 'transform'
sourceMap: true, // transform mode only
sourceUrl: 'file:///app/enums.ts',
});
```
This is the low-level building block: `configureTypeScript()` is sugar over
running this inside the default load step. Exposing both mirrors the
`registerHooks()` philosophy — a convenient default plus composable
primitives. It also means userland loaders (tsx, ts-node, test runners,
coverage tools) can drop their own SWC/esbuild binaries and rely on the copy
Node.js already ships.
### 3. Non-goal: a resolvable `node:amaro` builtin
We deliberately do **not** propose `import amaro from 'node:amaro'`. Amaro is
an implementation detail (a wrapper around a pinned SWC build); branding the
module namespace with it would lock us in. Everything is exposed through
`node:module` under TypeScript-named APIs, keeping the freedom to swap the
underlying transpiler.
## Usage examples
Run a legacy codebase full of enums, unchanged:
```console
$ node --import ./ts.mjs ./src/main.ts # ts.mjs: 2 lines
```
Monorepo where workspace packages ship raw `.ts` (symlinked under
`node_modules`):
```js
// instrument once at the app entry
require('node:module').configureTypeScript({ nodeModules: true });
```
A test runner enabling full support only for the duration of a run:
```js
const prev = configureTypeScript({ mode: 'transform', nodeModules: true });
await runTests();
configureTypeScript(prev);
```
## Trade-offs and risks
* **Ecosystem signaling.** The strongest argument for the status quo is that
strip-only + no-`node_modules` pressures the ecosystem toward erasable,
pre-built packages. This proposal keeps both defaults intact. The opt-in is
code the *application author* writes, exactly like installing `amaro` from
npm today — we are removing an npm round-trip, not changing the default
posture. Package authors still cannot rely on consumers having enabled it.
* **Bundle surface becomes API surface.** Exposing `transform` mode means the
bundled SWC's transform behavior becomes observable and semver-relevant.
It already is, indirectly, through the npm `amaro` package that pins the
same SWC; documenting mode `transform` as release-notes-worthy when Amaro
is bumped is enough.
* **Mid-flight reconfiguration** can produce a process where some modules
were stripped and others transformed. This is the same class of
already-accepted behavior as `registerHooks()` being called at any time;
the docs should recommend configuring once, before loading application
code.
* **Source-map cost.** Transform mode emits inline source maps and benefits
from `--enable-source-maps` for accurate traces; the docs should say so
(the npm loader emits a warning — we can do the same once, lazily).
[nodejs/node#61803]: https://github.com/nodejs/node/pull/61803
[nodejs/typescript#51]: https://github.com/nodejs/typescript/issues/51
2 条评论