diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index e5a07630355cdb..ae84f7eb6d0a6e 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -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(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 { @@ -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. diff --git a/playground/dynamic-import/__tests__/dynamic-import.spec.ts b/playground/dynamic-import/__tests__/dynamic-import.spec.ts index 384f4190295f46..76589d94b493fb 100644 --- a/playground/dynamic-import/__tests__/dynamic-import.spec.ts +++ b/playground/dynamic-import/__tests__/dynamic-import.spec.ts @@ -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(() => diff --git a/playground/dynamic-import/index.html b/playground/dynamic-import/index.html index 1289efc3e1ac4e..a5a0832ba5c299 100644 --- a/playground/dynamic-import/index.html +++ b/playground/dynamic-import/index.html @@ -45,6 +45,9 @@
+
todo
+
todo
+