nodejs / nodejs/node

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

Ouverte
#64,518 6 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

module strip-types
Langage dominant
JavaScript
Étoiles
122k
Forks
37.3k
Merge moyen
4 j 2 h
PR mergées (30 j)
283

Description

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:

// enable-ts.mjs
import { configureTypeScript } from 'node:module';
configureTypeScript({ mode: 'transform', nodeModules: true });
$ 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)
configuration call that reconfigures the built-in TypeScript pipeline used
by both the CJS and ESM loaders, --eval, and STDIN input.

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).
  • 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):

$ 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:

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:

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:

$ node --import ./ts.mjs ./src/main.ts     # ts.mjs: 2 lines

Monorepo where workspace packages ship raw .ts (symlinked under
node_modules):

// 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:

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).

Guide de contribution

Ouvrir le guide de contribution

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Piste de recherche

Commencez par deps/amaro/dist/index.js et les API node:module existantes stripTypeScriptTypes() et registerHooks() afin de comprendre la capacité de transformation incluse et l’intégration du loader. Définissez l’API publique et son comportement pour le mode transformation, la gestion de node_modules, les source maps, la restauration de la configuration et les interactions avec le cache de compilation ; le travail est considéré comme terminé lorsque l’implémentation, la documentation et la couverture correspondantes sont présentes pour les points d’entrée indiqués.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
javascript, node.js
Domaine
backend
Type d'issue
Fonctionnalité
Difficulté
5/5
Temps estimé
Plus d'une semaine
Activité
Calme
Clarté
Plutôt claire
Accessibilité débutants
35/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.