Skip to content

feat(core): add runtimeTsTransform for runtime-loaded TypeScript - #1579

Draft
fi3ework wants to merge 1 commit into
mainfrom
feat/runtime-ts-transform
Draft

fi3ework wants to merge 1 commit into
mainfrom
feat/runtime-ts-transform

Conversation

@fi3ework

Copy link
Copy Markdown
Member

Summary

Closes #1577.

Loading a .ts file at runtime — via createRequire or a dynamic import() with a runtime path, outside the bundle graph — fails when the file's module style mismatches its package type scope. A CommonJS-style .ts (module.exports = ...) in a "type": "module" scope loads as ESM and throws module is not defined; the mirror case (an ESM-style .ts in a "type": "commonjs" scope) fails too. Node's type stripping erases types but never converts module systems, so today the docs tell users to manually register ts-node/@swc-node.

This PR builds the fix in. A new per-project config option runtimeTsTransform (default true) installs a Node module.registerHooks sync load hook in the test and globalSetup workers that intervenes only on those two mismatch cases, transforming with the SWC that already ships inside Rspack (rspack.experiments.swc) — no new dependency, no user configuration.

  • Minimal intervention: every other file keeps Node-native semantics; a third-party .ts loader (ts-node/@swc-node/tsx) takes precedence and the hook passes through. Only file: URLs, .ts extension, node_modules excluded.
  • Lazy cost: the ~39 MB native binding is required only on an actual mismatch hit, never at worker bootstrap — bootstrap cost is unchanged when the feature never fires.
  • Node-version gated: self-gates on the sync-hook reentrancy fix (module: handle null source from async loader hooks in sync hooks nodejs/node#59929: >= 22.22.3 / 24.11.1 / 25.1.0 / 26.0.0) and is silently inactive below, where the documented manual workaround still applies. Set runtimeTsTransform: false to opt out entirely.
  • Scope: node executor only; the browser wire strips the field. Docs added (bilingual config page + rewritten troubleshooting section). Limitations: type erasure + module lowering only (no tsconfig paths, no decorators metadata, no .tsx/.mts/.cts); hook-loaded files stay invisible to watch-mode re-runs and rstest.mock, same as today.

Related Links

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).

Loading a `.ts` file at runtime (via `createRequire` or a dynamic
`import()` with a runtime path) outside the bundle graph fails when its
module style mismatches the package `type` scope: a CommonJS-style `.ts`
in a `type: module` scope, or the mirror case in a `type: commonjs`
scope. Node's type stripping erases types but never converts module
systems, so these load as the wrong format and throw.

Add a per-project `runtimeTsTransform` option (default `true`) that
installs a Node `module.registerHooks` sync load hook in the test and
globalSetup workers. The hook intervenes only on the two mismatch cases,
transforming with the SWC that already ships inside Rspack
(`rspack.experiments.swc`), so there is no new dependency and no user
configuration. The native binding is loaded lazily, only on an actual
mismatch hit. All other files keep Node-native semantics, and a
third-party `.ts` loader (ts-node/@swc-node/tsx) takes precedence.

The feature self-gates on Node versions with the sync-hook reentrancy
fix (nodejs/node#59929: >= 22.22.3 / 24.11.1 / 25.1.0 / 26.0.0) and is
silently inactive below them, where the documented manual workaround
still applies. Node executor only; the browser wire strips it.
@fi3ework
fi3ework marked this pull request as draft July 16, 2026 03:36

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15236a8ed9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


export const looksLikeEsm = (source: string): boolean => {
const stripped = stripComments(source);
return ESM_SYNTAX_RE.test(stripped) && !CJS_MARKER_RE.test(stripped);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow ESM transforms when modules call require()

In a type: commonjs scope, an ESM-style .ts file can still legitimately call a local require created with createRequire. Because looksLikeEsm rejects any source containing require(, the hook leaves that file as commonjs-typescript instead of transforming it, so Node parses it as CommonJS and fails on the import/export declarations. This breaks the documented mirror case for runtime-loaded ESM TypeScript modules that need to load a CJS dependency.

Useful? React with 👍 / 👎.

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.

[Feature]: built-in TS transform for runtime-loaded modules outside the bundle graph

1 participant