Skip to content

fix(webpack,rspack): build the virtual module prefix on the real path of the context - #641

Open
AronSoto wants to merge 1 commit into
unjs:mainfrom
AronSoto:fix/symlinked-context
Open

AronSoto wants to merge 1 commit into
unjs:mainfrom
AronSoto:fix/symlinked-context

Conversation

@AronSoto

@AronSoto AronSoto commented Nov 3, 2026 •

Copy link
Copy Markdown

resolves #640

Description

When the context of a webpack or rspack build goes through a symlink, every virtual module fails to build with ENOENT. The reproduction is in #640.

Cause. The virtual module prefix is built on compiler.options.context as given. The virtual module is written under that path, but both resolvers follow symlinks (resolve.symlinks is on by default), so the module is then read from the real path, where nothing was written. The loaders and the importer decoding also compare the resolved (real) path against the prefix.

Fix. Build the prefix on the real path of the context in both adapters, through a small realContext helper in src/utils/webpack-like.ts.

  • With no link left in the prefix, neither resolver rewrites a virtual path built on it.
  • It uses realpathSync and not realpathSync.native: it resolves only links and junctions, and leaves Windows short names (RUNNER~1) and letter case as the context gave them. This matters on Windows: when rspack resolves through a link it also expands short names, while webpack keeps them, so the safe prefix is one with no link left to resolve.
  • A context that does not exist keeps the path as given, as before.
  • With resolve.symlinks: false nothing changes in practice: the virtual path is built on a path with no links in it, so it resolves to itself.

Tests

  • test/unit-tests/symlinked-context: webpack and rspack builds whose context is a symlink to the project (a junction on Windows), with one virtual module importing another. The test checks four things:

    • the build succeeds;
    • load receives both ids;
    • the virtual importer reaches resolveId decoded;
    • the output contains what load returned.

    On main it fails with the ENOENT errors from webpack/rspack: virtual modules fail with ENOENT when the build context goes through a symlink #640. With the fix it passes. Reverting only one adapter's change fails only that bundler's case.

  • A direct test of realContext in test/unit-tests/utils/webpack-like.test.ts.

  • pnpm test, pnpm lint and pnpm typecheck pass locally (Linux, Node 22).

  • The repo's CI, run on my fork:

    • With the fix, every job is green: Ubuntu and Windows on Node 22, 24 and 26, plus Bun on Ubuntu, Windows and macOS. (run)
    • With only the new test and no fix, the two new cases fail on every OS with those ENOENT errors (a junction on Windows, the real /var link on macOS), and the rest of the suite passes. (run)

Notes

  • This is not what fix(webpack,rspack): keep the module type of real files in the load rule #639 fixes, but both touch the same import line in src/webpack/index.ts and src/rspack/index.ts, so whichever lands second needs a one-line rebase. I merged the two locally, kept both imports, and the tests of both pass.
  • Not in this PR: on rspack, a virtual importer reaches resolveId with a leading separator (/\0virtual-a), with or without a symlink, because the importer is sliced at a prefix that has no trailing separator. The new test tolerates that. I can send a separate fix if you want one.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed virtual module resolution when projects are accessed through directory symlinks.
    • Webpack and Rspack builds can now correctly load and bundle virtual modules in symlinked project directories. This also improves consistency when resolving project paths across different directory naming conventions.

… of the context

Webpack and Rspack resolve symlinks in module paths (`resolve.symlinks` is on by
default). The virtual module prefix was built on `compiler.options.context` as
given, so when the context goes through a link (any project under macOS's
`/var`, or a linked project folder) the virtual module was written under the
link, the resolver followed the link to the real path, and the build failed with
ENOENT on `<real>/_virtual_...` (webpack) or `<real>/node_modules/.virtual/...`
(rspack).

The prefix is now built on the real path of the context, which is also what the
loaders and the importer decoding compare resolved paths against. A context that
does not exist keeps the path as given.
@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

The adapters now resolve the build context before constructing virtual module prefixes. Tests cover the path-resolution helper and builds through a symlinked context with webpack and rspack.

Changes: Symlinked build context normalization

Layer / File(s) Summary
Resolve build contexts for virtual prefixes
src/utils/webpack-like.ts, src/webpack/index.ts, src/rspack/index.ts
realContext returns the resolved real path when possible and the original context if resolution fails. Both adapters use it when constructing virtual module prefixes.
Test path resolution and symlinked builds
test/unit-tests/utils/webpack-like.test.ts, test/unit-tests/symlinked-context/symlinked-context.test.ts
Unit tests cover real paths, symlinks, and missing paths. Integration tests check virtual module resolution and bundle output for webpack and rspack builds through a symlinked context.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: antfu, nullvoxpopuli, sxzz

Merge Risk: Minimal · up to 4b42d

No merge-blocking risk is established from the supplied evidence.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: webpack and rspack now build the virtual module prefix from the real context path.
Linked Issues check Passed Directly linked issue #640 asks that webpack and rspack virtual modules build when the context goes through a symlink or junction. The reviewed head builds the virtual module prefix on the real contex…
Out of Scope Changes check Passed All changed code connects to #640. The diff changes one prefix line in src/webpack/index.ts and one in src/rspack/index.ts, and adds the realContext helper with its documentation in src/utils/webpack-…
Full details: Linked Issues check

Explanation

Directly linked issue #640 asks that webpack and rspack virtual modules build when the context goes through a symlink or junction. The reviewed head builds the virtual module prefix on the real context path in both adapters. In src/webpack/index.ts, VIRTUAL_MODULE_PREFIX = resolve(realContext(compiler.options.context ?? process.cwd()), 'virtual'). In src/rspack/index.ts, applyRspackPlugins uses resolve(realContext(...), 'node_modules/.virtual', ...). The helper realContext in src/utils/webpack-like.ts calls realpathSync and returns the original path on error, so a nonexistent context keeps its given path, as the issue expects. The PR adds test/unit-tests/symlinked-context, which covers webpack and rspack builds through a symlinked context and checks that load receives both ids and that the bundle contains the loaded output. The PR also adds a direct unit test for realContext. I did not run the tests. The rspack importer leading-separator behavior mentioned in the PR description is not part of #640 and is not required here. No unmet coding requirement is found.

Full details: Out of Scope Changes check

Explanation

All changed code connects to #640. The diff changes one prefix line in src/webpack/index.ts and one in src/rspack/index.ts, and adds the realContext helper with its documentation in src/utils/webpack-like.ts. Both adapter edits add the realContext import to the existing import line. The helper and its tests are the direct fix and its verification. The new test files, test/unit-tests/symlinked-context and test/unit-tests/utils/webpack-like.test.ts, support the fix and do not add unrelated behavior. The PR description states that the import-line overlap with #639 needs a rebase. This is a merge concern, not an unrelated change. No unrelated change is demonstrated.

Full details: Docstring Coverage

Explanation

Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 2 functions across 5 files.

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

Finishing Touches
Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/rspack/index.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

src/utils/webpack-like.ts

ESLint skipped: the matched ESLint configuration already failed (missing-dependency).

src/webpack/index.ts

ESLint skipped: the matched ESLint configuration already failed (missing-dependency).

  • 2 others

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6ee1f0aa-5110-4513-b40a-8c03389f800c
Commits

Reviewing files that changed from the base of the PR and between 5daf6e6 and 4b42d2c.

Files selected for processing: 5
  • src/rspack/index.ts
  • src/utils/webpack-like.ts
  • src/webpack/index.ts
  • test/unit-tests/symlinked-context/symlinked-context.test.ts
  • test/unit-tests/utils/webpack-like.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

This branch has not been deployed

No deployments
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.

webpack/rspack: virtual modules fail with ENOENT when the build context goes through a symlink

1 participant