Skip to content

fix: recreate webpack virtual modules when resolveId is skipped - #617

Open
rosslh wants to merge 4 commits into
unjs:mainfrom
ownright:fix/webpack-virtual-module-cache
Open

rosslh wants to merge 4 commits into
unjs:mainfrom
ownright:fix/webpack-virtual-module-cache

Conversation

@rosslh

@rosslh rosslh commented Sep 22, 2026 •

Copy link
Copy Markdown

Fixes: #618

On webpack, unplugin can serve modules that are not real files, for example icon imports in unplugin-icons. It creates an empty placeholder in memory and points the module at a made-up _virtual_ path. The plugin's load hook supplies the real source.

Current behaviour

The placeholder is only created in resolveId. Webpack stores the _virtual_ path in its on-disk cache. The placeholder never reaches disk, so a new process starts without it.

When that cache is reused (a next dev restart, or a CI or Vercel build that reuses .next/cache), webpack already knows the path and skips resolveId. It then opens the path and fails with ENOENT.

This is the unplugin side of unplugin/unplugin-icons#206. That issue goes away once a release of this package is published and unplugin-icons picks it up.

Related: unjs/unplugin#32

Expected behaviour

A cached virtual module still builds in a new process. The placeholder is there before webpack opens the path, even when resolveId never runs.

This change

compilation.hooks.buildModule creates the same empty placeholder if this process does not already have it. resolveId uses the same helper, so a missing placeholder is also recreated there.

We skip the write if the path already exists on disk or in memory. The _virtual_ prefix has no trailing separator, so a real file such as _virtual_helper.js would otherwise be overwritten.

The virtual filesystem is still installed only for plugins that define resolveId. Rspack and the other bundler adapters are unchanged.

Tests

  • pnpm exec vitest run test/unit-tests/webpack/virtual-cache.test.ts — a normal virtual load; a build where a fake resolver returns the _virtual_ path so resolveId is skipped; the same build after the in-memory map is cleared; and an on-disk _virtual_helper.js that must keep its contents
  • pnpm exec vitest run test/unit-tests/webpack test/unit-tests/virtual-id
  • pnpm exec eslint src/webpack/index.ts test/unit-tests/webpack/virtual-cache.test.ts

The skipped-resolveId case fails on current main with ENOENT on _virtual_~demo and passes with this change. It fakes the skip with a resolver; it does not drive webpack's filesystem cache.

For the cache path itself, we started and stopped a Next.js webpack app with the filesystem cache enabled. The ENOENT showed up, and it did not come back with this change applied.

Summary by CodeRabbit

Summary by CodeRabbit

  • Bug Fixes

    • Improved webpack build reliability by restoring missing virtual modules from cached resources.
    • Preserved existing real files and in-memory virtual files without overwriting them.
    • Restored virtual modules even when standard module resolution is bypassed.
  • Tests

    • Added coverage for initial virtual-module loading, cache-based restoration, in-memory cache clearing, and conflicts with real files.
    • Verified that cached builds produce the expected virtual or real-file output.

Webpack can restore a module whose resource is already the _virtual_ path
from cache. That file exists only in memory, so a later process ENOENTs
unless the empty virtual file is written again before the read.
buildModule must skip paths that exist on disk, because the prefix has
no trailing separator and would otherwise shadow files such as
_virtual_helper.js with an empty virtual module.
Skip writeModule only when _virtualFiles already has the path. Treating
__vfsModules as proof the file exists was the old bug.
@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 26e70af6-6273-4396-a16f-cd16c57f3a08

📥 Commits

Reviewing files that changed from the base of the PR and between 3263e23 and f7b02bc.

📒 Files selected for processing (2)
  • src/webpack/index.ts
  • test/unit-tests/webpack/virtual-cache.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/unit-tests/webpack/virtual-cache.test.ts
  • src/webpack/index.ts

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


📝 Walkthrough

Walkthrough

Webpack now restores missing virtual-module placeholders during webpack module building and resolution. Tests cover cached resolution, cleared in-memory files, repeated compiler runs, and collisions with real files.

Changes

Webpack virtual-module restoration

Layer / File(s) Summary
Virtual-module restoration
src/webpack/index.ts
Webpack unwraps nested input filesystems and recreates missing virtual modules when they are not present on disk or in webpack’s in-memory files. The shared helper runs during resolution and buildModule.
Virtual-module cache validation
test/unit-tests/webpack/virtual-cache.test.ts
Tests cover initial loading, cached resolution without resolveId, restoration after clearing webpack’s in-memory files, compiler cleanup, and preservation of a real _virtual_helper.js file.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to f7b02

No actionable merge-blocking risk is established from the available evidence.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 primary change: recreating webpack virtual modules when resolveId is skipped.
Linked Issues check ✅ Passed The PR meets the coding requirements in [#618]. src/webpack/index.ts adds ensureVirtualModule and calls it from compilation.hooks.buildModule, so webpack can recreate a missing _virtual_ place…
Out of Scope Changes check ✅ Passed The changes stay within [#618]. The source changes implement cached virtual-module restoration and protect real files with the same prefix. The test changes verify the reported failure and the related…
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 2 files.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

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/webpack/index.ts

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

test/unit-tests/webpack/virtual-cache.test.ts

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


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

Comment @coderabbitai help to get the list of available commands.

@rosslh rosslh changed the title Fix/webpack virtual module cache fix: recreate webpack virtual modules when resolveId is skipped Sep 22, 2026
CodeRabbit's pre-merge docstring coverage check requires 80% on
functions touched by the diff.
@rosslh

rosslh commented Oct 1, 2026

Copy link
Copy Markdown
Author

Hi @sxzz, I'd love to get your review on this if you have a moment. It fixes the ENOENT on cached webpack builds that people are hitting downstream in unplugin/unplugin-icons#206. I'm happy to adjust the approach if you'd prefer it done differently. Thanks for maintaining unplugin!

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: ENOENT on cached virtual modules when resolveId is skipped

1 participant