@@ -24,28 +24,13 @@ interface ModuleGraph {
2424 getFileName ( referenceId : string ) : string ;
2525}
2626
27- /** Collect the sorted, transitive dependency ids of a module, following static and dynamic imports. */
28- function collectTransitiveDeps ( graph : ModuleGraph , rootId : string ) : string [ ] {
29- const deps = new Set < string > ( ) ;
30- const queue = [ rootId ] ;
31- while ( queue . length > 0 ) {
32- const current = queue . pop ( ) ! ;
33- if ( deps . has ( current ) ) continue ;
34-
35- const modInfo = graph . getModuleInfo ( current ) ;
36- if ( isContentDataIncrementalModule ( modInfo ) ) continue ;
37-
38- deps . add ( current ) ;
39- if ( ! modInfo ) continue ;
27+ interface HashableModuleGraph extends ModuleGraph {
28+ getModuleIds ( ) : IterableIterator < string > ;
29+ }
4030
41- for ( const dep of modInfo . importedIds ) {
42- if ( ! deps . has ( dep ) ) queue . push ( dep ) ;
43- }
44- for ( const dep of modInfo . dynamicallyImportedIds ) {
45- if ( ! deps . has ( dep ) ) queue . push ( dep ) ;
46- }
47- }
48- return [ ...deps ] . sort ( ) ;
31+ interface TransitiveGraphCache {
32+ hashes : Map < string , string > ;
33+ serverIslandModules : Set < string > ;
4934}
5035
5136/** Each placeholder pattern paired with the token that has to be present for it to match. */
@@ -114,12 +99,151 @@ function hashModules(graph: ModuleGraph, sortedIds: string[]): string {
11499 return hasher . digest ( 'hex' ) ;
115100}
116101
102+ /**
103+ * Build a transitive hash for every module with one pass over the dependency graph.
104+ * Strongly connected components collapse cycles into a DAG, whose hashes can be
105+ * folded into every importer without walking shared dependencies again for each root.
106+ */
107+ function createTransitiveGraphCache ( graph : HashableModuleGraph ) : TransitiveGraphCache {
108+ const modules = new Map < string , HashableModuleInfo | null > ( ) ;
109+ const dependencies = new Map < string , string [ ] > ( ) ;
110+ const excludedModules = new Set < string > ( ) ;
111+ const pending = [ ...graph . getModuleIds ( ) ] ;
112+ for ( const id of pending ) {
113+ if ( modules . has ( id ) ) continue ;
114+
115+ const info = graph . getModuleInfo ( id ) ;
116+ modules . set ( id , info ) ;
117+ if ( isContentDataIncrementalModule ( info ) ) {
118+ excludedModules . add ( id ) ;
119+ continue ;
120+ }
121+
122+ const importedIds = [ ...( info ?. importedIds ?? [ ] ) , ...( info ?. dynamicallyImportedIds ?? [ ] ) ] ;
123+ dependencies . set ( id , importedIds ) ;
124+ pending . push ( ...importedIds ) ;
125+ }
126+ for ( const id of excludedModules ) modules . delete ( id ) ;
127+ for ( const [ id , importedIds ] of dependencies ) {
128+ dependencies . set (
129+ id ,
130+ importedIds . filter ( ( importedId ) => ! excludedModules . has ( importedId ) ) ,
131+ ) ;
132+ }
133+
134+ const reverseDependencies = new Map < string , string [ ] > ( ) ;
135+ for ( const id of modules . keys ( ) ) reverseDependencies . set ( id , [ ] ) ;
136+ for ( const [ id , importedIds ] of dependencies ) {
137+ for ( const importedId of importedIds ) reverseDependencies . get ( importedId ) ?. push ( id ) ;
138+ }
139+
140+ const visited = new Set < string > ( ) ;
141+ const finishOrder : string [ ] = [ ] ;
142+ for ( const rootId of modules . keys ( ) ) {
143+ if ( visited . has ( rootId ) ) continue ;
144+ visited . add ( rootId ) ;
145+ const stack : Array < [ string , number ] > = [ [ rootId , 0 ] ] ;
146+ while ( stack . length > 0 ) {
147+ const frame = stack [ stack . length - 1 ] ;
148+ const importedIds = dependencies . get ( frame [ 0 ] ) ?? [ ] ;
149+ if ( frame [ 1 ] < importedIds . length ) {
150+ const importedId = importedIds [ frame [ 1 ] ++ ] ;
151+ if ( ! visited . has ( importedId ) ) {
152+ visited . add ( importedId ) ;
153+ stack . push ( [ importedId , 0 ] ) ;
154+ }
155+ } else {
156+ finishOrder . push ( frame [ 0 ] ) ;
157+ stack . pop ( ) ;
158+ }
159+ }
160+ }
161+
162+ const componentByModule = new Map < string , number > ( ) ;
163+ const components : string [ ] [ ] = [ ] ;
164+ for ( const rootId of finishOrder . toReversed ( ) ) {
165+ if ( componentByModule . has ( rootId ) ) continue ;
166+ const componentIndex = components . length ;
167+ const component : string [ ] = [ ] ;
168+ const stack = [ rootId ] ;
169+ componentByModule . set ( rootId , componentIndex ) ;
170+ while ( stack . length > 0 ) {
171+ const id = stack . pop ( ) ! ;
172+ component . push ( id ) ;
173+ for ( const importerId of reverseDependencies . get ( id ) ?? [ ] ) {
174+ if ( ! componentByModule . has ( importerId ) ) {
175+ componentByModule . set ( importerId , componentIndex ) ;
176+ stack . push ( importerId ) ;
177+ }
178+ }
179+ }
180+ components . push ( component . sort ( ) ) ;
181+ }
182+
183+ const componentDependencies = components . map ( ( ) => new Set < number > ( ) ) ;
184+ const componentImporters = components . map ( ( ) => new Set < number > ( ) ) ;
185+ for ( const [ id , importedIds ] of dependencies ) {
186+ const componentIndex = componentByModule . get ( id ) ! ;
187+ for ( const importedId of importedIds ) {
188+ const dependencyIndex = componentByModule . get ( importedId ) ! ;
189+ if ( dependencyIndex === componentIndex ) continue ;
190+ componentDependencies [ componentIndex ] . add ( dependencyIndex ) ;
191+ componentImporters [ dependencyIndex ] . add ( componentIndex ) ;
192+ }
193+ }
194+
195+ const componentHashes = new Map < number , string > ( ) ;
196+ const componentHasServerIsland = new Map < number , boolean > ( ) ;
197+ const unresolvedDependencies = componentDependencies . map ( ( items ) => items . size ) ;
198+ const ready = unresolvedDependencies . flatMap ( ( count , index ) => ( count === 0 ? [ index ] : [ ] ) ) ;
199+ for ( const componentIndex of ready ) {
200+ const hasher = crypto . createHash ( 'sha256' ) ;
201+ hasher . update ( hashModules ( graph , components [ componentIndex ] ) ) ;
202+ const dependencyHashes = [ ...componentDependencies [ componentIndex ] ]
203+ . map ( ( dependencyIndex ) => componentHashes . get ( dependencyIndex ) ! )
204+ . sort ( ) ;
205+ for ( const dependencyHash of dependencyHashes ) {
206+ hasher . update ( '\n' ) ;
207+ hasher . update ( dependencyHash ) ;
208+ }
209+ componentHashes . set ( componentIndex , hasher . digest ( 'hex' ) ) ;
210+ componentHasServerIsland . set (
211+ componentIndex ,
212+ components [ componentIndex ] . some (
213+ ( id ) => ( modules . get ( id ) ?. meta ?. astro ?. serverComponents ?. length ?? 0 ) > 0 ,
214+ ) ||
215+ [ ...componentDependencies [ componentIndex ] ] . some ( ( dependencyIndex ) =>
216+ componentHasServerIsland . get ( dependencyIndex ) ,
217+ ) ,
218+ ) ;
219+
220+ for ( const importerIndex of componentImporters [ componentIndex ] ) {
221+ unresolvedDependencies [ importerIndex ] -- ;
222+ if ( unresolvedDependencies [ importerIndex ] === 0 ) ready . push ( importerIndex ) ;
223+ }
224+ }
225+
226+ return {
227+ hashes : new Map (
228+ [ ...componentByModule ] . map ( ( [ id , componentIndex ] ) => [
229+ id ,
230+ componentHashes . get ( componentIndex ) ! ,
231+ ] ) ,
232+ ) ,
233+ serverIslandModules : new Set (
234+ [ ...componentByModule ]
235+ . filter ( ( [ , componentIndex ] ) => componentHasServerIsland . get ( componentIndex ) )
236+ . map ( ( [ id ] ) => id ) ,
237+ ) ,
238+ } ;
239+ }
240+
117241/**
118242 * Hash the transitive graph of each client entrypoint and accumulate the result
119243 * against every page that uses it, keyed by page component.
120244 */
121245function collectClientEntrypointHashes (
122- graph : ModuleGraph ,
246+ transitiveHashes : Map < string , string > ,
123247 entrypointIds : Iterable < string > ,
124248 pagesByEntrypoint : Map < string , Set < PageBuildData > > ,
125249 hashesByComponent : Map < string , string [ ] > ,
@@ -128,7 +252,8 @@ function collectClientEntrypointHashes(
128252 const pages = pagesByEntrypoint . get ( entrypointId ) ;
129253 if ( ! pages ?. size ) continue ;
130254
131- const hash = hashModules ( graph , collectTransitiveDeps ( graph , entrypointId ) ) ;
255+ const hash = transitiveHashes . get ( entrypointId ) ;
256+ if ( ! hash ) continue ;
132257 for ( const pageData of pages ) {
133258 let list = hashesByComponent . get ( pageData . component ) ;
134259 if ( ! list ) {
@@ -149,19 +274,20 @@ function collectClientEntrypointHashes(
149274 * build we hash each entrypoint's transitive graph and fold it into the dependency
150275 * hash of every route that uses it.
151276 */
152- function foldClientDependencies ( graph : ModuleGraph , internals : BuildInternals ) : void {
277+ function foldClientDependencies ( graph : HashableModuleGraph , internals : BuildInternals ) : void {
153278 const baseHashes = internals . pageDependencyHashes ;
154279 if ( ! baseHashes ) return ;
155280
281+ const { hashes : transitiveHashes } = createTransitiveGraphCache ( graph ) ;
156282 const hashesByComponent = new Map < string , string [ ] > ( ) ;
157283 collectClientEntrypointHashes (
158- graph ,
284+ transitiveHashes ,
159285 internals . discoveredClientOnlyComponents . keys ( ) ,
160286 internals . pagesByClientOnly ,
161287 hashesByComponent ,
162288 ) ;
163289 collectClientEntrypointHashes (
164- graph ,
290+ transitiveHashes ,
165291 internals . discoveredScripts ,
166292 internals . pagesByScriptId ,
167293 hashesByComponent ,
@@ -188,35 +314,22 @@ function foldClientDependencies(graph: ModuleGraph, internals: BuildInternals):
188314 * entry's graph into another route's hash.
189315 */
190316function collectContentEntryHashes (
191- graph : ModuleGraph & { getModuleIds ( ) : IterableIterator < string > } ,
317+ graph : HashableModuleGraph ,
192318 root : URL ,
319+ transitiveHashes : Map < string , string > ,
193320) : Map < string , string > {
194321 const entryHashes = new Map < string , string > ( ) ;
195322 for ( const id of graph . getModuleIds ( ) ) {
196323 if ( ! hasContentFlag ( id , PROPAGATED_ASSET_FLAG ) ) continue ;
197324 // e.g. "/abs/src/content/docs/one.mdx?astroPropagatedAssets" -> render module id.
198325 const renderModuleId = removeQueryString ( id ) ;
199326 const key = rootRelativePath ( root , renderModuleId , false ) ;
200- const deps = collectTransitiveDeps ( graph , renderModuleId ) ;
201- entryHashes . set ( key , hashModules ( graph , deps ) ) ;
327+ const hash = transitiveHashes . get ( renderModuleId ) ;
328+ if ( hash ) entryHashes . set ( key , hash ) ;
202329 }
203330 return entryHashes ;
204331}
205332
206- /**
207- * Whether any module in a page's render graph uses a server island. The Astro
208- * compiler records `server:defer` usage as `serverComponents` metadata on the
209- * module that hosts it, so a page (or one of its layouts/components) that renders
210- * an island is detectable from the graph it already walks for hashing.
211- */
212- function pageContainsServerIsland ( graph : ModuleGraph , ids : string [ ] ) : boolean {
213- for ( const id of ids ) {
214- const serverComponents = graph . getModuleInfo ( id ) ?. meta ?. astro ?. serverComponents ;
215- if ( serverComponents ?. length ) return true ;
216- }
217- return false ;
218- }
219-
220333/**
221334 * Captures a dependency hash for each prerendered page route during the build.
222335 *
@@ -243,6 +356,7 @@ export function pluginIncremental(internals: BuildInternals, root: URL): VitePlu
243356 return ;
244357 }
245358
359+ const transitiveGraph = createTransitiveGraphCache ( this ) ;
246360 const hashes = new Map < string , string > ( ) ;
247361 const serverIslandComponents = new Set < string > ( ) ;
248362 for ( const id of this . getModuleIds ( ) ) {
@@ -253,16 +367,22 @@ export function pluginIncremental(internals: BuildInternals, root: URL): VitePlu
253367 const pageData = getPageDataByViteID ( internals , info . id ) ;
254368 if ( ! pageData ) continue ;
255369
256- const deps = collectTransitiveDeps ( this , info . id ) ;
370+ const hash = transitiveGraph . hashes . get ( info . id ) ;
371+ if ( ! hash ) continue ;
372+
257373 // Key by component path (e.g. "src/pages/blog/[slug].astro")
258- hashes . set ( pageData . component , hashModules ( this , deps ) ) ;
259- if ( pageContainsServerIsland ( this , deps ) ) {
374+ hashes . set ( pageData . component , hash ) ;
375+ if ( transitiveGraph . serverIslandModules . has ( info . id ) ) {
260376 serverIslandComponents . add ( pageData . component ) ;
261377 }
262378 }
263379
264380 internals . pageDependencyHashes = hashes ;
265- internals . contentEntryRenderHashes = collectContentEntryHashes ( this , root ) ;
381+ internals . contentEntryRenderHashes = collectContentEntryHashes (
382+ this ,
383+ root ,
384+ transitiveGraph . hashes ,
385+ ) ;
266386 internals . serverIslandPageComponents = serverIslandComponents ;
267387 } ,
268388 } ;
0 commit comments