ITADN

Expose the full Amaro loader API via 'node:module'

#64518Openmcollina 创建于 2026-07-15
modulestrip-types
M
mcollinacommented
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 条评论