From 380e5330b73cb0b1dd3698e0f9e22bb749162475 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:53:09 +0800 Subject: [PATCH] fix(rolldown): exclude dynamic imports from initial chunks --- .../src/node/rolldown/events-manager.ts | 4 ++ .../src/node/utils/__tests__/chunk.test.ts | 70 +++++++++++++++++++ packages/rolldown/src/node/utils/chunk.ts | 4 +- 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 packages/rolldown/src/node/utils/__tests__/chunk.test.ts diff --git a/packages/rolldown/src/node/rolldown/events-manager.ts b/packages/rolldown/src/node/rolldown/events-manager.ts index b3736801f..de82fe455 100644 --- a/packages/rolldown/src/node/rolldown/events-manager.ts +++ b/packages/rolldown/src/node/rolldown/events-manager.ts @@ -348,6 +348,10 @@ export class RolldownEventsManager { this.eventCount = snapshot.eventCount this.lastEvent = snapshot.lastEvent this.chunks = new Map(snapshot.chunks) + // Recompute derived flags when loading caches written by older versions. + const initialChunkIds = getInitialChunkIds(Array.from(this.chunks.values())) + for (const chunk of this.chunks.values()) + chunk.is_initial = initialChunkIds.has(chunk.chunk_id) this.packageGraphReady = snapshot.packageGraphReady ?? !!snapshot.packages?.length this.packages = new Map(snapshot.packages ?? []) this.assets.clear() diff --git a/packages/rolldown/src/node/utils/__tests__/chunk.test.ts b/packages/rolldown/src/node/utils/__tests__/chunk.test.ts new file mode 100644 index 000000000..84904598b --- /dev/null +++ b/packages/rolldown/src/node/utils/__tests__/chunk.test.ts @@ -0,0 +1,70 @@ +import type { RolldownChunkInfo } from '../../../shared/types' +import { describe, expect, it } from 'vitest' +import { RolldownEventsManager } from '../../rolldown/events-manager' +import { getInitialChunkIds } from '../chunk' + +function chunk(options: Pick & Partial): RolldownChunkInfo { + return { + name: null, + advanced_chunk_group_id: null, + is_user_defined_entry: false, + is_async_entry: false, + entry_module: null, + modules: [], + reason: 'common', + imports: [], + ...options, + } +} + +describe('initial chunks', () => { + it('follows static chains and cycles while excluding dynamic-only subtrees', () => { + const chunks = [ + chunk({ chunk_id: 0, is_user_defined_entry: true, imports: [{ chunk_id: 1, kind: 'import-statement' }, { chunk_id: 2, kind: 'dynamic-import' }] }), + chunk({ chunk_id: 1, imports: [{ chunk_id: 0, kind: 'import-statement' }, { chunk_id: 3, kind: 'import-statement' }] }), + chunk({ chunk_id: 2, imports: [{ chunk_id: 4, kind: 'import-statement' }] }), + chunk({ chunk_id: 3, imports: [] }), + chunk({ chunk_id: 4, imports: [] }), + chunk({ chunk_id: 5, is_user_defined_entry: true, imports: [{ chunk_id: 3, kind: 'import-statement' }] }), + ] + expect([...getInitialChunkIds(chunks)].sort()).toEqual([0, 1, 3, 5]) + }) + + it('includes chunks reachable through both static and dynamic imports', () => { + const chunks = [ + chunk({ chunk_id: 0, is_user_defined_entry: true, imports: [{ chunk_id: 1, kind: 'dynamic-import' }, { chunk_id: 2, kind: 'import-statement' }] }), + chunk({ chunk_id: 1, imports: [{ chunk_id: 3, kind: 'import-statement' }] }), + chunk({ chunk_id: 2, imports: [{ chunk_id: 1, kind: 'import-statement' }] }), + chunk({ chunk_id: 3, imports: [] }), + ] + expect([...getInitialChunkIds(chunks)].sort()).toEqual([0, 1, 2, 3]) + }) + + it('keeps user-defined entries initial even when another entry imports them dynamically', () => { + const chunks = [ + chunk({ chunk_id: 0, is_user_defined_entry: true, imports: [{ chunk_id: 1, kind: 'dynamic-import' }] }), + chunk({ chunk_id: 1, is_user_defined_entry: true, imports: [{ chunk_id: 2, kind: 'import-statement' }] }), + chunk({ chunk_id: 2, imports: [] }), + ] + expect([...getInitialChunkIds(chunks)].sort()).toEqual([0, 1, 2]) + }) + + it('returns no initial chunks without user-defined entries', () => { + expect([...getInitialChunkIds([])]).toEqual([]) + expect([...getInitialChunkIds([chunk({ chunk_id: 0 })])]).toEqual([]) + }) + + it('recomputes initial flags when restoring an older snapshot', () => { + const chunks = [ + chunk({ chunk_id: 0, is_user_defined_entry: true, imports: [{ chunk_id: 1, kind: 'import-statement' }, { chunk_id: 2, kind: 'dynamic-import' }] }), + chunk({ chunk_id: 1, imports: [] }), + chunk({ chunk_id: 2, imports: [{ chunk_id: 3, kind: 'import-statement' }] }), + chunk({ chunk_id: 3, imports: [] }), + ] + const manager = new RolldownEventsManager() + const snapshot = manager.snapshot() + snapshot.chunks = chunks.map(chunk => [chunk.chunk_id, { ...chunk, is_initial: true }]) + manager.restore(snapshot) + expect([...manager.chunks.values()].filter(chunk => chunk.is_initial).map(chunk => chunk.chunk_id).sort()).toEqual([0, 1]) + }) +}) diff --git a/packages/rolldown/src/node/utils/chunk.ts b/packages/rolldown/src/node/utils/chunk.ts index e2fe56be0..5655a49a5 100644 --- a/packages/rolldown/src/node/utils/chunk.ts +++ b/packages/rolldown/src/node/utils/chunk.ts @@ -1,6 +1,6 @@ import type { RolldownChunkInfo } from '../../shared/types' -// initial chunks = user-defined entry chunk (initial entry chunk) + user-defined entry chunk's imports (initial common chunk) +// Initial chunks are user-defined entry chunks and their transitive static imports. export function getInitialChunkIds(chunks: RolldownChunkInfo[]) { const chunkMap = new Map(chunks.map(chunk => [chunk.chunk_id, chunk])) const entryChunkIds = chunks.filter(chunk => !!chunk.is_user_defined_entry).map(chunk => chunk.chunk_id) @@ -18,7 +18,7 @@ export function getInitialChunkIds(chunks: RolldownChunkInfo[]) { const chunk = chunkMap.get(chunkId) if (chunk?.imports) { for (const _import of chunk.imports) { - if (!initialChunkIds.has(_import.chunk_id)) { + if (_import.kind !== 'dynamic-import' && !initialChunkIds.has(_import.chunk_id)) { initialChunkIds.add(_import.chunk_id) queue.push(_import.chunk_id) }