Skip to content

fix(build): exclude filtered require.context modules - #2736

Merged
james-elicx merged 8 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/fix-require-context-regex-imports
Jul 31, 2026
Merged

fix(build): exclude filtered require.context modules#2736
james-elicx merged 8 commits into
cloudflare:mainfrom
NathanDrake2406:nathan/fix-require-context-regex-imports

Conversation

@NathanDrake2406

Copy link
Copy Markdown
Contributor

Overview

Goal Keep regexp-excluded require.context files out of the module graph
Core change Filter filesystem entries during the transform, then emit static imports only for matches
Key boundary The regexp constrains dependency creation, not only the runtime context map
Expected impact Excluded modules are neither bundled nor evaluated, including from client components

Why

Webpack applies the context regexp before it creates each module dependency. Vinext instead generated a broad eager import.meta.glob and 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

Scenario Before After
File matches the regexp Imported and exposed through the context Imported and exposed through the context
File does not match the regexp Eagerly imported, then hidden from the runtime map Not imported
Matching file is created or deleted in development Broad glob invalidated the importer The plugin watches context membership and invalidates the importer
Source contains "use client" or a hashbang No generated imports were prepended by this plugin Static imports are inserted after the directive or hashbang

The callable context API, sorted keys, MODULE_NOT_FOUND behavior, recursive flag, and existing global/sticky regexp normalization remain unchanged.

Maintainer review path
  1. packages/vinext/src/plugins/require-context.ts for build-time enumeration, regexp filtering, generated imports, and development invalidation.
  2. tests/fixtures/app-basic/app/nextjs-compat/require-context/ for the client-component repro with one accepted module and one excluded side-effect module.
  3. tests/nextjs-compat/require-context.test.ts for 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 rendered true. It now renders false.

  • 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.js
  • vp 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#build
  • Pre-commit full check, staged unit/integration tests, and knip
Risk / compatibility
  • Public API: unchanged.
  • Build behavior: the transform now reads the context directory because the regexp must be applied before imports are generated.
  • Development: existing matches remain normal static dependencies; context metadata handles create/delete events that change the matched set.
  • Known compatibility limit: the documented no-regexp extensionless alias gap remains unchanged.

References

Reference Why it matters
Webpack 5.98 context dependency creation Applies regExp.test(request) before constructing ContextElementDependency
Webpack require.context documentation Defines the directory, recursion, and regexp arguments
Next.js require-context test Canonical compatibility expectation for regexp-filtered keys
Next.js fixture call Shows the supported static call form
Regression test Demonstrates that excluded modules must not evaluate
Original compatibility issue #1501 Introduced the transform's intended behavior

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.
@pkg-pr-new

pkg-pr-new Bot commented Jul 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2736
npm i https://pkg.pr.new/create-vinext-app@2736
npm i https://pkg.pr.new/@vinext/types@2736
npm i https://pkg.pr.new/vinext@2736

commit: 68f719c

@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 68f719c against base 25dc2f3 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 134.3 KB 134.3 KB ⚫ -0.0%
Client entry size (gzip) vinext 121.9 KB 121.9 KB ⚫ -0.0%
Dev server cold start vinext 2.79 s 2.77 s ⚫ -0.8%
Production build time vinext 2.96 s 2.96 s ⚫ -0.3%
RSC entry closure size (gzip) vinext 111.3 KB 111.3 KB ⚫ +0.0%
Server bundle size (gzip) vinext 189.0 KB 189.0 KB ⚫ +0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@NathanDrake2406

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/vinext/src/plugins/require-context.ts
@NathanDrake2406

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/vinext/src/plugins/require-context.ts Outdated
Comment thread packages/vinext/src/plugins/require-context.ts Outdated
Comment thread packages/vinext/src/plugins/require-context.ts Outdated
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.
@NathanDrake2406

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/vinext/src/plugins/require-context.ts Outdated
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.
@NathanDrake2406

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/vinext/src/plugins/require-context.ts
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.
@NathanDrake2406

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/vinext/src/plugins/require-context.ts Outdated
Comment thread packages/vinext/src/plugins/require-context.ts
Comment thread packages/vinext/src/plugins/require-context.ts
…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.
@NathanDrake2406
NathanDrake2406 marked this pull request as ready for review July 28, 2026 07:47
@james-elicx

Copy link
Copy Markdown
Member

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 — clean
  • vp test run tests/require-context.test.ts — 4 passed
  • vp 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
  • Confirmed the AST hashbang.end / directive-offset logic used by findImportInsertionOffset matches 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}` });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ask-bonk

ask-bonk Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review posted successfully.

Summary

I reviewed PR #2736 (fix(build): exclude filtered require.context modules) and posted a review with a COMMENT verdict — no blocking issues.

Core change is correct. The PR replaces the old approach (broad eager import.meta.glob filtered at runtime) with build-time enumeration + regexp filtering that emits static imports only for accepted modules. This matches webpack's semantics of applying the context regexp before creating each module dependency, which keeps excluded source out of the module graph and client chunks — the actual bug being fixed.

Verification I ran locally (all green):

  • vp check on the changed source + unit test — clean
  • Unit tests (4), nextjs-compat integration tests (5), and the production-build test (1) all pass
  • Confirmed the OXC AST hashbang/directive node shapes match what findImportInsertionOffset relies on

Findings: 3 non-blocking observations, all documented trade-offs rather than regressions:

  1. Recursive contexts invalidate on any dev file change in the watched dir (deliberate, safe)
  2. No-regexp contexts emit static imports for non-JS assets too (consistent with prior behavior + documented divergence)
  3. realpath() per-directory adds syscalls on deep non-symlinked trees (correct, minor perf note)

The earlier Codex review rounds already addressed the substantive edge cases (binding collisions, Node 22.0/22.1 withFileTypes, symlink traversal + distinct aliases, DT_UNKNOWN stat fallback, reproducible binding indices, dev invalidation cleanup), and those fixes are correctly reflected in the current code.

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.

github run

@james-elicx
james-elicx merged commit 87a3f37 into cloudflare:main Jul 31, 2026
58 checks passed
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.

2 participants