Skip to content

node:module: stripTypeScriptTypes strip mode + StrippedTypeScript compile cache (+3 tests) - #35517

Draft
cirospaciari wants to merge 15 commits into
claude/node-v26-fix-tlsfrom
claude/node-strip-types
Draft

cirospaciari wants to merge 15 commits into
claude/node-v26-fix-tlsfrom
claude/node-strip-types

Conversation

@cirospaciari

Copy link
Copy Markdown
Member

Implements module.stripTypeScriptTypes(code[, options]) with Node v26 semantics, reports process.config.variables.node_use_amaro: true, and adds Node's StrippedTypeScript transpilation-cache entry type to the NODE_COMPILE_CACHE emulation. Vendors 3 upstream tests this unlocks (all previously skipped on the node_use_amaro gate).

Why strip mode is not a transpiler wrapper

Node v26.0.0 removed the transform mode and sourceMap option (nodejs/node#61803); the only remaining mode, 'strip', replaces type syntax in place with whitespace so line/column positions in the output equal the input:

stripTypeScriptTypes('const x: number = 1;') === 'const x         = 1;'

Bun's transpiler re-prints from the AST, which cannot preserve positions, so strip mode is a dedicated pass ported from amaro's swc_ts_fast_strip (the exact library Node embeds):

  1. Lexer (js_parser/lexer.rs): captures the token stream when track_tokens is set — same pattern as the existing track_comments, truncated on backtracking via the snapshot lengths.
  2. Parser: records the byte span of every type-only construct while skipping it (P::ts_strip; ~30 recording sites across parse_*.rs). Every site is behind an Option null-check that is None for every normal parse; erased statements are recorded at the S::TypeScript construction sites the parser already has.
  3. Post-pass (js_parser/ts_strip.rs, new): applies swc's algorithm over spans + tokens — whitespace substitution preserving newlines and UTF-8 character widths (U+00A0/U+2002 for multi-byte), ASI-protection semicolons (let x = 1⏎type A = string⏎(f)() gets a ; written into the blank), the generic-arrow <→( rewrites, and ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX rejections (enum, namespace, parameter properties, import =, export =, <T>expr assertions, identifier-named module) with amaro's exact messages. Erased ambient (declare) containers suppress inner rejections by span containment, mirroring swc's unvisited subtrees.

Validation lives in internal/shared.ts and matches lib/internal/modules/typescript.js check-for-check: argument order, ERR_INVALID_ARG_TYPE/ERR_INVALID_ARG_VALUE, once-per-process ExperimentalWarning, sourceURL suffix, and the filename:line\n<snippet> stack decoration. Parse errors throw ERR_INVALID_TYPESCRIPT_SYNTAX; both codes are SyntaxError subclasses like Node.

Verified against the oracle: a 156-case differential corpus (every construct class: annotations, generics, as/satisfies/const-assertions, class modifiers/overloads/index signatures, import/export type specifiers, ASI hazards, multi-byte identifiers, comments, hashbang) runs byte-identical to node v26.3.0 on 153 cases. The 3 divergences are: parse-error message text comes from Bun's parser (code/class match; 2 cases), and class C { override m() {} } without extends is an swc-parser-only strictness error Bun's parser accepts (1 case).

node_use_amaro

Node builds with amaro report process.config.variables.node_use_amaro: true; six upstream tests skip on it. With the API implemented the flag now reports true. No other vendored test reads the flag (checked the whole vendored tree).

StrippedTypeScript compile-cache entries

Node keeps a second NODE_COMPILE_CACHE entry per TypeScript file: the transpilation cache (CachedCodeType::kStrippedTypeScript), keyed by the raw source. NodeCompileCache.rs now models entry types as an enum (CommonJs/Esm/StrippedTypeScript, discriminants preserve the old is_cjs key salt so existing cache dirs stay valid) and records a transpilation entry when a TS module is transpiled, with Node's log lines (saving transpilation cache…, retrieving transpile cache… success, writing cache for StrippedTypeScript… success, skip persisting… because cache was the same) and full on-disk validation. The stored transpiled text is validated on reload but not yet fed back to the loader (Bun's own runtime transpiler cache already skips re-transpiling); noted in a comment. The code-cache accepted line now says V8 code cache for… matching Node's wording (compile_cache.cc:318) — the looser regex in the previously-vendored compile-cache tests still matches, verified by re-running all 14 of them.

Vendored tests (byte-verbatim, all run their bodies)

  • test-module-strip-types.js — 6/6 subtests pass (skips as "Requires Amaro" on unfixed builds)
  • test-compile-cache-typescript-commonjs.js — .ts/.cts/.mts loads with both cache-entry kinds across two runs
  • test-compile-cache-typescript-esm.js

Plus test/js/node/module/strip-typescript-types.test.ts (15 tests, every expected string captured from Node v26.3.0; fails on unfixed builds — the export doesn't exist).

Evaluated and not vendored (with reasons)

  • test-util-getcallsites — needs getCallSites().length > 1 at module top level; Bun's CJS loader is native so only the file frame exists (Node's JS loader frames pad the stack).
  • test-compile-cache-typescript-strip-sourcemaps — asserts CommonJS classification for an import-less .ts entry; Bun classifies it ESM. Changing Bun's module-type default is not on the table for a compat test.
  • test-inspector-strip-types — needs the inspector work that lives on claude/node-v26-combined-34719; hangs on this base.
  • test-node-output-eval.mjs — snapshot-asserts strip-only execution semantics (-p 'enum Foo{}' must fail with the strip error); Bun executes TS with transform semantics by design.

Carried repairs

The first two commits are cherry-picks of the base-branch merge-damage repairs (reject_bad_negations field and the -e/-p eval binding — bun -e code printed help and exited 0 on the current base, breaking every subprocess test). They already exist on sibling session branches and will drop out when the base is fixed.

Known pre-existing on this base (not this PR): transpiler.test.js "deeply nested unary operators" fails because bun -e '- - - …' (code starting with -) exits with the wrong code after the -e/-p rewrite; reproduces on the base + repairs without this PR's changes.

…or messages

The merge of claude/node-v26-permission-wave2 dropped the
reject_bad_negations field from the ParseOptions initializer in
Arguments.rs (added by the cli negation-errors commit), breaking the
build, and resolved three node_fs.rs call sites back to the pre-parity
generic 'path must be a string' errors, orphaning
PathOrFdExt::from_js_required. Restores the field and the
from_js_required calls; deletes BUFFER_EXPECTED_TYPES, superseded by
throw_invalid_argument_type_list at its only former call site.
…strip mode

Node v26's stripTypeScriptTypes (amaro/swc_ts_fast_strip) blanks type-only
syntax in place so line/column positions match the input; the transform mode
and sourceMap option were removed upstream (nodejs/node#61803). Bun's
transpiler re-prints from the AST and cannot preserve positions, so strip
mode is a dedicated pass:

- the lexer captures the token stream when track_tokens is set (same
  pattern as track_comments; snapshot/restore truncates it)
- the parser records the byte span of each type-only construct while
  skipping it (P::ts_strip, recording sites across parse_*.rs, each behind
  an Option check that is None for every normal parse)
- js_parser/ts_strip.rs ports swc_ts_fast_strip's post-pass: whitespace
  substitution preserving newlines and character widths, ASI-protection
  semicolons, generic-arrow rewrites, and the unsupported-syntax
  rejections (enum, namespace, parameter properties, import =, export =,
  angle-bracket assertions, grouping-changing casts) with amaro's messages

module.stripTypeScriptTypes validates like lib/internal/modules/
typescript.js (ERR_INVALID_ARG_TYPE/VALUE order, once-per-process
ExperimentalWarning, sourceURL suffix) in internal/shared.ts and calls the
native pass; parse errors map to ERR_INVALID_TYPESCRIPT_SYNTAX and
unsupported constructs to ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, both
SyntaxError like Node.

process.config.variables.node_use_amaro now reports true, matching a Node
build with amaro, which un-skips the upstream tests gated on it.

NODE_COMPILE_CACHE learns Node's StrippedTypeScript entry type: TypeScript
modules record a transpilation-cache entry keyed by the raw source next to
their CommonJS/ESM code-cache entry, with Node's log lines (saving/
retrieving transpile cache, writing/skip persisting) and on-disk
validation; the code-cache accepted line now matches Node's V8-prefixed
wording.

Vendored: test-module-strip-types, test-util-getcallsites,
test-compile-cache-typescript-{commonjs,esm,strip-sourcemaps}.
The 'Merge origin/claude/node-v26-combined-34660' merge (de14ef6)
resolved src/runtime/cli/Arguments.rs and src/js/node/util.ts wholesale
to the pre-merge side, silently reverting the combined branch's work:

- Arguments.rs lost the -e/-p/--print value-binding rewrite (including
  every 'eval.provided = true' write, whose consumer in mod.rs survived),
  so 'bun -e code' printed the help text and exited 0 — breaking every
  subprocess spawned with -e. Also lost: --check/-c, --input-type,
  --inspect-port/--debug-port parsing, NODE_OPTIONS validation, and the
  --no-<flag> negation errors. Restored via a proper 3-way merge against
  the original merge base, keeping the later permission-wave2 hunks.
- util.ts lost util.diff() (its myers_diff backend survived unreferenced).

readline.ts was also resolved to one side, but its dropped hunks are
superseded by the newer promises rework already on this branch; no
change needed there.
…ct classes

- drop the binary-grouping rejection: the swc_ts_fast_strip revision in
  Node v26.3.0's amaro predates it, so no input triggers it there
- blank the 'abstract' keyword of abstract class statements
- leave 'export as namespace ns;' verbatim (no swc visitor erases it)
- un-vendor test-util-getcallsites (needs >1 call-site frame at module
  top level; Bun's CJS loader is native so only the file frame exists)
  and test-compile-cache-typescript-strip-sourcemaps (asserts CommonJS
  classification for an import-less .ts entry; Bun classifies it ESM)
@robobun

robobun commented Jul 25, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 11:37 PM PT - Aug 21st, 2026

❌ @robobun, your commit 6cb3c82 has 4 failures in Build #103289 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 35517

That installs a local version of the PR into your bun-35517 executable, so you can run:

bun-35517 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. node:module does not export (or implement) stripTypeScriptTypes #32196 - Implements the missing module.stripTypeScriptTypes export and functionality
  2. node:module stripTypeScriptTypes is documented but is not actually supported #25058 - Adds the actual stripTypeScriptTypes implementation that was documented but not supported
  3. Add Node's util.diff and show Bun support in Bun's Node reference #20396 - Implements util.diff() (Myers diff algorithm port from Node.js)

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #32196
Fixes #25058
Fixes #20396

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. node:module: implement stripTypeScriptTypes #32206 - Also implements module.stripTypeScriptTypes in node:module, touching the same parser AST flags, bindings, error codes, and test file

🤖 Generated with Claude Code

robobun added 5 commits August 3, 2026 20:50
…aude/node-strip-types

# Conflicts:
#	src/js_parser/lexer.rs
#	src/js_parser/parser.rs
#	src/jsc/NodeCompileCache.rs
#	src/jsc/bindings/ErrorCode.ts
#	src/runtime/cli/Arguments.rs
…-damage in Rust

- src/jsc/bindings/BunHeapProfiler.h: restore header required by
  $newCppFunction("BunHeapProfiler.cpp", ...) codegen (GeneratedJS2Native.h
  #include).
- src/jsc/web_worker.rs: deref parent VM directly; parent_ref local was
  removed.
- src/runtime/node/node_module_binding.rs: add use_define_for_class_fields
  to ParseOptions initializer.
- src/runtime/node/path.rs: make resolve_{posix,windows}_t pub(crate) so
  permission.rs can call them.
- src/clap/lib.rs: make Diagnostic fields pub so bun_runtime can read them.
- src/runtime/cli/run_command.rs: exec_check back to pub(crate)
  (unreachable_pub).
- src/runtime/permission.rs: restore bun_threading::RwLock and
  bun_core::env_var::NODE_OPTIONS (merge damage reverted them to disallowed
  std::sync::RwLock / std::env::var).
- src/runtime/jsc_hooks.rs, src/runtime/timer/Timer.rs: keep // SAFETY:
  adjacent to its unsafe block.
- src/jsc/BunHeapProfiler.rs: then_some over then(|| ...).
…aude/node-strip-types

# Conflicts:
#	src/jsc/bindings/BunHeapProfiler.h
#	src/jsc/web_worker.rs
#	src/runtime/cli/run_command.rs
@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Cross-reference: this resolves #25058 (stripTypeScriptTypes missing from node:module), which is still open, so a Fixes #25058 line in the description would close it when this lands. #32206, the earlier transpiler-based implementation of the same API, has been closed in favor of this PR, since strip mode in Node v26 needs the position-preserving output that re-printing from the AST cannot give.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants