Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 31 additions & 5 deletions packages/vite/src/node/plugins/importAnalysisBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,36 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin[] {
}

if (imports.length) {
// Each import is wrapped as `__vitePreload(factory, __VITE_PRELOAD__)`, so its
// marker comes right after it. Nested imports nest the wrappers — in
// `import('a').then(() => import('b'))` b's marker ends up before a's — so imports
// and markers form balanced brackets. Pairing each import with the next marker
// would give b's marker to both and lose a's CSS to `void 0` (#22700); instead,
// match them with a stack in one pass: each marker closes the innermost open import.
const importMarkerPos = new Array<number>(imports.length).fill(-1)
const openImports: number[] = []
let nextImport = 0
let markerStartPos = findPreloadMarker(code, imports[0].e)
while (markerStartPos !== -1) {
while (
nextImport < imports.length &&
imports[nextImport].e <= markerStartPos
) {
openImports.push(nextImport++)
}
if (openImports.length) {
importMarkerPos[openImports.pop()!] = markerStartPos
}
markerStartPos = findPreloadMarker(
code,
markerStartPos + preloadMarker.length,
)
}
// #3051: a lone import whose marker isn't placed after it pairs with the only marker
if (imports.length === 1 && importMarkerPos[0] === -1) {
importMarkerPos[0] = findPreloadMarker(code)
}

for (let index = 0; index < imports.length; index++) {
// To handle escape sequences in specifier strings, the .n field will be provided where possible.
const {
Expand Down Expand Up @@ -438,11 +468,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin[] {
addDeps(normalizedFile)
}

let markerStartPos = findPreloadMarker(code, end)
// fix issue #3051
if (markerStartPos === -1 && imports.length === 1) {
markerStartPos = findPreloadMarker(code)
}
const markerStartPos = importMarkerPos[index]

if (markerStartPos > 0) {
// the dep list includes the main chunk, so only need to reload when there are actual other deps.
Expand Down
38 changes: 38 additions & 0 deletions playground/dynamic-import/__tests__/dynamic-import.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,44 @@ test('should work with load ../ and contain itself directory', async () => {
.toMatch('dynamic-import-nested-self-content')
})

// #22700: nested `import('a').then(() => import('b'))` where `a` has a CSS
// side-effect dep — the outer import's CSS must still be loaded in build output
test('should load css of nested dynamic import', async () => {
await expect
.poll(() => page.textContent('.then-css-outer'))
.toMatch('then-css-outer')
await expect.poll(() => getColor('.then-css-outer')).toBe('red')
await expect
.poll(() => page.textContent('.then-css-inner'))
.toMatch('then-css-inner')
await expect.poll(() => getColor('.then-css-inner')).toBe('green')
})

// #22721: each nested dynamic import must preload its OWN chunk's css. The inner
// import is immediately followed by its own `__vite__mapDeps(...)` (its `.then`
// callback contains no further import, so the first dep list after it is its
// own), so we read that list back and confirm it resolves to the inner css. A
// pairing that walks imports front-to-back and skips already-claimed markers
// swaps the lists, putting the outer chunk's css here instead.
test.runIf(isBuild)(
'should preload its own css for a nested dynamic import',
() => {
const js = findAssetFile(/index-[-\w]{8}\.js$/) ?? ''
const depTable = js.match(/m\.f=(\[[^\]]*\])/)
const innerCall = js.match(
/import\([^)]*\binner-[-\w]+\.js[^)]*\)[\s\S]*?__vite__mapDeps\(\[([\d,]+)\]\)/,
)
expect(depTable, 'preload dep table not found').not.toBeNull()
expect(innerCall, 'inner import preload call not found').not.toBeNull()
const files: string[] = JSON.parse(depTable![1])
const innerPreloads = innerCall![1].split(',').map((i) => files[Number(i)])
expect(innerPreloads.some((f) => /\binner-[-\w]+\.css$/.test(f))).toBe(true)
expect(innerPreloads.some((f) => /\bouter-[-\w]+\.css$/.test(f))).toBe(
false,
)
},
)

test('should work a load path that contains parentheses.', async () => {
await expect
.poll(() =>
Expand Down
3 changes: 3 additions & 0 deletions playground/dynamic-import/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@

<div class="dynamic-import-nested-self"></div>

<div class="then-css-outer">todo</div>
<div class="then-css-inner">todo</div>

<script type="module" src="./nested/index.js"></script>
<script type="module" src="./(app)/nest/index.js"></script>
<style>
Expand Down
9 changes: 9 additions & 0 deletions playground/dynamic-import/nested/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,13 @@ import(`../nested/static.js`).then((mod) => {
text('.dynamic-import-static', mod.self)
})

// #22700: in a nested `import().then(() => import())`, the outer import's CSS
// dep used to be dropped to `void 0` in the build output, orphaning the CSS.
import('./then-css/outer.js').then((outerMod) => {
text('.then-css-outer', outerMod.outer)
return import('./then-css/inner.js').then((innerMod) => {
text('.then-css-inner', innerMod.inner)
})
})

console.log('index.js')
3 changes: 3 additions & 0 deletions playground/dynamic-import/nested/then-css/inner.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.then-css-inner {
color: #008000;
}
2 changes: 2 additions & 0 deletions playground/dynamic-import/nested/then-css/inner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './inner.css'
export const inner = 'then-css-inner'
3 changes: 3 additions & 0 deletions playground/dynamic-import/nested/then-css/outer.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.then-css-outer {
color: #ff0000;
}
2 changes: 2 additions & 0 deletions playground/dynamic-import/nested/then-css/outer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './outer.css'
export const outer = 'then-css-outer'
Loading