Skip to content

Commit da5df06

Browse files
killaguclaude
andcommitted
refactor(core): converge manifest logic into ManifestStore + relative paths
- Move cache-check and collector logic from EggLoader/FileLoader into ManifestStore.resolveModule() and ManifestStore.globFiles() methods - Remove resolveCacheCollector/fileDiscoveryCollector from EggLoader - Remove fileDiscoveryCollector from FileLoaderOptions - Remove invalidation.baseDir — build and runtime paths may differ - Store all paths as relative (to baseDir) in manifest JSON - ManifestStore is now baseDir-aware, converts abs↔rel internally - Add ManifestStore.createCollector() factory for collection-only mode - EggLoader.manifest is always set (loaded or collector) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0313705 commit da5df06

File tree

9 files changed

+348
-260
lines changed

9 files changed

+348
-260
lines changed

packages/core/src/loader/egg_loader.ts

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,8 @@ export class EggLoader {
7070
readonly appInfo: EggAppInfo;
7171
readonly outDir?: string;
7272
dirs?: EggDirInfo[];
73-
/** Pre-computed startup manifest for skipping file I/O */
74-
readonly manifest: ManifestStore | null;
75-
/** Collected resolveModule results for manifest generation */
76-
readonly resolveCacheCollector: Record<string, string | null> = {};
77-
/** Collected file discovery results for manifest generation */
78-
readonly fileDiscoveryCollector: Record<string, string[]> = {};
73+
/** Startup manifest — loaded from cache or collecting for generation */
74+
readonly manifest: ManifestStore;
7975

8076
/**
8177
* @class
@@ -164,11 +160,10 @@ export class EggLoader {
164160
*/
165161
this.appInfo = this.getAppInfo();
166162

167-
// Load pre-computed startup manifest if available
168-
this.manifest = ManifestStore.load(this.options.baseDir, this.serverEnv, this.serverScope);
169-
if (this.manifest) {
170-
debug('startup manifest loaded, will skip redundant file I/O');
171-
}
163+
// Load pre-computed manifest or create a collector for future generation
164+
this.manifest =
165+
ManifestStore.load(this.options.baseDir, this.serverEnv, this.serverScope) ??
166+
ManifestStore.createCollector(this.options.baseDir);
172167
}
173168

174169
get app(): EggCore {
@@ -1651,7 +1646,6 @@ export class EggLoader {
16511646
target,
16521647
inject: this.app,
16531648
manifest: this.manifest,
1654-
fileDiscoveryCollector: this.fileDiscoveryCollector,
16551649
};
16561650

16571651
const timingKey = `Load "${String(property)}" to Application`;
@@ -1678,7 +1672,6 @@ export class EggLoader {
16781672
property,
16791673
inject: this.app,
16801674
manifest: this.manifest,
1681-
fileDiscoveryCollector: this.fileDiscoveryCollector,
16821675
};
16831676

16841677
const timingKey = `Load "${String(property)}" to Context`;
@@ -1715,28 +1708,19 @@ export class EggLoader {
17151708
}
17161709

17171710
resolveModule(filepath: string): string | undefined {
1718-
// Check manifest cache first
1719-
if (this.manifest) {
1720-
const cached = this.manifest.getResolveCache(filepath);
1721-
if (cached !== undefined) {
1722-
debug('[resolveModule:manifest] %o => %o', filepath, cached);
1723-
return cached ?? undefined;
1724-
}
1725-
}
1711+
return this.manifest.resolveModule(filepath, () => this.#doResolveModule(filepath));
1712+
}
17261713

1714+
#doResolveModule(filepath: string): string | undefined {
17271715
let fullPath: string | undefined;
17281716
try {
17291717
fullPath = utils.resolvePath(filepath);
17301718
} catch {
1731-
// debug('[resolveModule] Module %o resolve error: %s', filepath, err.stack);
1719+
// ignore resolve errors
17321720
}
17331721
if (!fullPath) {
17341722
fullPath = this.#resolveFromOutDir(filepath);
17351723
}
1736-
1737-
// Collect for manifest generation
1738-
this.resolveCacheCollector[filepath] = fullPath ?? null;
1739-
17401724
return fullPath;
17411725
}
17421726

@@ -1780,14 +1764,11 @@ export class EggLoader {
17801764
* Should be called after all loading phases complete.
17811765
*/
17821766
generateManifest(extensions?: Record<string, unknown>): StartupManifest {
1783-
return ManifestStore.generate({
1784-
baseDir: this.options.baseDir,
1767+
return this.manifest.generateManifest({
17851768
serverEnv: this.serverEnv,
17861769
serverScope: this.serverScope,
17871770
typescriptEnabled: isSupportTypeScript(),
17881771
extensions,
1789-
resolveCache: this.resolveCacheCollector,
1790-
fileDiscovery: this.fileDiscoveryCollector,
17911772
});
17921773
}
17931774
}

packages/core/src/loader/file_loader.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,8 @@ export interface FileLoaderOptions {
5050
/** set property's case when converting a filepath to property list. */
5151
caseStyle?: CaseStyle | CaseStyleFunction;
5252
lowercaseFirst?: boolean;
53-
/** Pre-computed startup manifest for skipping globby scans */
54-
manifest?: ManifestStore | null;
55-
/** Collector for file discovery results during manifest generation */
56-
fileDiscoveryCollector?: Record<string, string[]>;
53+
/** Startup manifest for caching globby scans and collecting results */
54+
manifest?: ManifestStore;
5755
}
5856

5957
export interface FileLoaderParseItem {
@@ -198,17 +196,11 @@ export class FileLoader {
198196
const items: FileLoaderParseItem[] = [];
199197
debug('[parse] parsing directories: %j', directories);
200198
for (const directory of directories) {
201-
const cachedFiles = this.options.manifest?.getFileDiscovery(directory);
202-
const filepaths = cachedFiles ?? globby.sync(files, { cwd: directory });
203-
if (cachedFiles) {
204-
debug('[parse:manifest] using cached files for %o, count: %d', directory, cachedFiles.length);
205-
} else {
206-
debug('[parse] globby files: %o, cwd: %o => %o', files, directory, filepaths);
207-
// Collect for manifest generation
208-
if (this.options.fileDiscoveryCollector) {
209-
this.options.fileDiscoveryCollector[directory] = filepaths;
210-
}
211-
}
199+
const manifest = this.options.manifest;
200+
const filepaths = manifest
201+
? manifest.globFiles(directory, () => globby.sync(files, { cwd: directory }))
202+
: globby.sync(files, { cwd: directory });
203+
debug('[parse] files: %o, cwd: %o => %o', files, directory, filepaths);
212204
for (const filepath of filepaths) {
213205
const fullpath = path.join(directory, filepath);
214206
if (!fs.statSync(fullpath).isFile()) continue;

packages/core/src/loader/manifest.ts

Lines changed: 89 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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

3433
export 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

236297
export 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

Comments
 (0)