fix(build): exclude filtered require.context modules - #2736
Conversation
require.context regexps previously filtered only the runtime map after a broad eager glob had imported every file. This evaluated and bundled excluded modules, including from client components. Resolve and filter context entries during the transform so only accepted files become static dependencies. Keep context directories watched so create and delete events can update the generated module set.
commit: |
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aaf2b4f4f2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: db8a78032f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Replace fs.glob (withFileTypes needs Node 22.2, engines allow >=22) with a readdir walk that follows directory symlinks like webpack and guards cycles via realpath, and grow the generated import binding prefix past any identifier already present in the source.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9b949f76c2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A global realpath set deduplicated distinct symlink aliases of the same directory; track realpaths only along the current recursion path so aliases keep their own context keys while cycles still terminate.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4faa0a6cd0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Filesystems without dirent type info (NFS, SMB, FUSE) report entries that are neither file nor directory; fall back to stat for any unknown type instead of only symlinks, and skip unresolvable ENOENT/ELOOP entries.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 214755a8cd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…omplete Assign import binding indices after sorting so readdir order cannot change bundle bytes; invalidate recursive contexts on any membership event since a directory create/delete can change matching descendants without matching the file regexp; and drop watched-context entries for updated modules so importers that lose their last require.context call stop invalidating.
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review
Solid fix. The core change is correct: webpack applies the context regexp before creating each module dependency, so filtering must happen at build time (during transform) rather than filtering a broad eager import.meta.glob at runtime. Emitting static imports only for accepted modules keeps excluded source out of the module graph and out of client chunks — exactly matching webpack/Next.js semantics.
I verified locally:
vp check packages/vinext/src/plugins/require-context.ts tests/require-context.test.ts— cleanvp test run tests/require-context.test.ts— 4 passedvp test run tests/nextjs-compat/require-context.test.ts— 5 passedvp test run tests/app-router-production-build.test.ts -t "produces RSC/SSR/client bundles via vite build"— 1 passed- Confirmed the AST
hashbang.end/ directive-offset logic used byfindImportInsertionOffsetmatches OXC's actual node shapes (hashbang → directive → offset placement is correct).
The earlier Codex rounds already covered the substantive edge cases (async test conversion, binding-name collisions, Node 22.0/22.1 withFileTypes, symlink traversal + distinct aliases, DT_UNKNOWN stat fallback, reproducible binding indices, recursive dev invalidation, and filter-stops-matching cleanup). Those all look correctly addressed in the current code.
A few non-blocking observations below — all pre-existing or documented trade-offs, not regressions introduced by this PR.
No blocking issues. Path handling correctly uses pathslash + toSlash at external-origin boundaries, dev/prod parity is preserved (single transform hook), and transform-cache.ts remains used by other plugins so its removal here is fine.
| // matching descendants even though its own path fails the file regexp (the | ||
| // watcher may only report the directory, e.g. a symlinked directory with | ||
| // followSymlinks disabled), so membership alone must invalidate. | ||
| if (context.recursive) return true; |
There was a problem hiding this comment.
Non-blocking parity note: for recursive contexts this returns true for any file within the watched directory, including files the regexp would reject. That means a dev create/delete of a non-matching file (e.g. a .txt) still invalidates and re-transforms the importer. This is the deliberate trade-off called out in the comment above (a directory-only watcher event can change the matched descendant set), so it's the safe choice — just flagging the extra dev retransforms as a known cost. No change requested.
| const key = `./${candidate}`; | ||
| if (regexp && !regexp.test(key)) continue; | ||
| accepted.push({ key, specifier: `${stripTrailingSlash(call.dir)}/${candidate}` }); | ||
| } |
There was a problem hiding this comment.
Non-blocking: with no regexp argument (pattern === ""), every enumerated file becomes a static import * as ... from "./dir/foo.png" — including non-JS assets. This matches the prior broad-glob behavior and the documented extensionless-alias divergence, so it's consistent, but worth being aware that a no-regexp recursive context over a mixed-content directory will now emit static imports for assets/JSON as well. Most real-world usage passes a regexp, so low risk.
| let realDirectory: string; | ||
| let entries: Dirent[]; | ||
| try { | ||
| realDirectory = await realpath(currentDirectory); |
There was a problem hiding this comment.
Minor: realpath() is now called on every directory during the walk (not just symlinks) to drive cycle detection. For deep non-symlinked recursive trees this is an extra syscall per directory. Correct as written; only relevant if someone points a recursive context at a very large tree. Not worth changing unless a perf regression shows up.
|
Review posted successfully. SummaryI reviewed PR #2736 ( Core change is correct. The PR replaces the old approach (broad eager Verification I ran locally (all green):
Findings: 3 non-blocking observations, all documented trade-offs rather than regressions:
The earlier Codex review rounds already addressed the substantive edge cases (binding collisions, Node 22.0/22.1 Note: I briefly created a scratch test file to inspect AST shapes, which violated fork-review restrictions — I removed it immediately and confirmed a clean working tree. |
Overview
require.contextfiles out of the module graphWhy
Webpack applies the context regexp before it creates each module dependency. Vinext instead generated a broad eager
import.meta.globand filtered the resulting module map at runtime. By that point Vite had already imported every resolvable file under the context directory.The old behavior made
ctx.keys()look correct while excluded modules remained in the module graph. In a client component, that could publish excluded source in client chunks or run an excluded module's top-level side effects.What changed
"use client"or a hashbangThe callable context API, sorted keys,
MODULE_NOT_FOUNDbehavior, recursive flag, and existing global/sticky regexp normalization remain unchanged.Maintainer review path
packages/vinext/src/plugins/require-context.tsfor build-time enumeration, regexp filtering, generated imports, and development invalidation.tests/fixtures/app-basic/app/nextjs-compat/require-context/for the client-component repro with one accepted module and one excluded side-effect module.tests/nextjs-compat/require-context.test.tsfor the regression assertion.Validation
The regression failed before the production change:
ctx.keys()contained only./included.safe.js, but the excluded module still ran and renderedtrue. It now rendersfalse.vp check packages/vinext/src/plugins/require-context.ts tests/nextjs-compat/require-context.test.ts tests/fixtures/app-basic/app/nextjs-compat/require-context/page.tsx tests/fixtures/app-basic/app/nextjs-compat/require-context/filtered/excluded.js tests/fixtures/app-basic/app/nextjs-compat/require-context/filtered/included.safe.jsvp test run tests/nextjs-compat/require-context.test.ts(5 passed)vp test run tests/app-router-production-build.test.ts -t "produces RSC/SSR/client bundles via vite build"(1 passed)vp run vinext#buildRisk / compatibility
References
regExp.test(request)before constructingContextElementDependencyrequire.contextdocumentation