Skip to content

[13.x] Fix Vite CSS not loaded from nested chunk imports#59662

Merged
taylorotwell merged 2 commits into
laravel:13.xfrom
karim1999:fix/vite-recursive-css-imports
Apr 16, 2026
Merged

[13.x] Fix Vite CSS not loaded from nested chunk imports#59662
taylorotwell merged 2 commits into
laravel:13.xfrom
karim1999:fix/vite-recursive-css-imports

Conversation

@karim1999

Copy link
Copy Markdown
Contributor

Description

The __invoke method in Illuminate\Foundation\Vite only resolves CSS from direct imports of an entrypoint (one level deep). When Vite 8 (which uses Rolldown instead of Rollup) builds the manifest, it keeps CSS on the chunk where the component is defined rather than hoisting it to the nearest entry. This causes CSS from deeply nested imports to never receive a <link rel="stylesheet"> tag in the rendered HTML.

The problem

Given this manifest structure:

entry.js
  └── imports: [_layout.js]
        ├── css: [layout.css]        ✅ loaded (direct import)
        └── imports: [_header.js]
              └── css: [header.css]  ❌ never loaded (nested import)

The current code at line 399 iterates $chunk['imports'] but does not recurse into those imports' own imports:

foreach ($chunk['imports'] ?? [] as $import) {
    // only walks direct imports, misses nested ones
}

The fix

This PR extracts import resolution into a resolveImports() method that recursively walks the full import tree with cycle detection via a $seen array. The single call site in __invoke changes from:

foreach ($chunk['imports'] ?? [] as $import) {

to:

foreach ($this->resolveImports($manifest, $chunk) as $import) {

This matches the approach already used by the prefetch code in the same file, which recursively walks imports using a closure.

Why this is backward-compatible

With older Vite versions (5–7) that use Rollup, CSS from nested components was hoisted to the nearest entry or parent chunk. In those manifests, nested chunks have empty or no css arrays, so the recursive walk produces identical output to the current code. The fix only adds CSS tags for chunks that actually declare CSS — which only happens with Vite 8+ (Rolldown) manifests.

References

  • Vite's Backend Integration guide explicitly documents that imports should be resolved recursively: "For each import in the entry point's imports list, recursively follow all the chunks in the imported chunk's imports list"
  • Vite 8 switched from Rollup to Rolldown which changed how CSS is associated with chunks in the manifest
  • The prefetch strategy code in the same file (lines 470–482) already resolves imports recursively

Impact

In a real-world project with 100+ entrypoints, this bug caused 18 CSS files to silently go missing from production builds after upgrading to Vite 8, while everything worked correctly in dev mode (where Vite's dev server handles CSS inline).

Tests

  • Added testViteWithNestedCssImport that creates a manifest with a 3-level import chain (entry → layout → header), each with its own CSS, and asserts both CSS files are loaded
  • Updated testItGeneratesPreloadDirectivesForJsAndCssImports to reflect the new (depth-first) discovery order of preload directives
  • All 54 existing Vite tests pass

karim1999 and others added 2 commits April 13, 2026 13:13
The `__invoke` method only resolved CSS from direct imports (one level
deep). Vite 8 (Rolldown) keeps CSS on the chunk where the component is
defined rather than hoisting it to the nearest entry, so CSS from deeply
nested imports never received a `<link>` tag.

This adds a `resolveImports` method that recursively walks the full
import tree—matching the approach already used by the prefetch code and
documented in Vite's backend integration guide.

The fix is backward-compatible: with older Vite/Rollup builds where CSS
was hoisted, nested chunks have empty `css` arrays, so the recursive
walk produces identical output.
@taylorotwell
taylorotwell merged commit 04130c8 into laravel:13.x Apr 16, 2026
52 checks passed
jonagoldman pushed a commit to deplox/laravel-framework that referenced this pull request Apr 30, 2026
* [13.x] Fix Vite CSS not loaded from nested chunk imports

The `__invoke` method only resolved CSS from direct imports (one level
deep). Vite 8 (Rolldown) keeps CSS on the chunk where the component is
defined rather than hoisting it to the nearest entry, so CSS from deeply
nested imports never received a `<link>` tag.

This adds a `resolveImports` method that recursively walks the full
import tree—matching the approach already used by the prefetch code and
documented in Vite's backend integration guide.

The fix is backward-compatible: with older Vite/Rollup builds where CSS
was hoisted, nested chunks have empty `css` arrays, so the recursive
walk produces identical output.

* Remove unnecessary pass-by-reference

---------

Co-authored-by: Pascal Baljet <[email protected]>
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.

3 participants