Skip to content

[node-resolve]: the external module render path is not expected #1873

Open

Description

@underfin
  • Rollup Plugin Name:
  • Rollup Plugin Version:
  • Rollup Version:
  • Operating System (or Browser):
  • Node Version:
  • Link to reproduction (⚠️ read below):

Expected Behavior

Expected render external module path with absolute path.

Actual Behavior

Get render external module path with original module request path.

Additional Information

reproduction
https://stackblitz.com/edit/github-ttay3lbj?file=package.json,rolldown.config.mjs,entry.js,rollup.config.js,rollup-without-node-resolve.config.js

Run npm run rollup::build:without-node-resolve, it using node js resolve instead of node-resolve plugin, here output

import { rollup } from 'file:///home/projects/vbwimvyraa.github/node_modules/.pnpm/rollup@4.41.1/node_modules/rollup/dist/es/rollup.js';

Run npm run build:rollup, it using node resolver plugin, here output

import { rollup } from 'rollup';

It caused by https://github.com/rollup/plugins/blob/master/packages/node-resolve/src/index.js#L329-L331. The rollup core is using 'file:///home/projects/vbwimvyraa.github/node_modules/.pnpm/rollup@4.41.1/node_modules/rollup/dist/es/rollup.js' to check external and find it is a external absolute module. But the node-resolve plugin overwrite it.

Activity

  1. charliecreates commented on Oct 20, 2025

    @charliecreates
    Contributor
    Expand this to see my work.
    • Reproduced the StackBlitz scenario for #1873: rollup.config.js (with nodeResolve()) emits import { rollup } from 'rollup' while rollup-without-node-resolve.config.js emits import { rollup } from 'file:///.../node_modules/.../rollup.js'.
    • Compared rollup.config.js and rollup-without-node-resolve.config.js; both treat 'rollup' as external (per output), isolating the functional difference to the presence of nodeResolve().
    • Traced the discrepancy to the resolveId contract: core resolution returns an absolute id with { external: true }, while @rollup/plugin-node-resolve preserves the bare specifier by returning { id: 'rollup', external: true }, directly changing the emitted import; see Rollup’s resolveId docs (link).
    • Validated the behavior by stubbing resolveId to return { id: 'rollup', external: true } vs { id: '/abs/.../rollup.js', external: true }, confirming output[0].code switches between import ... from 'rollup' and an absolute file:///... path.
    • Noted alignment with Rollup’s renderExternal semantics—emitted specifiers mirror the external id; customizing the path is supported via output.renderExternal (docs).
  2. charliecreates commented on Oct 20, 2025

    @charliecreates
    Contributor

    Thanks for the clear repro and pointers.

    What you’re seeing comes from node-resolve’s resolveId flow. When another plugin (or your external option) marks a specifier as external, node-resolve intentionally returns false so Rollup treats it as external without changing the specifier. That preserves the original text (e.g. 'rollup') instead of substituting an absolute path. See the “Handle plugins that manually make the result external” branch in packages/node-resolve/src/index.js.

    Without node-resolve in the pipeline, Rollup core may use the fully resolved absolute file URL for that external in your setup, which explains the difference between the two runs.

    If you want externals rendered as absolute paths, the recommended way is to control it via output.paths:

    // rollup.config.js
    import { pathToFileURL } from 'node:url';
    import { createRequire } from 'node:module';
    const require = createRequire(import.meta.url);
    
    export default {
      // ...
      external: ['rollup'],
      output: {
        format: 'esm',
        paths: (id) => {
          if (id === 'rollup') {
            // Map to the absolute file URL you want in output
            return pathToFileURL(require.resolve('rollup/dist/es/rollup.js')).href;
          }
          return id;
        }
      },
      plugins: [
        nodeResolve()
      ]
    };

    That approach keeps node-resolve’s behavior intact (don’t rewrite external specifiers) while giving you full control over how externals print in the generated code.

    A couple of questions to make sure we’re covering your exact case:

    1. Which versions are you using? (Rollup, @rollup/plugin-node-resolve, Node)
    2. How are you marking modules external in the config (array, regex, function)? A short snippet would help.

    If there’s a strong use case for node-resolve to preserve the resolved absolute id when something is external, we could discuss adding an opt-in (e.g., a flag to return the resolved id with external: true instead of false). That would be a behavior change, so we’d gate it behind an option if we go that route.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions