Skip to content

Let webpack resolve nested node_modules like node does - #27421

Draft
dennisoelkers wants to merge 1 commit into
masterfrom
fix/webpack-resolve-nested-node-modules
Draft

dennisoelkers wants to merge 1 commit into
masterfrom
fix/webpack-resolve-nested-node-modules

Conversation

@dennisoelkers

Copy link
Copy Markdown
Member

Description

Motivation and Context

This PR is changing resolve.modules configuration for both core and
plugin webpack configs, to avoid silently collapsing package versions
from different node_modules directories.

/nocl Internal refactoring.

How Has This Been Tested?

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Refactoring (non-breaking change)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have requested a documentation update.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.

`resolve.modules` treats its entries differently depending on their shape: a
bare name like `node_modules` makes webpack walk up the directory tree from
the importing module, exactly like node, while an absolute path is searched
as one fixed directory and nothing else. `webpack/core.js` and
`webpack.preflight.ts` listed only absolute paths, so nested `node_modules`
were invisible to the bundle and every request resolved to whatever yarn
happened to hoist to the top level — regardless of the range the requiring
package declared.

That silently collapses versions the package manager deliberately kept
apart. The failure mode is not a missing module but a wrong one, and when
two packages depend on each other it becomes a require cycle: the second
module gets the first's half-initialised `module.exports` and calling it
throws at import time. `get-intrinsic` 1.3.0 with a hoisted `gopd` 1.0.1 is
exactly this shape, and it takes down the whole bundle on load. Note that
`webpack.vendor.ts` sets no `resolve` block at all and so has always had the
correct behaviour — this only aligns the other two configs with it.

Adding the bare `node_modules` entry restores node's semantics. The absolute
entries stay as fallbacks: plugins build out of tree, where `rootPath` is the
plugin directory, and walking up from a plugin source file never reaches
`<web-interface>/node_modules`.

## Plugin builds are deliberately left alone

`PluginWebpackConfig` prepends `<web-interface>/node_modules` to this list,
so for plugin builds the merged order is:

    [<wi>/src, <wi>/node_modules, <plugin>/src/web, node_modules,
     <plugin>/node_modules, <plugin>/public]

The flat `<wi>/node_modules` staying ahead of the walk-up entry is
load-bearing, not an oversight. graylog-plugin-enterprise ships its own
`react` 18.3.1, `react-dom` 18.2.0, `styled-components` 6.1.1 and `moment`
2.29.4; those must come from the web interface so plugins share the single
instance in the vendor DLL. Moving the walk-up entry ahead of it would give
plugins their own React and break hooks and theming. Plugin bundles
therefore keep the old flattening behaviour. Fixing that properly means
pinning the shared singletons with `resolve.alias` instead of relying on
list order, which is a bigger change and left for later.

## Verification

Resolution was checked directly through webpack's own `enhanced-resolve`
with the real merged module lists:

| From | Request | Before | After |
| --- | --- | --- | --- |
| `get-intrinsic` | `hasown` | `<wi>/node_modules/hasown` 2.0.2 | `get-intrinsic/node_modules/hasown` 2.0.0 |
| `@floating-ui/dom` | `@floating-ui/utils` | root 0.2.10 | nested 0.2.8 |
| `i18next` | `@babel/runtime/...` | root | nested |
| plugin source | `react` | `<wi>` 18.3.1 | `<wi>` 18.3.1 |
| plugin source | `styled-components` | `<wi>` 6.1.1 | `<wi>` 6.1.1 |

Bundle contents confirm it end to end: the app build goes from 0 to 61
modules resolved out of a nested `node_modules`, at a cost of about 83 KB of
minified JS (+0.35%). Duplication only appears where yarn could not hoist,
which is precisely where the versions genuinely conflict.

`yarn build`, `yarn build:preflight` and `yarn check-production-build` all
pass, `yarn tsgo` is clean, and `yarn test` is 777 suites / 4169 passed / 0
failures. `yarn lint:path` reports the same 28 pre-existing errors on these
two files before and after, none on the changed lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant