fix(core): support local plugins using NodeNext .js import specifiers#35834
Merged
Conversation
Node's native type stripping loads .ts sources but doesn't rewrite NodeNext explicit `.js` specifiers to their `.ts` sources, so loading a local plugin whose source uses `import './nodes.js'` (where only `nodes.ts` exists) failed. Add dependency-free `.js`->`.ts` resolution on the native-strip path: a `Module._resolveFilename` fallback for CJS, and a self-contained inline ESM resolve hook (no ts-node/swc-node) for ESM, which preserves native stripping and true ESM semantics. The ESM hook is skipped when a transpiler is preloaded via `--require`/`--import` to avoid a loader-hook-worker crash.
✅ Deploy Preview for nx-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for nx-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Contributor
|
View your CI Pipeline Execution ↗ for commit 0f27739
☁️ Nx Cloud last updated this comment at |
meeroslav
approved these changes
Jun 2, 2026
jaysoo
approved these changes
Jun 3, 2026
vrxj81
pushed a commit
to vrxj81/nx
that referenced
this pull request
Jun 7, 2026
…nrwl#35834) ## Current Behavior Loading a local Nx plugin from its TypeScript source (via the `development`/source export condition, including secondary entry points) fails when the source uses NodeNext-style explicit `.js` relative imports — `import './nodes.js'` where the file on disk is `nodes.ts`. Any `nx` command that builds the project graph errors with `Cannot find module './nodes.js'`. Node's native TypeScript stripping loads the `.ts` source but does not rewrite the explicit `.js` specifier to its `.ts` sibling, and nothing in the plugin-loading path did either. ## Expected Behavior Local plugins whose TypeScript sources use NodeNext `.js` import specifiers load correctly from source, for both `type: module` (ESM) and `type: commonjs` packages. ## Implementation Details Add dependency-free `.js` → `.ts` resolution on the native-strip plugin-loading path, mirroring how `tsx` / `ts-node`'s `experimentalResolver` patch module resolution: - **CJS:** a `Module._resolveFilename` fallback that rewrites `.js`/`.mjs`/`.cjs` → `.ts`/`.tsx`/`.mts`/`.cts` when the requesting module is itself TypeScript and the `.js` file doesn't exist. - **ESM:** a self-contained inline `module.register` resolve hook (no `ts-node`/`swc-node` dependency) that does the same on the dynamic-import path, leaving Node's native stripping to load the resolved `.ts` — preserving true ESM semantics. Both are best-effort, fire only on a not-found error, and never alter resolutions that already succeed. `type: commonjs` sources still rely on the existing swc/ts-node fallback to transpile their `import` syntax (native strip can't run ESM syntax in a CJS module), after which the CJS resolver patch handles the emitted `require('./x.js')`. > [!NOTE] > The ESM resolve hook is skipped when a transpiler is preloaded via `--require`/`--import` (e.g. `--require ts-node/register`), which only happens when Nx itself runs from `.ts` source — registering it there would crash the `module.register` loader-hook worker. Published Nx (compiled `.js` workers, no preload) is unaffected.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Current Behavior
Loading a local Nx plugin from its TypeScript source (via the
development/source export condition, including secondary entry points) fails when the source uses NodeNext-style explicit.jsrelative imports —import './nodes.js'where the file on disk isnodes.ts. Anynxcommand that builds the project graph errors withCannot find module './nodes.js'.Node's native TypeScript stripping loads the
.tssource but does not rewrite the explicit.jsspecifier to its.tssibling, and nothing in the plugin-loading path did either.Expected Behavior
Local plugins whose TypeScript sources use NodeNext
.jsimport specifiers load correctly from source, for bothtype: module(ESM) andtype: commonjspackages.Implementation Details
Add dependency-free
.js→.tsresolution on the native-strip plugin-loading path, mirroring howtsx/ts-node'sexperimentalResolverpatch module resolution:Module._resolveFilenamefallback that rewrites.js/.mjs/.cjs→.ts/.tsx/.mts/.ctswhen the requesting module is itself TypeScript and the.jsfile doesn't exist.module.registerresolve hook (nots-node/swc-nodedependency) that does the same on the dynamic-import path, leaving Node's native stripping to load the resolved.ts— preserving true ESM semantics.Both are best-effort, fire only on a not-found error, and never alter resolutions that already succeed.
type: commonjssources still rely on the existing swc/ts-node fallback to transpile theirimportsyntax (native strip can't run ESM syntax in a CJS module), after which the CJS resolver patch handles the emittedrequire('./x.js').Note
The ESM resolve hook is skipped when a transpiler is preloaded via
--require/--import(e.g.--require ts-node/register), which only happens when Nx itself runs from.tssource — registering it there would crash themodule.registerloader-hook worker. Published Nx (compiled.jsworkers, no preload) is unaffected.