nodejs / nodejs/node

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

Đang mở
#64,518 6 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

module strip-types
Ngôn ngữ chính
JavaScript
Star
122k
Fork
37.3k
Merge trung bình
4 ngày 2 giờ
Pull request đã merge (30 ngày)
283

Mô tả

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

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu với deps/amaro/dist/index.js và các API node:module hiện có là stripTypeScriptTypes() và registerHooks() để hiểu khả năng transform được đóng gói và việc tích hợp loader. Xác định API công khai và hành vi của API này đối với transform mode, cách xử lý node_modules, source maps, việc khôi phục cấu hình và các tương tác với compile cache; công việc được xem là hoàn tất khi có implementation, documentation và coverage tương ứng cho các entry point được liệt kê.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
javascript, node.js
Lĩnh vực
backend
Loại issue
Tính năng
Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
35/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.