@@ -15,7 +15,6 @@ export interface ManifestInvalidation {
1515 configFingerprint : string ;
1616 serverEnv : string ;
1717 serverScope : string ;
18- baseDir : string ;
1918 typescriptEnabled : boolean ;
2019}
2120
@@ -25,19 +24,27 @@ export interface StartupManifest {
2524 invalidation : ManifestInvalidation ;
2625 /** Plugin-specific manifest data, keyed by plugin name */
2726 extensions : Record < string , unknown > ;
28- /** resolveModule cache: filepath -> resolved path | null */
27+ /** resolveModule cache: relative filepath -> resolved relative path | null */
2928 resolveCache : Record < string , string | null > ;
30- /** directory path -> file relative paths (filtered) */
29+ /** relative directory path -> file relative paths */
3130 fileDiscovery : Record < string , string [ ] > ;
3231}
3332
3433export class ManifestStore {
3534 readonly data : StartupManifest ;
35+ readonly baseDir : string ;
3636
37- private constructor ( data : StartupManifest ) {
37+ // Collectors for manifest generation (populated during loading)
38+ readonly #resolveCacheCollector: Record < string , string | null > = { } ;
39+ readonly #fileDiscoveryCollector: Record < string , string [ ] > = { } ;
40+
41+ private constructor ( data : StartupManifest , baseDir : string ) {
3842 this . data = data ;
43+ this . baseDir = baseDir ;
3944 }
4045
46+ // --- Factory Methods ---
47+
4148 /**
4249 * Load and validate manifest from `.egg/manifest.json`.
4350 * Returns null if manifest doesn't exist or is invalid.
@@ -70,7 +77,29 @@ export class ManifestStore {
7077 }
7178
7279 debug ( 'manifest loaded successfully' ) ;
73- return new ManifestStore ( data ) ;
80+ return new ManifestStore ( data , baseDir ) ;
81+ }
82+
83+ /**
84+ * Create a collector-only ManifestStore (no cached data).
85+ * Used during normal startup to collect data for future manifest generation.
86+ */
87+ static createCollector ( baseDir : string ) : ManifestStore {
88+ const emptyData : StartupManifest = {
89+ version : MANIFEST_VERSION ,
90+ generatedAt : '' ,
91+ invalidation : {
92+ lockfileFingerprint : '' ,
93+ configFingerprint : '' ,
94+ serverEnv : '' ,
95+ serverScope : '' ,
96+ typescriptEnabled : false ,
97+ } ,
98+ extensions : { } ,
99+ resolveCache : { } ,
100+ fileDiscovery : { } ,
101+ } ;
102+ return new ManifestStore ( emptyData , baseDir ) ;
74103 }
75104
76105 static #validate( data : StartupManifest , baseDir : string , serverEnv : string , serverScope : string ) : boolean {
@@ -85,8 +114,6 @@ export class ManifestStore {
85114 return false ;
86115 }
87116
88- // Note: baseDir is NOT validated — build env and runtime env may have different paths
89-
90117 if ( inv . serverEnv !== serverEnv ) {
91118 debug ( 'manifest serverEnv mismatch: expected %s, got %s' , serverEnv , inv . serverEnv ) ;
92119 return false ;
@@ -97,7 +124,6 @@ export class ManifestStore {
97124 return false ;
98125 }
99126
100- // Use stat-based fingerprint (mtime+size) for cheap validation
101127 const currentLockfileFingerprint = ManifestStore . #lockfileFingerprint( baseDir ) ;
102128 if ( inv . lockfileFingerprint !== currentLockfileFingerprint ) {
103129 debug ( 'manifest lockfileFingerprint mismatch' ) ;
@@ -113,43 +139,67 @@ export class ManifestStore {
113139 return true ;
114140 }
115141
116- // --- Query APIs ---
142+ // --- High-level APIs (cache + collect) ---
117143
118144 /**
119- * Look up a cached resolveModule result.
120- * @returns resolved path (cache hit), `null` (known missing), or `undefined` (not in cache)
145+ * Resolve a module path. Checks cache first, falls back to resolver, collects result.
121146 */
122- getResolveCache ( filepath : string ) : string | null | undefined {
147+ resolveModule ( filepath : string , fallback : ( ) => string | undefined ) : string | undefined {
148+ const relKey = this . #toRelative( filepath ) ;
123149 const cache = this . data . resolveCache ;
124- if ( ! cache || ! ( filepath in cache ) ) return undefined ;
125- return cache [ filepath ] ;
150+ if ( cache && relKey in cache ) {
151+ const cached = cache [ relKey ] ;
152+ debug ( '[resolveModule:manifest] %o => %o' , filepath , cached ) ;
153+ return cached !== null ? this . #toAbsolute( cached ) : undefined ;
154+ }
155+
156+ const result = fallback ( ) ;
157+ this . #resolveCacheCollector[ relKey ] = result !== undefined ? this . #toRelative( result ) : null ;
158+ return result ;
126159 }
127160
128- getFileDiscovery ( directory : string ) : string [ ] | undefined {
129- return this . data . fileDiscovery ?. [ directory ] ;
161+ /**
162+ * Get file list for a directory. Checks cache first, falls back to globber, collects result.
163+ */
164+ globFiles ( directory : string , fallback : ( ) => string [ ] ) : string [ ] {
165+ const relKey = this . #toRelative( directory ) ;
166+ const cached = this . data . fileDiscovery ?. [ relKey ] ;
167+ if ( cached ) {
168+ debug ( '[globFiles:manifest] using cached files for %o, count: %d' , directory , cached . length ) ;
169+ return cached ;
170+ }
171+
172+ const result = fallback ( ) ;
173+ this . #fileDiscoveryCollector[ relKey ] = result ;
174+ return result ;
130175 }
131176
177+ /**
178+ * Look up a plugin extension by name.
179+ */
132180 getExtension ( name : string ) : unknown {
133181 return this . data . extensions ?. [ name ] ;
134182 }
135183
136184 // --- Generation APIs ---
137185
138- static generate ( options : ManifestGenerateOptions ) : StartupManifest {
186+ /**
187+ * Generate a StartupManifest from collected data.
188+ */
189+ generateManifest ( options : ManifestGenerateOptions ) : StartupManifest {
139190 return {
140191 version : MANIFEST_VERSION ,
141192 generatedAt : new Date ( ) . toISOString ( ) ,
142193 invalidation : {
143- lockfileFingerprint : ManifestStore . #lockfileFingerprint( options . baseDir ) ,
144- configFingerprint : ManifestStore . #directoryFingerprint( path . join ( options . baseDir , 'config' ) ) ,
194+ lockfileFingerprint : ManifestStore . #lockfileFingerprint( this . baseDir ) ,
195+ configFingerprint : ManifestStore . #directoryFingerprint( path . join ( this . baseDir , 'config' ) ) ,
145196 serverEnv : options . serverEnv ,
146197 serverScope : options . serverScope ,
147- baseDir : options . baseDir ,
148198 typescriptEnabled : options . typescriptEnabled ,
149199 } ,
150200 extensions : options . extensions ?? { } ,
151- resolveCache : options . resolveCache ?? { } ,
152- fileDiscovery : options . fileDiscovery ?? { } ,
201+ resolveCache : this . #resolveCacheCollector ,
202+ fileDiscovery : this . #fileDiscoveryCollector ,
153203 } ;
154204 }
155205
@@ -171,9 +221,24 @@ export class ManifestStore {
171221 }
172222 }
173223
174- // --- Fingerprint Utilities (stat-based, no content reads) ---
224+ // --- Path Utilities ---
225+
226+ #toRelative( absPath : string ) : string {
227+ if ( path . isAbsolute ( absPath ) ) {
228+ return path . relative ( this . baseDir , absPath ) ;
229+ }
230+ return absPath ;
231+ }
232+
233+ #toAbsolute( relPath : string ) : string {
234+ if ( path . isAbsolute ( relPath ) ) {
235+ return relPath ;
236+ }
237+ return path . join ( this . baseDir , relPath ) ;
238+ }
239+
240+ // --- Fingerprint Utilities ---
175241
176- /** Fingerprint a file by mtime+size — avoids reading file content. */
177242 static #statFingerprint( filepath : string ) : string | null {
178243 try {
179244 const stat = fs . statSync ( filepath ) ;
@@ -183,7 +248,6 @@ export class ManifestStore {
183248 }
184249 }
185250
186- /** Find and fingerprint the project's lockfile. */
187251 static #lockfileFingerprint( baseDir : string ) : string {
188252 for ( const name of LOCKFILE_NAMES ) {
189253 const fp = ManifestStore . #statFingerprint( path . join ( baseDir , name ) ) ;
@@ -192,7 +256,6 @@ export class ManifestStore {
192256 return '' ;
193257 }
194258
195- /** Fingerprint a directory tree by file names, mtimes, and sizes. */
196259 static #directoryFingerprint( dirpath : string ) : string {
197260 const hash = createHash ( 'md5' ) ;
198261 const visited = new Set < string > ( ) ;
@@ -207,7 +270,6 @@ export class ManifestStore {
207270 } catch {
208271 return ;
209272 }
210- // Prevent symlink cycles
211273 if ( visited . has ( realPath ) ) return ;
212274 visited . add ( realPath ) ;
213275
@@ -225,7 +287,6 @@ export class ManifestStore {
225287 hash . update ( `dir:${ entry . name } \n` ) ;
226288 ManifestStore . #fingerprintRecursive( fullPath , hash , visited ) ;
227289 } else if ( entry . isFile ( ) ) {
228- // Use stat metadata instead of reading file contents
229290 const fp = ManifestStore . #statFingerprint( fullPath ) ;
230291 hash . update ( `file:${ entry . name } :${ fp ?? 'missing' } \n` ) ;
231292 }
@@ -234,11 +295,8 @@ export class ManifestStore {
234295}
235296
236297export interface ManifestGenerateOptions {
237- baseDir : string ;
238298 serverEnv : string ;
239299 serverScope : string ;
240300 typescriptEnabled : boolean ;
241301 extensions ?: Record < string , unknown > ;
242- resolveCache ?: Record < string , string | null > ;
243- fileDiscovery ?: Record < string , string [ ] > ;
244302}
0 commit comments