Skip to content

Conversation

@danielroe
Copy link
Member

@danielroe danielroe commented Aug 27, 2025

🔗 Linked issue

resolves #27863

related: #26565 and #29624

📚 Description

Background

By default, JS chunks emitted in a vite build are hashed. This means they can be cached immutably because their content never changes without a file name change.

However, there are some very significant consequences. Namely, a change to a single component in a Nuxt build can cause every hash to be invalidated, massively increasing the chance of 404s.

Here's an example:

  1. a component is changed slightly. the hash of its JS chunk changes
  2. the page which uses the component has to be updated to reference the new file name
  3. the entry now has its hash changed because it dynamically imports the page
  4. EVERY OTHER FILE which imports the entry has its hash changed because the entry file name is changed.

In a Nuxt app almost every file will import the entry, so it is particularly prone to this.

Other projects, like Vitepress, use a static hashmap which is embedded in the HTML and allows mapping every chunk.

Approach

This PR improves chunk stability by using an import map to resolve the entry chunk of the bundle.

This injects an import map at the top of your <head> tag:

<script type="importmap">{"imports":{"#entry":"/_nuxt/DC5HVSK5.js"}}</script>

Within the script chunks emitted by Vite, imports will be from #entry. This means that changes to the entry will not invalidate chunks which are otherwise unchanged. In the example above, steps 1, 2 and 3 still occur, but at that point a 'boundary' is in place and the rest of the project does not have its hashes invalidated.

Importantly, although we are rewriting imports to use #entry they are still treated as hashed assets by the browser so this should not impact (desired) cache invalidation.

This is more lightweight than the Vitepress approach of embedding a hashmap of the entire project.

If you need to disable this feature (to support Safari <16.4, for example - cross-browser lack of support for import maps is at 1.75%) you can do so:

export default defineNuxtConfig({
  experimental: {
    entryImportMap: false
  },
  // or, better, simply tell vite your desired target
  // which nuxt will respect
  vite: {
    build: {
      target: 'safari13'
    },
  },
})

@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@danielroe danielroe requested review from a team, atinux and pi0 and removed request for a team August 27, 2025 08:51
@danielroe danielroe moved this to Discussing in Team Board Aug 27, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 27, 2025

Warning

Rate limit exceeded

@danielroe has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 18 minutes and 52 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 9dd43ae and 578c128.

📒 Files selected for processing (1)
  • docs/2.guide/3.going-further/1.experimental-features.md (1 hunks)

Walkthrough

Adds an experimental feature to stabilise the client entry chunk using an import map. Introduces StableEntryPlugin for Vite client builds to capture the final hashed entry filename and rewrite entry imports to a stable alias (#entry). Registers a virtual module #internal/entry-chunk.mjs in Nitro to expose the entry filename. Server renderer injects an import map in the head mapping #entry to the built asset URL when present. Schema adds experimental.entryImportMap (default true). Documentation section about entryImportMap was added twice, creating a duplicate.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/vite-chunks

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (9)
packages/schema/src/types/schema.ts (1)

1447-1451: Clarify scope in JSDoc (Vite-only, prod-only).

Minor doc enhancement to prevent misconfiguration when using non‑Vite builders or dev mode.

     /**
-     * Whether to improve chunk stability by using an import map to resolve the entry chunk of the bundle.
+     * Whether to improve chunk stability by using an import map to resolve the entry chunk of the bundle.
+     * Applies to the Vite client build in production when hashed entry filenames are used.
+     * Ignored in development and by other builders.
      */
     entryImportMap: boolean
packages/vite/src/client.ts (1)

136-138: Consider a fallback for browsers without native import maps.

StableEntryPlugin rewrites imports to #entry. On browsers lacking import map support, this will fail at runtime. Two options:

  • Document the requirement and recommend es-module-shims for legacy targets.
  • Or conditionally inject a shim when experimental.entryImportMap is enabled and the project targets such browsers.

Would you like me to open a follow-up proposing conditional shim injection and a docs note on supported browsers?

docs/2.guide/3.going-further/1.experimental-features.md (1)

641-662: Add builder scope and browser support note.

Call out that this is a Vite client build feature and that native import maps are required (or a shim).

 ## entryImportMap

-By default, Nuxt improves chunk stability by using an import map to resolve the entry chunk of the bundle.
+By default, Nuxt (Vite builder) improves chunk stability by using an import map to resolve the entry chunk of the bundle.
+
+::note
+This requires native import map support (e.g. Chrome 89+, Firefox 108+, Safari 16.4+) or a polyfill such as `es-module-shims` if you target older browsers.
+::
packages/nuxt/src/core/nitro.ts (1)

133-134: Virtual module placeholder is sensible; minor robustness consideration

The placeholder export works and will be overridden by the Vite plugin. If you want to make the intent explicit for TS consumers and future readers, consider exporting a typed value (string | undefined) and a JSDoc explaining that Vite replaces this at build time. Not required, just a small clarity win.

Example:

-      '#internal/entry-chunk.mjs': () => `export const entryFileName = undefined`,
+      '#internal/entry-chunk.mjs': () => [
+        '/** Resolved at client build time by StableEntryPlugin */',
+        'export const entryFileName /*: string | undefined*/ = undefined',
+      ].join('\n'),
packages/vite/src/plugins/stable-entry.ts (5)

16-20: Avoid duplicating assignment; centralise virtual registration

You’re assigning both options.virtual and options._config.virtual to the same factory. Fine, but a tiny helper avoids drift and makes intent clearer.

-  nitro.options.virtual ||= {}
-  nitro.options._config.virtual ||= {}
-
-  nitro.options._config.virtual['#internal/entry-chunk.mjs'] = nitro.options.virtual['#internal/entry-chunk.mjs'] = () => `export const entryFileName = ${JSON.stringify(entryFileName)}`
+  const setVirtual = (key: string, fn: () => string) => {
+    nitro.options.virtual ||= {}
+    nitro.options._config.virtual ||= {}
+    nitro.options.virtual[key] = fn
+    nitro.options._config.virtual[key] = fn
+  }
+  setVirtual('#internal/entry-chunk.mjs', () => `export const entryFileName = ${JSON.stringify(entryFileName)}`)

27-33: Plugin gating is good; consider guarding for non-string patterns

Nice check to only run when hashed entry names are enabled. Add a defensive branch for functions/arrays to avoid silent false negatives if users customise entryFileNames as a function.

-      return toArray(config.build?.rollupOptions?.output).some(output => typeof output?.entryFileNames === 'string' && output?.entryFileNames.includes('[hash]'))
+      return toArray(config.build?.rollupOptions?.output).some((output) => {
+        const pattern = output?.entryFileNames
+        if (typeof pattern === 'string') { return pattern.includes('[hash]') }
+        // if a function is used assume hashing may be applied
+        return typeof pattern === 'function'
+      })

40-43: Regex is safe from ReDoS but too narrow for absolute paths

The basename is escaped (good), but [\./]* won’t match path segments like /_nuxt/. Use a broader class bounded by quotes.

-      const filename = new RegExp(`(?<=['"])[\\./]*${escapeStringRegexp(basename(entry))}`, 'g')
+      const filename = new RegExp(String.raw`(?<=['"])[^'"]*?${escapeStringRegexp(basename(entry))}(?=['"])`, 'g')

Note: This also quietens false positives from static analysers without weakening safety.


51-58: Prefix trimming: ensure trailing slash for consistency

When buildAssetsDir is customised without a trailing slash, startsWith(prefix) may not match. Normalise to include a trailing slash before compare/slice.

-      const prefix = withoutLeadingSlash(nuxt.options.app.buildAssetsDir)
+      const prefix = withoutLeadingSlash(nuxt.options.app.buildAssetsDir).replace(/\/?$/, '/')

10-60: Add a minimal test to lock behaviour

Consider a unit/integration test that builds a tiny project twice with a one-line change in a component and asserts:

  • Only the entry filename changes.
  • All imports of the entry are rewritten to '#entry'.
  • The virtual module exports the final entryFileName.

I can provide a fixture and pnpm script if helpful.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e73827a and e7f67b7.

📒 Files selected for processing (7)
  • docs/2.guide/3.going-further/1.experimental-features.md (1 hunks)
  • packages/nuxt/src/core/nitro.ts (1 hunks)
  • packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts (2 hunks)
  • packages/schema/src/config/experimental.ts (1 hunks)
  • packages/schema/src/types/schema.ts (1 hunks)
  • packages/vite/src/client.ts (2 hunks)
  • packages/vite/src/plugins/stable-entry.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/schema/src/types/schema.ts
  • packages/vite/src/client.ts
  • packages/vite/src/plugins/stable-entry.ts
  • packages/schema/src/config/experimental.ts
  • packages/nuxt/src/core/nitro.ts
  • packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts
🧠 Learnings (1)
📚 Learning: 2024-11-05T15:22:54.759Z
Learnt from: GalacticHypernova
PR: nuxt/nuxt#26468
File: packages/nuxt/src/components/plugins/loader.ts:24-24
Timestamp: 2024-11-05T15:22:54.759Z
Learning: In `packages/nuxt/src/components/plugins/loader.ts`, the references to `resolve` and `distDir` are legacy code from before Nuxt used the new unplugin VFS and will be removed.

Applied to files:

  • packages/nuxt/src/core/nitro.ts
🧬 Code graph analysis (2)
packages/vite/src/client.ts (1)
packages/vite/src/plugins/stable-entry.ts (1)
  • StableEntryPlugin (10-60)
packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts (1)
packages/nuxt/src/core/runtime/nitro/utils/paths.ts (1)
  • buildAssetsURL (14-16)
🪛 ast-grep (0.38.6)
packages/vite/src/plugins/stable-entry.ts

[warning] 39-39: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp((?<=['"])[\\./]*${escapeStringRegexp(basename(entry))}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🪛 LanguageTool
docs/2.guide/3.going-further/1.experimental-features.md

[uncategorized] ~653-~653: Possible missing comma found.
Context: ...unchanged. If you need to disable this feature you can do so: ```ts twoslash [nuxt.co...

(AI_HYDRA_LEO_MISSING_COMMA)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: test-fixtures (windows-latest, built, webpack, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, built, rspack, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, dev, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, built, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, webpack, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, webpack, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, rspack, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: release-pkg-pr-new
  • GitHub Check: test-benchmark
  • GitHub Check: test-size
  • GitHub Check: typecheck (ubuntu-latest, bundler)
  • GitHub Check: typecheck (windows-latest, bundler)
  • GitHub Check: code
🔇 Additional comments (5)
packages/vite/src/client.ts (1)

23-23: LGTM: plugin import is correct.

Import path and placement look good.

packages/schema/src/config/experimental.ts (1)

219-219: Defaulting to true: confirm builder compatibility.

This enables the feature for all builders, though only Vite consumes it. That’s fine, but please double‑check no unintended side effects occur for webpack/rspack.

If you want stricter scoping, I can propose a resolver that enables it by default only when builder === 'vite'.

packages/nuxt/src/core/runtime/nitro/handlers/renderer.ts (2)

31-32: LGTM: virtual import for entry filename.

Importing from the virtual module is appropriate and aligns with the Vite plugin’s writeBundle hook.


187-197: Ensure CSP nonce on inline import map and verify SSRContext field

It looks like the head runtime already supports a nonce attribute for script tags (see packages/nuxt/src/head/runtime/components.ts defining nonce: String). Before merging this patch, please confirm which property on your SSR context holds the CSP nonce (e.g. ssrContext.cspNonce or simply ssrContext.nonce).

Key points to address:

  • Add the script tag’s nonce only when the SSR context exposes it.
  • Retain tagPriority: -2 to guarantee earliest insertion before module scripts.
  • Update the spread logic once you’ve confirmed the exact property name.

Suggested diff (adjust property name as needed):

 if (entryFileName && !NO_SCRIPTS) {
   ssrContext.head.push({
     script: [{
       tagPosition: 'head',
       tagPriority: -2,
       type: 'importmap',
       innerHTML: JSON.stringify({ imports: { '#entry': buildAssetsURL(entryFileName) } }),
-      // Ensure CSP compatibility where nonces are used
-      ...(ssrContext as any).cspNonce ? { nonce: (ssrContext as any).cspNonce } : {},
+      // Inject CSP nonce if available on the SSR context
+      ...((ssrContext as any).cspNonce ?? (ssrContext as any).nonce
+        ? { nonce: (ssrContext as any).cspNonce ?? (ssrContext as any).nonce }
+        : {}),
     }],
   }, headEntryOptions)
 }
packages/vite/src/plugins/stable-entry.ts (1)

35-50: Clarify renderChunk vs generateBundle usage and improve the entry-chunk regex

  • meta.chunks in renderChunk is part of Rollup’s public API, but its fileName values contain hash placeholders rather than the actual hashes (rollupjs.org).
  • If you need to rewrite code using the final hashed filenames, use the generateBundle hook (which receives bundle: { [fileName]: OutputAsset | OutputChunk }) to access real fileNames (rollupjs.org).
  • You can still use renderChunk, but be aware of placeholder names (guard meta?.chunks) and that placeholders only get resolved later.
  • In either hook, update the regex to match nested paths (e.g. /_nuxt/entry-*.js), not just ./ or / prefixes.

Suggested diffs:

Option A: Continue in renderChunk with improved regex and a guard

     renderChunk(code: string, chunk: RenderedChunk, _options, meta) {
-      const entry = Object.values(meta.chunks).find(c => c.isEntry && c.name === 'entry')?.fileName
+      if (!meta?.chunks) { return null }
+      const entry = Object.values(meta.chunks).find(c => c.isEntry && c.name === 'entry')?.fileName
       if (!entry || !chunk.imports.includes(entry)) {
         return
       }
-      const filename = new RegExp(`(?<=['"])[\\./]*${escapeStringRegexp(basename(entry))}`, 'g')
+      const filenameRE = new RegExp(String.raw`(?<=['"])[^'"]*?${escapeStringRegexp(basename(entry))}(?=['"])`, 'g')
       const s = new MagicString(code)
-      s.replaceAll(filename, '#entry')
+      s.replaceAll(filenameRE, '#entry')

Option B: Switch to generateBundle to work with actual filenames

-    renderChunk (code, chunk, _options, meta) { … },
+    generateBundle(_options: OutputOptions, bundle) {
+      const entryFile = Object.values(bundle).find(
+        (c): c is OutputChunk => c.type === 'chunk' && c.isEntry && c.name === 'entry'
+      )?.fileName
+      if (!entryFile) { return }
+      const base = escapeStringRegexp(basename(entryFile))
+      const filenameRE = new RegExp(String.raw`(?<=['"])[^'"]*?${base}(?=['"])`, 'g')
+
+      for (const chunk of Object.values(bundle)) {
+        if (chunk.type !== 'chunk' || !chunk.imports?.includes(entryFile)) { continue }
+        const s = new MagicString(chunk.code)
+        s.replaceAll(filenameRE, '#entry')
+        if (s.hasChanged()) {
+          chunk.code = s.toString()
+          if (sourcemap) { chunk.map = s.generateMap({ hires: true }) }
+        }
+      }
+    },

Likely an incorrect or invalid review comment.

@codspeed-hq
Copy link

codspeed-hq bot commented Aug 27, 2025

CodSpeed Performance Report

Merging #33075 will not alter performance

Comparing feat/vite-chunks (578c128) with main (8c821f1)

Summary

✅ 10 untouched benchmarks

@pkg-pr-new
Copy link

pkg-pr-new bot commented Aug 27, 2025

Open in StackBlitz

@nuxt/kit

npm i https://pkg.pr.new/@nuxt/kit@33075

nuxt

npm i https://pkg.pr.new/nuxt@33075

@nuxt/rspack-builder

npm i https://pkg.pr.new/@nuxt/rspack-builder@33075

@nuxt/schema

npm i https://pkg.pr.new/@nuxt/schema@33075

@nuxt/vite-builder

npm i https://pkg.pr.new/@nuxt/vite-builder@33075

@nuxt/webpack-builder

npm i https://pkg.pr.new/@nuxt/webpack-builder@33075

commit: 578c128

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (8)
packages/vite/src/plugins/stable-entry.ts (8)

16-19: Factor out the virtual module generator and avoid chained assignment for clarity

This reduces cognitive load and makes it easier to extend later (e.g., more exports).

Apply this diff:

-  nitro.options.virtual ||= {}
-  nitro.options._config.virtual ||= {}
-
-  nitro.options._config.virtual['#internal/entry-chunk.mjs'] = nitro.options.virtual['#internal/entry-chunk.mjs'] = () => `export const entryFileName = ${JSON.stringify(entryFileName)}`
+  nitro.options.virtual ||= {}
+  nitro.options._config.virtual ||= {}
+  const renderEntryChunkVirtual = () => `export const entryFileName = ${JSON.stringify(entryFileName)}`
+  nitro.options.virtual['#internal/entry-chunk.mjs'] = renderEntryChunkVirtual
+  nitro.options._config.virtual['#internal/entry-chunk.mjs'] = renderEntryChunkVirtual

26-39: Prefer the full Vite apply(config, env) signature and short-circuit non-build commands

This makes intent explicit and avoids any type friction with Vite’s Plugin['apply'] overloads.

Apply this diff:

-    apply (config) {
+    apply (config, env) {
+      if (env?.command !== 'build') { return false }
       if (nuxt.options.dev || !nuxt.options.experimental.entryImportMap) {
         return false
       }
       if (config.build?.target) {
         const targets = toArray(config.build.target)
         if (!targets.every(isSupported)) {
           return false
         }
       }
       // only apply plugin if the entry file name is hashed
       return toArray(config.build?.rollupOptions?.output).some(output => typeof output?.entryFileNames === 'string' && output?.entryFileNames.includes('[hash]'))
     },

40-44: Avoid shadowing chunk for readability

Minor readability: don’t shadow the outer chunk variable in the find callback.

Apply this diff:

-      const entry = Object.values(meta.chunks).find(chunk => chunk.isEntry && chunk.name === 'entry')?.fileName
+      const entry = Object.values(meta.chunks).find(c => c.isEntry && c.name === 'entry')?.fileName

46-49: Make the replacement regex more robust (nested paths and querystrings) while staying safe

Current pattern misses cases like "/_nuxt/entry/XYZ.js" or specifiers with query strings. Using escapeStringRegexp is good; extend the non-greedy path segment and optional query handling.

Apply this diff:

-      const filename = new RegExp(`(?<=['"])[\\./]*${escapeStringRegexp(basename(entry))}`, 'g')
+      const entryBase = escapeStringRegexp(basename(entry))
+      // Match optional path segments and optional query, bounded by quotes
+      const filename = new RegExp(`(?<=['"])(?:[^"'\\n]*/)?${entryBase}(?:\\?[^'"]*)?(?=['"])`, 'g')

Note: Static analysis warning about variable-driven regex is mitigated by escapeStringRegexp; the rest is simple character classes, so ReDoS risk is negligible.


41-49: Guard against false negatives by relaxing the import check

RenderedChunk.imports contains chunk fileNames; in some outputs it may include path segments. Checking only includes(entry) could skip valid chunks. Consider suffix match.

Apply this diff:

-      if (!entry || !chunk.imports.includes(entry)) {
+      if (!entry || !chunk.imports.some(i => i === entry || i.endsWith('/' + basename(entry)))) {
         return
       }

57-64: Normalise entryFileName consistently with buildAssetsDir

If Rollup prefixes fileNames with assets/, keep it; if the fileName is absolute, strip buildAssetsDir with or without leading slash for safety.

Apply this diff:

-      const prefix = withoutLeadingSlash(nuxt.options.app.buildAssetsDir)
-      if (entry?.startsWith(prefix)) {
-        entry = entry.slice(prefix.length)
-      }
+      const prefix = withoutLeadingSlash(nuxt.options.app.buildAssetsDir)
+      if (entry?.startsWith('/' + prefix)) {
+        entry = entry.slice(prefix.length + 1)
+      } else if (entry?.startsWith(prefix)) {
+        entry = entry.slice(prefix.length)
+      }

69-87: Consider expanding target parsing and documenting unsupported engines

build.target often includes es20xx or mixed entries. Your fallback is sensible; adding a short comment and supporting common aliases (e.g., ios_saf) could reduce surprises.

Apply this diff:

-const supportedEnvironments = {
+// Minimal set; unknown engines are treated as supported to avoid over-restricting.
+const supportedEnvironments: Record<string, number> = {
   chrome: 89,
   edge: 89,
   firefox: 108,
   ie: Infinity,
   ios: 16.4,
   opera: 75,
   safari: 16.4,
+  // Common esbuild aliases (mapped to Safari iOS engine)
+  ios_saf: 16.4,
 }

1-88: Add targeted tests for entry rewrite and import-map gating

Recommend unit/integration tests to lock behaviour:

  • renderChunk rewrites imports to #entry only when the entry is imported.
  • writeBundle exposes the correct entryFileName relative to buildAssetsDir.
  • apply gating respects experimental.entryImportMap, dev mode, and unsupported targets.

I can draft a minimal test harness using a synthetic Rollup bundle and verify renderChunk/writeBundle outputs if helpful.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e7f67b7 and 2b5365e.

📒 Files selected for processing (1)
  • packages/vite/src/plugins/stable-entry.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/vite/src/plugins/stable-entry.ts
🪛 ast-grep (0.38.6)
packages/vite/src/plugins/stable-entry.ts

[warning] 45-45: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp((?<=['"])[\\./]*${escapeStringRegexp(basename(entry))}, 'g')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: code
🔇 Additional comments (1)
packages/vite/src/plugins/stable-entry.ts (1)

21-65: Overall plugin logic LGTM; nice gating and minimal surface area

Good use of applyToEnvironment, hashed-entry gating, and virtual module exposure. The approach should materially improve chunk stability with low overhead.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
docs/2.guide/3.going-further/1.experimental-features.md (3)

651-655: Add a blank line before the admonition for correct rendering.

Keeps formatting consistent with other sections and avoids parser hiccups.

Apply this diff:

-Within the script chunks emitted by Vite, imports will be from `#entry`. This means that changes to the entry will not invalidate chunks which are otherwise unchanged.
-::note
+Within the script chunks emitted by Vite, imports will reference `#entry` instead of the hashed entry filename. This prevents otherwise-unchanged chunks from being invalidated when only the entry changes.
+
+::note

647-649: Format the import map example for readability.

Multiline JSON improves scanability and aligns with other docs samples.

Apply this diff:

-```html
-<script type="importmap">{"imports":{"#entry":"/_nuxt/DC5HVSK5.js"}}</script>
-```
+```html
+<script type="importmap">
+{ "imports": { "#entry": "/_nuxt/DC5HVSK5.js" } }
+</script>
+```

641-666: Clarify scope (client build only) and add references for consistency with other sections.

Most experimental sections include default/scope notes and a read-more link.

Apply this diff:

 ## entryImportMap

-By default, Nuxt improves chunk stability by using an import map to resolve the entry chunk of the bundle.
+By default, Nuxt improves chunk stability by using an import map to resolve the entry chunk of the bundle.
+
+*Enabled by default for client builds.*

Optionally add a read-more block after the section:

+::read-more{icon="i-simple-icons-github" to="https://github.com/nuxt/nuxt/pull/33075" target="_blank"}
+See PR #33075 for implementation details and discussion.
+::
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2b5365e and 9dd43ae.

📒 Files selected for processing (1)
  • docs/2.guide/3.going-further/1.experimental-features.md (1 hunks)
🧰 Additional context used
🪛 LanguageTool
docs/2.guide/3.going-further/1.experimental-features.md

[uncategorized] ~652-~652: Loose punctuation mark.
Context: ... chunks which are otherwise unchanged. ::note Nuxt smartly disables this feature...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~654-~654: Loose punctuation mark.
Context: ...a value that does not include [hash]. :: If you need to disable this feature y...

(UNLIKELY_OPENING_PUNCTUATION)


[uncategorized] ~657-~657: Possible missing comma found.
Context: ...hash]`. :: If you need to disable this feature you can do so: ```ts twoslash [nuxt.co...

(AI_HYDRA_LEO_MISSING_COMMA)

@danielroe danielroe merged commit 926782d into main Aug 28, 2025
83 of 85 checks passed
@danielroe danielroe deleted the feat/vite-chunks branch August 28, 2025 10:35
@github-project-automation github-project-automation bot moved this from Discussing to Later in Team Board Aug 28, 2025
@florian-strasser
Copy link

I guess this could also have an impact on #32105

@fikryrmdhna
Copy link

Is this only for Nuxt version 4? Can it be used for Nuxt version 3? @danielroe

@TheAlexLichter
Copy link
Member

@fikryrmdhna features are still backported to Nuxt 3. See https://github.com/nuxt/nuxt/releases/tag/v3.19.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Later

Development

Successfully merging this pull request may close these issues.

reduce likelihood of chunk hash changes when changing code

5 participants