Skip to content

fix(core): support local plugins using NodeNext .js import specifiers#35834

Merged
leosvelperez merged 1 commit into
masterfrom
fix-plugin-loader-nodenext
Jun 3, 2026
Merged

fix(core): support local plugins using NodeNext .js import specifiers#35834
leosvelperez merged 1 commit into
masterfrom
fix-plugin-loader-nodenext

Conversation

@leosvelperez

@leosvelperez leosvelperez commented May 29, 2026

Copy link
Copy Markdown
Member

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.

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.
@leosvelperez leosvelperez self-assigned this May 29, 2026
@netlify

netlify Bot commented May 29, 2026

Copy link
Copy Markdown

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit 0f27739
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/6a1995eaacdb6200075a3f3e
😎 Deploy Preview https://deploy-preview-35834--nx-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented May 29, 2026

Copy link
Copy Markdown

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit 0f27739
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/6a1995ea3af8b10008dabd2c
😎 Deploy Preview https://deploy-preview-35834--nx-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@nx-cloud

nx-cloud Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 0f27739

Command Status Duration Result
nx affected --targets=lint,test,build,e2e,e2e-c... ✅ Succeeded 41m 21s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 3s View ↗
nx-cloud record -- pnpm nx-cloud conformance:check ✅ Succeeded 16s View ↗
nx build workspace-plugin ✅ Succeeded <1s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded 20s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 6s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-29 14:19:55 UTC

@leosvelperez leosvelperez marked this pull request as ready for review May 29, 2026 14:26
@leosvelperez leosvelperez requested a review from a team as a code owner May 29, 2026 14:26
@leosvelperez leosvelperez requested a review from JamesHenry May 29, 2026 14:26
@leosvelperez leosvelperez merged commit 7438746 into master Jun 3, 2026
26 checks passed
@leosvelperez leosvelperez deleted the fix-plugin-loader-nodenext branch June 3, 2026 13:22
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.
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.

3 participants