@eggjs/core 7.0.2-beta.2 → 7.0.2-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/egg.d.ts +25 -0
- package/dist/egg.js +21 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.js +6 -1
- package/dist/lifecycle.d.ts +62 -1
- package/dist/lifecycle.js +131 -8
- package/dist/loader/egg_loader.d.ts +15 -0
- package/dist/loader/egg_loader.js +195 -17
- package/dist/loader/file_loader.d.ts +10 -3
- package/dist/loader/file_loader.js +18 -10
- package/dist/loader/loader_fs.d.ts +17 -0
- package/dist/loader/loader_fs.js +271 -0
- package/dist/loader/manifest.d.ts +98 -0
- package/dist/loader/manifest.js +341 -0
- package/package.json +23 -16
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { RealLoaderFS as RealLoaderFS$1 } from "@eggjs/loader-fs";
|
|
4
|
+
import multimatch from "multimatch";
|
|
5
|
+
|
|
6
|
+
//#region src/loader/loader_fs.ts
|
|
7
|
+
var ManifestLoaderFS = class {
|
|
8
|
+
#manifest;
|
|
9
|
+
#fallback;
|
|
10
|
+
#manifestFiles;
|
|
11
|
+
#manifestDirectories;
|
|
12
|
+
#resolveCacheTargets;
|
|
13
|
+
constructor(manifest, fallback = new RealLoaderFS$1()) {
|
|
14
|
+
this.#manifest = manifest;
|
|
15
|
+
this.#fallback = fallback;
|
|
16
|
+
this.#resolveCacheTargets = new Set(Object.values(manifest.data.resolveCache).filter((target) => typeof target === "string"));
|
|
17
|
+
const manifestFiles = /* @__PURE__ */ new Set();
|
|
18
|
+
const manifestDirectories = /* @__PURE__ */ new Set();
|
|
19
|
+
for (const [dir, files] of Object.entries(manifest.data.fileDiscovery)) {
|
|
20
|
+
addManifestDirectory(manifestDirectories, dir);
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
const fullRel = path.posix.join(dir, file);
|
|
23
|
+
manifestFiles.add(fullRel);
|
|
24
|
+
addManifestDirectory(manifestDirectories, path.posix.dirname(fullRel));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const target of this.#resolveCacheTargets) {
|
|
28
|
+
manifestFiles.add(target);
|
|
29
|
+
addManifestDirectory(manifestDirectories, path.posix.dirname(target));
|
|
30
|
+
}
|
|
31
|
+
this.#manifestFiles = manifestFiles;
|
|
32
|
+
this.#manifestDirectories = manifestDirectories;
|
|
33
|
+
}
|
|
34
|
+
exists(filepath) {
|
|
35
|
+
const entry = this.#getManifestEntry(filepath);
|
|
36
|
+
if (entry) return entry.type !== "missing";
|
|
37
|
+
return this.#fallback.exists(filepath);
|
|
38
|
+
}
|
|
39
|
+
stat(filepath) {
|
|
40
|
+
const entry = this.#getManifestEntry(filepath);
|
|
41
|
+
if (entry?.type === "file" || entry?.type === "directory") return createVirtualStats(entry.type);
|
|
42
|
+
if (entry?.type === "missing") throw createNotFoundError("stat", filepath);
|
|
43
|
+
return this.#fallback.stat(filepath);
|
|
44
|
+
}
|
|
45
|
+
realpath(filepath) {
|
|
46
|
+
const entry = this.#getManifestEntry(filepath);
|
|
47
|
+
if (entry?.type === "file" || entry?.type === "directory") return this.#toAbsolute(entry.rel);
|
|
48
|
+
if (entry?.type === "missing") throw createNotFoundError("realpath", filepath);
|
|
49
|
+
return this.#fallback.realpath(filepath);
|
|
50
|
+
}
|
|
51
|
+
readJSON(filepath) {
|
|
52
|
+
const entry = this.#getManifestEntry(filepath);
|
|
53
|
+
if (entry?.type === "file") {
|
|
54
|
+
const bundled = this.#loadBundledModule(entry.rel);
|
|
55
|
+
if (bundled !== void 0) return unwrapDefaultExport(bundled);
|
|
56
|
+
}
|
|
57
|
+
if (entry?.type === "missing") throw createNotFoundError("open", filepath);
|
|
58
|
+
return this.#fallback.readJSON(filepath);
|
|
59
|
+
}
|
|
60
|
+
glob(patterns, options) {
|
|
61
|
+
const cwd = options?.cwd === void 0 ? process.cwd() : String(options.cwd);
|
|
62
|
+
const absoluteCwd = path.resolve(cwd);
|
|
63
|
+
const cwdRel = this.#toRelative(absoluteCwd);
|
|
64
|
+
const manifestFiles = this.#listManifestFilesUnder(cwdRel);
|
|
65
|
+
if (manifestFiles === void 0) return this.#fallback.glob(patterns, options);
|
|
66
|
+
const matched = filterManifestGlob(manifestFiles, patterns, options);
|
|
67
|
+
if (options?.absolute) return matched.map((file) => path.join(absoluteCwd, file));
|
|
68
|
+
return matched;
|
|
69
|
+
}
|
|
70
|
+
async loadFile(filepath) {
|
|
71
|
+
const entry = this.#getManifestEntry(filepath);
|
|
72
|
+
if (entry?.type === "file") {
|
|
73
|
+
const bundled = this.#loadBundledModule(entry.rel);
|
|
74
|
+
if (bundled !== void 0) return unwrapDefaultExport(bundled);
|
|
75
|
+
}
|
|
76
|
+
if (entry?.type === "missing") throw createNotFoundError("open", filepath);
|
|
77
|
+
return this.#fallback.loadFile(filepath);
|
|
78
|
+
}
|
|
79
|
+
#getManifestEntry(filepath) {
|
|
80
|
+
const rel = this.#toRelative(filepath);
|
|
81
|
+
const resolved = this.#resolveManifestFile(rel);
|
|
82
|
+
if (resolved?.type === "file") return {
|
|
83
|
+
type: "file",
|
|
84
|
+
rel: resolved.rel
|
|
85
|
+
};
|
|
86
|
+
if (resolved?.type === "missing") return {
|
|
87
|
+
type: "missing",
|
|
88
|
+
rel
|
|
89
|
+
};
|
|
90
|
+
if (this.#isManifestDirectory(rel)) return {
|
|
91
|
+
type: "directory",
|
|
92
|
+
rel
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
#resolveManifestFile(rel) {
|
|
96
|
+
const cache = this.#manifest.data.resolveCache;
|
|
97
|
+
if (Object.hasOwn(cache, rel)) {
|
|
98
|
+
const cached = cache[rel];
|
|
99
|
+
return cached === null ? { type: "missing" } : {
|
|
100
|
+
type: "file",
|
|
101
|
+
rel: cached
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (this.#isManifestFile(rel)) return {
|
|
105
|
+
type: "file",
|
|
106
|
+
rel
|
|
107
|
+
};
|
|
108
|
+
const discovered = this.#resolveFromFileDiscovery(rel);
|
|
109
|
+
if (discovered) return {
|
|
110
|
+
type: "file",
|
|
111
|
+
rel: discovered
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
#resolveFromFileDiscovery(rel) {
|
|
115
|
+
const matchedDir = this.#nearestManifestDiscoveryDir(rel);
|
|
116
|
+
if (matchedDir === void 0 || rel === matchedDir) return;
|
|
117
|
+
const request = matchedDir === "" ? rel : rel.slice(matchedDir.length + 1);
|
|
118
|
+
for (const file of this.#manifest.data.fileDiscovery[matchedDir]) {
|
|
119
|
+
if (file === request) return path.posix.join(matchedDir, file);
|
|
120
|
+
const ext = path.posix.extname(file);
|
|
121
|
+
if (!ext || ext === ".map") continue;
|
|
122
|
+
const extensionlessFile = file.slice(0, -ext.length);
|
|
123
|
+
if (extensionlessFile === request || extensionlessFile === `${request}/index`) return path.posix.join(matchedDir, file);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
#nearestManifestDiscoveryDir(rel) {
|
|
127
|
+
let current = path.posix.dirname(rel);
|
|
128
|
+
while (true) {
|
|
129
|
+
const dir = current === "." ? "" : current;
|
|
130
|
+
if (Object.hasOwn(this.#manifest.data.fileDiscovery, dir)) return dir;
|
|
131
|
+
if (dir === "") return;
|
|
132
|
+
current = path.posix.dirname(dir);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
#isManifestFile(rel) {
|
|
136
|
+
return this.#manifestFiles.has(rel);
|
|
137
|
+
}
|
|
138
|
+
#isManifestDirectory(rel) {
|
|
139
|
+
return this.#manifestDirectories.has(rel);
|
|
140
|
+
}
|
|
141
|
+
#listManifestFilesUnder(cwdRel) {
|
|
142
|
+
if (!this.#isManifestDirectory(cwdRel)) return;
|
|
143
|
+
const files = /* @__PURE__ */ new Set();
|
|
144
|
+
for (const [dir, entries] of Object.entries(this.#manifest.data.fileDiscovery)) {
|
|
145
|
+
const prefix = dir === cwdRel ? "" : relativePrefix(cwdRel, dir);
|
|
146
|
+
if (prefix === void 0) continue;
|
|
147
|
+
for (const entry of entries) files.add(path.posix.join(prefix, entry));
|
|
148
|
+
}
|
|
149
|
+
for (const target of Object.values(this.#manifest.data.resolveCache)) {
|
|
150
|
+
if (!target) continue;
|
|
151
|
+
const prefix = relativePrefix(cwdRel, path.posix.dirname(target));
|
|
152
|
+
if (prefix !== void 0) files.add(path.posix.join(prefix, path.posix.basename(target)));
|
|
153
|
+
}
|
|
154
|
+
return [...files].sort();
|
|
155
|
+
}
|
|
156
|
+
#loadBundledModule(rel) {
|
|
157
|
+
const loader = globalThis.__EGG_BUNDLE_MODULE_LOADER__;
|
|
158
|
+
if (!loader) return void 0;
|
|
159
|
+
for (const key of this.#bundleKeys(rel)) {
|
|
160
|
+
const loaded = loader(key);
|
|
161
|
+
if (loaded !== void 0) return loaded;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
#bundleKeys(rel) {
|
|
165
|
+
const abs = this.#toAbsolute(rel);
|
|
166
|
+
return [...new Set([rel, normalizePath(abs)])];
|
|
167
|
+
}
|
|
168
|
+
#toRelative(filepath) {
|
|
169
|
+
return normalizePath(path.relative(this.#manifest.baseDir, path.resolve(filepath)));
|
|
170
|
+
}
|
|
171
|
+
#toAbsolute(rel) {
|
|
172
|
+
return path.isAbsolute(rel) ? rel : path.join(this.#manifest.baseDir, rel);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
function normalizePath(filepath) {
|
|
176
|
+
return filepath.replaceAll(path.sep, "/");
|
|
177
|
+
}
|
|
178
|
+
function addManifestDirectory(directories, dir) {
|
|
179
|
+
let current = dir === "." ? "" : dir;
|
|
180
|
+
while (true) {
|
|
181
|
+
directories.add(current);
|
|
182
|
+
if (current === "") return;
|
|
183
|
+
const parent = path.posix.dirname(current);
|
|
184
|
+
current = parent === "." ? "" : parent;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function relativePrefix(cwdRel, dirRel) {
|
|
188
|
+
if (cwdRel === "") return dirRel;
|
|
189
|
+
if (dirRel === cwdRel) return "";
|
|
190
|
+
if (dirRel.startsWith(cwdRel + "/")) return dirRel.slice(cwdRel.length + 1);
|
|
191
|
+
}
|
|
192
|
+
function filterManifestGlob(files, patterns, options) {
|
|
193
|
+
const patternList = Array.isArray(patterns) ? patterns : [patterns];
|
|
194
|
+
if (!patternList.some((pattern) => !pattern.startsWith("!"))) return [];
|
|
195
|
+
const ignoreList = Array.isArray(options?.ignore) ? options.ignore.map(String) : options?.ignore ? [String(options.ignore)] : [];
|
|
196
|
+
return multimatch(files, patternList.map(normalizeAlternationGroups).concat(ignoreList.map((pattern) => `!${normalizeAlternationGroups(pattern)}`)), toMultimatchOptions(options));
|
|
197
|
+
}
|
|
198
|
+
function toMultimatchOptions(options) {
|
|
199
|
+
return {
|
|
200
|
+
...options?.dot !== void 0 ? { dot: options.dot } : {},
|
|
201
|
+
...options?.caseSensitiveMatch !== void 0 ? { nocase: !options.caseSensitiveMatch } : {},
|
|
202
|
+
...options?.braceExpansion !== void 0 ? { nobrace: !options.braceExpansion } : {},
|
|
203
|
+
...options?.extglob !== void 0 ? { noext: !options.extglob } : {},
|
|
204
|
+
...options?.globstar !== void 0 ? { noglobstar: !options.globstar } : {},
|
|
205
|
+
...options?.baseNameMatch !== void 0 ? { matchBase: options.baseNameMatch } : {}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function normalizeAlternationGroups(pattern) {
|
|
209
|
+
let normalized = "";
|
|
210
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
211
|
+
const char = pattern[index];
|
|
212
|
+
if (char !== "(" || isExtglobPrefix(pattern[index - 1])) {
|
|
213
|
+
normalized += char;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const end = pattern.indexOf(")", index + 1);
|
|
217
|
+
if (end === -1) {
|
|
218
|
+
normalized += char;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const group = pattern.slice(index + 1, end);
|
|
222
|
+
if (group.includes("|")) {
|
|
223
|
+
normalized += `{${group.replaceAll("|", ",")}}`;
|
|
224
|
+
index = end;
|
|
225
|
+
} else normalized += char;
|
|
226
|
+
}
|
|
227
|
+
return normalized;
|
|
228
|
+
}
|
|
229
|
+
function isExtglobPrefix(char) {
|
|
230
|
+
return char === "@" || char === "!" || char === "?" || char === "+" || char === "*";
|
|
231
|
+
}
|
|
232
|
+
function unwrapDefaultExport(value) {
|
|
233
|
+
let unwrapped = value;
|
|
234
|
+
if (isRecord(unwrapped) && isRecord(unwrapped.default) && unwrapped.default.__esModule === true) unwrapped = unwrapped.default;
|
|
235
|
+
if (isRecord(unwrapped) && "default" in unwrapped) return unwrapped.default;
|
|
236
|
+
return unwrapped;
|
|
237
|
+
}
|
|
238
|
+
function isRecord(value) {
|
|
239
|
+
return value !== null && typeof value === "object";
|
|
240
|
+
}
|
|
241
|
+
function createVirtualStats(type) {
|
|
242
|
+
const stat = Object.create(fs.Stats.prototype);
|
|
243
|
+
const isFile = type === "file";
|
|
244
|
+
const timestamp = /* @__PURE__ */ new Date(0);
|
|
245
|
+
Object.defineProperties(stat, {
|
|
246
|
+
size: { value: 0 },
|
|
247
|
+
atimeMs: { value: 0 },
|
|
248
|
+
mtimeMs: { value: 0 },
|
|
249
|
+
ctimeMs: { value: 0 },
|
|
250
|
+
birthtimeMs: { value: 0 },
|
|
251
|
+
atime: { value: timestamp },
|
|
252
|
+
mtime: { value: timestamp },
|
|
253
|
+
ctime: { value: timestamp },
|
|
254
|
+
birthtime: { value: timestamp },
|
|
255
|
+
isFile: { value: () => isFile },
|
|
256
|
+
isDirectory: { value: () => !isFile },
|
|
257
|
+
isSymbolicLink: { value: () => false }
|
|
258
|
+
});
|
|
259
|
+
return stat;
|
|
260
|
+
}
|
|
261
|
+
function createNotFoundError(syscall, filepath) {
|
|
262
|
+
const err = /* @__PURE__ */ new Error(`ENOENT: no such file or directory, ${syscall} '${filepath}'`);
|
|
263
|
+
err.code = "ENOENT";
|
|
264
|
+
err.errno = -2;
|
|
265
|
+
err.syscall = syscall;
|
|
266
|
+
err.path = filepath;
|
|
267
|
+
return err;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
//#endregion
|
|
271
|
+
export { ManifestLoaderFS };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//#region src/loader/manifest.d.ts
|
|
2
|
+
interface ManifestInvalidation {
|
|
3
|
+
lockfileFingerprint: string;
|
|
4
|
+
configFingerprint: string;
|
|
5
|
+
serverEnv: string;
|
|
6
|
+
serverScope: string;
|
|
7
|
+
typescriptEnabled: boolean;
|
|
8
|
+
}
|
|
9
|
+
interface StartupManifest {
|
|
10
|
+
version: number;
|
|
11
|
+
generatedAt: string;
|
|
12
|
+
invalidation: ManifestInvalidation;
|
|
13
|
+
/** Plugin-specific manifest data, keyed by plugin name */
|
|
14
|
+
extensions: Record<string, unknown>;
|
|
15
|
+
/** resolveModule cache: relative filepath -> resolved relative path | null */
|
|
16
|
+
resolveCache: Record<string, string | null>;
|
|
17
|
+
/** relative directory path -> file relative paths */
|
|
18
|
+
fileDiscovery: Record<string, string[]>;
|
|
19
|
+
}
|
|
20
|
+
declare class ManifestStore {
|
|
21
|
+
#private;
|
|
22
|
+
readonly data: StartupManifest;
|
|
23
|
+
readonly baseDir: string;
|
|
24
|
+
private constructor();
|
|
25
|
+
/**
|
|
26
|
+
* Register a pre-built manifest store for bundled egg apps. When set,
|
|
27
|
+
* `ManifestStore.load()` returns this store for matching baseDir requests,
|
|
28
|
+
* bypassing disk reads and invalidation checks. The bundler-generated entry
|
|
29
|
+
* calls this at startup before creating the Application.
|
|
30
|
+
*
|
|
31
|
+
* Uses globalThis so that bundled and external copies of @eggjs/core
|
|
32
|
+
* share the same store instance.
|
|
33
|
+
*/
|
|
34
|
+
static setBundleStore(store: ManifestStore | undefined): void;
|
|
35
|
+
/**
|
|
36
|
+
* Return the registered bundle store, if any.
|
|
37
|
+
*/
|
|
38
|
+
static getBundleStore(): ManifestStore | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Load and validate manifest from `.egg/manifest.json`.
|
|
41
|
+
* Returns null if manifest doesn't exist or is invalid.
|
|
42
|
+
*/
|
|
43
|
+
static load(baseDir: string, serverEnv: string, serverScope: string): ManifestStore | null;
|
|
44
|
+
/**
|
|
45
|
+
* Create a ManifestStore from pre-validated bundled data.
|
|
46
|
+
* Skips invalidation checks — the caller (bundler) is responsible for
|
|
47
|
+
* guaranteeing the data matches the shipped artifact.
|
|
48
|
+
*/
|
|
49
|
+
static fromBundle(data: StartupManifest, baseDir: string): ManifestStore;
|
|
50
|
+
/**
|
|
51
|
+
* Create a collector-only ManifestStore (no cached data).
|
|
52
|
+
* Used during normal startup to collect data for future manifest generation.
|
|
53
|
+
*/
|
|
54
|
+
static createCollector(baseDir: string): ManifestStore;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a module path. Checks cache first, falls back to resolver, collects result.
|
|
57
|
+
*/
|
|
58
|
+
resolveModule(filepath: string, fallback: () => string | undefined): string | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Get file list for a directory. Checks cache first, falls back to globber, collects result.
|
|
61
|
+
*/
|
|
62
|
+
globFiles(directory: string, fallback: () => string[]): string[];
|
|
63
|
+
/**
|
|
64
|
+
* Look up a plugin extension by name.
|
|
65
|
+
*/
|
|
66
|
+
getExtension(name: string): unknown;
|
|
67
|
+
/**
|
|
68
|
+
* Register plugin extension data for manifest generation.
|
|
69
|
+
*/
|
|
70
|
+
setExtension(name: string, data: unknown): void;
|
|
71
|
+
/**
|
|
72
|
+
* Generate a StartupManifest from collected data.
|
|
73
|
+
*/
|
|
74
|
+
generateManifest(options: ManifestGenerateOptions): StartupManifest;
|
|
75
|
+
static write(baseDir: string, manifest: StartupManifest): Promise<void>;
|
|
76
|
+
static clean(baseDir: string): void;
|
|
77
|
+
/**
|
|
78
|
+
* Enable Node.js module compile cache for the current process.
|
|
79
|
+
* Sets NODE_COMPILE_CACHE and NODE_COMPILE_CACHE_PORTABLE env vars
|
|
80
|
+
* so forked child processes also inherit compile cache.
|
|
81
|
+
*/
|
|
82
|
+
static enableCompileCache(baseDir: string): void;
|
|
83
|
+
/**
|
|
84
|
+
* Flush accumulated compile cache entries to disk.
|
|
85
|
+
*/
|
|
86
|
+
static flushCompileCache(): void;
|
|
87
|
+
/**
|
|
88
|
+
* Remove the compile cache directory.
|
|
89
|
+
*/
|
|
90
|
+
static cleanCompileCache(baseDir: string): void;
|
|
91
|
+
}
|
|
92
|
+
interface ManifestGenerateOptions {
|
|
93
|
+
serverEnv: string;
|
|
94
|
+
serverScope: string;
|
|
95
|
+
typescriptEnabled: boolean;
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
export { ManifestGenerateOptions, ManifestInvalidation, ManifestStore, StartupManifest };
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import BuiltinModule from "node:module";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { debuglog } from "node:util";
|
|
6
|
+
import { isSupportTypeScript } from "@eggjs/utils";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
//#region src/loader/manifest.ts
|
|
10
|
+
const debug = debuglog("egg/core/loader/manifest");
|
|
11
|
+
const MANIFEST_VERSION = 1;
|
|
12
|
+
const LOCKFILE_NAMES = [
|
|
13
|
+
"pnpm-lock.yaml",
|
|
14
|
+
"package-lock.json",
|
|
15
|
+
"yarn.lock"
|
|
16
|
+
];
|
|
17
|
+
const BUNDLE_STORE_KEY = "__EGG_BUNDLE_STORE__";
|
|
18
|
+
var ManifestStore = class ManifestStore {
|
|
19
|
+
data;
|
|
20
|
+
baseDir;
|
|
21
|
+
#resolveCacheCollector = {};
|
|
22
|
+
#fileDiscoveryCollector = {};
|
|
23
|
+
#extensionCollector = {};
|
|
24
|
+
constructor(data, baseDir) {
|
|
25
|
+
this.data = data;
|
|
26
|
+
this.baseDir = baseDir;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Register a pre-built manifest store for bundled egg apps. When set,
|
|
30
|
+
* `ManifestStore.load()` returns this store for matching baseDir requests,
|
|
31
|
+
* bypassing disk reads and invalidation checks. The bundler-generated entry
|
|
32
|
+
* calls this at startup before creating the Application.
|
|
33
|
+
*
|
|
34
|
+
* Uses globalThis so that bundled and external copies of @eggjs/core
|
|
35
|
+
* share the same store instance.
|
|
36
|
+
*/
|
|
37
|
+
static setBundleStore(store) {
|
|
38
|
+
globalThis[BUNDLE_STORE_KEY] = store;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Return the registered bundle store, if any.
|
|
42
|
+
*/
|
|
43
|
+
static getBundleStore() {
|
|
44
|
+
return globalThis[BUNDLE_STORE_KEY];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Load and validate manifest from `.egg/manifest.json`.
|
|
48
|
+
* Returns null if manifest doesn't exist or is invalid.
|
|
49
|
+
*/
|
|
50
|
+
static load(baseDir, serverEnv, serverScope) {
|
|
51
|
+
const bundleStore = ManifestStore.getBundleStore();
|
|
52
|
+
if (bundleStore && bundleStore.baseDir === baseDir) {
|
|
53
|
+
debug("load: returning registered bundle store for %s", baseDir);
|
|
54
|
+
return bundleStore;
|
|
55
|
+
}
|
|
56
|
+
if (serverEnv === "local" && process.env.EGG_MANIFEST !== "true") {
|
|
57
|
+
debug("skip manifest in local env (set EGG_MANIFEST=true to enable)");
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const manifestPath = path.join(baseDir, ".egg", "manifest.json");
|
|
61
|
+
let raw;
|
|
62
|
+
try {
|
|
63
|
+
raw = fs.readFileSync(manifestPath, "utf-8");
|
|
64
|
+
} catch {
|
|
65
|
+
debug("manifest not found at %s", manifestPath);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
let data;
|
|
69
|
+
try {
|
|
70
|
+
data = JSON.parse(raw);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
debug("failed to parse manifest: %s", e);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
if (!ManifestStore.#validate(data, baseDir, serverEnv, serverScope)) return null;
|
|
76
|
+
debug("manifest loaded successfully");
|
|
77
|
+
return new ManifestStore(data, baseDir);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a ManifestStore from pre-validated bundled data.
|
|
81
|
+
* Skips invalidation checks — the caller (bundler) is responsible for
|
|
82
|
+
* guaranteeing the data matches the shipped artifact.
|
|
83
|
+
*/
|
|
84
|
+
static fromBundle(data, baseDir) {
|
|
85
|
+
if (!data || data.version !== MANIFEST_VERSION) throw new Error(`[@eggjs/core] bundled manifest version mismatch: expected ${MANIFEST_VERSION}, got ${data?.version}`);
|
|
86
|
+
if (!data.invalidation) throw new Error("[@eggjs/core] bundled manifest missing invalidation data");
|
|
87
|
+
debug("manifest loaded from bundle");
|
|
88
|
+
return new ManifestStore(data, baseDir);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Create a collector-only ManifestStore (no cached data).
|
|
92
|
+
* Used during normal startup to collect data for future manifest generation.
|
|
93
|
+
*/
|
|
94
|
+
static createCollector(baseDir) {
|
|
95
|
+
return new ManifestStore({
|
|
96
|
+
version: MANIFEST_VERSION,
|
|
97
|
+
generatedAt: "",
|
|
98
|
+
invalidation: {
|
|
99
|
+
lockfileFingerprint: "",
|
|
100
|
+
configFingerprint: "",
|
|
101
|
+
serverEnv: "",
|
|
102
|
+
serverScope: "",
|
|
103
|
+
typescriptEnabled: false
|
|
104
|
+
},
|
|
105
|
+
extensions: {},
|
|
106
|
+
resolveCache: {},
|
|
107
|
+
fileDiscovery: {}
|
|
108
|
+
}, baseDir);
|
|
109
|
+
}
|
|
110
|
+
static #validate(data, baseDir, serverEnv, serverScope) {
|
|
111
|
+
if (data.version !== MANIFEST_VERSION) {
|
|
112
|
+
debug("manifest version mismatch: expected %d, got %d", MANIFEST_VERSION, data.version);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
const inv = data.invalidation;
|
|
116
|
+
if (!inv) {
|
|
117
|
+
debug("manifest missing invalidation data");
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (inv.serverEnv !== serverEnv) {
|
|
121
|
+
debug("manifest serverEnv mismatch: expected %s, got %s", serverEnv, inv.serverEnv);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (inv.serverScope !== serverScope) {
|
|
125
|
+
debug("manifest serverScope mismatch: expected %s, got %s", serverScope, inv.serverScope);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
const currentTypescriptEnabled = isSupportTypeScript();
|
|
129
|
+
if (inv.typescriptEnabled !== currentTypescriptEnabled) {
|
|
130
|
+
debug("manifest typescriptEnabled mismatch: expected %s, got %s", currentTypescriptEnabled, inv.typescriptEnabled);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const currentLockfileFingerprint = ManifestStore.#lockfileFingerprint(baseDir);
|
|
134
|
+
if (inv.lockfileFingerprint !== currentLockfileFingerprint) {
|
|
135
|
+
debug("manifest lockfileFingerprint mismatch");
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const currentConfigFingerprint = ManifestStore.#directoryFingerprint(path.join(baseDir, "config"));
|
|
139
|
+
if (inv.configFingerprint !== currentConfigFingerprint) {
|
|
140
|
+
debug("manifest configFingerprint mismatch");
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolve a module path. Checks cache first, falls back to resolver, collects result.
|
|
147
|
+
*/
|
|
148
|
+
resolveModule(filepath, fallback) {
|
|
149
|
+
const relKey = this.#toRelative(filepath);
|
|
150
|
+
const cache = this.data.resolveCache;
|
|
151
|
+
if (cache && relKey in cache) {
|
|
152
|
+
const cached = cache[relKey];
|
|
153
|
+
debug("[resolveModule:manifest] %o => %o", filepath, cached);
|
|
154
|
+
return cached !== null ? this.#toAbsolute(cached) : void 0;
|
|
155
|
+
}
|
|
156
|
+
const discovered = this.#resolveFromFileDiscovery(relKey);
|
|
157
|
+
if (discovered) {
|
|
158
|
+
debug("[resolveModule:fileDiscovery] %o => %o", filepath, discovered);
|
|
159
|
+
return discovered;
|
|
160
|
+
}
|
|
161
|
+
const result = fallback();
|
|
162
|
+
this.#resolveCacheCollector[relKey] = result !== void 0 ? this.#toRelative(result) : null;
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get file list for a directory. Checks cache first, falls back to globber, collects result.
|
|
167
|
+
*/
|
|
168
|
+
globFiles(directory, fallback) {
|
|
169
|
+
const relKey = this.#toRelative(directory);
|
|
170
|
+
const cache = this.data.fileDiscovery;
|
|
171
|
+
if (cache && relKey in cache) {
|
|
172
|
+
const cached = cache[relKey];
|
|
173
|
+
debug("[globFiles:manifest] using cached files for %o, count: %d", directory, cached.length);
|
|
174
|
+
return cached;
|
|
175
|
+
}
|
|
176
|
+
const result = fallback();
|
|
177
|
+
this.#fileDiscoveryCollector[relKey] = result;
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Look up a plugin extension by name.
|
|
182
|
+
*/
|
|
183
|
+
getExtension(name) {
|
|
184
|
+
if (Object.hasOwn(this.#extensionCollector, name)) return this.#extensionCollector[name];
|
|
185
|
+
return this.data.extensions?.[name];
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Register plugin extension data for manifest generation.
|
|
189
|
+
*/
|
|
190
|
+
setExtension(name, data) {
|
|
191
|
+
this.#extensionCollector[name] = data;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Generate a StartupManifest from collected data.
|
|
195
|
+
*/
|
|
196
|
+
generateManifest(options) {
|
|
197
|
+
return {
|
|
198
|
+
version: MANIFEST_VERSION,
|
|
199
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
200
|
+
invalidation: {
|
|
201
|
+
lockfileFingerprint: ManifestStore.#lockfileFingerprint(this.baseDir),
|
|
202
|
+
configFingerprint: ManifestStore.#directoryFingerprint(path.join(this.baseDir, "config")),
|
|
203
|
+
serverEnv: options.serverEnv,
|
|
204
|
+
serverScope: options.serverScope,
|
|
205
|
+
typescriptEnabled: options.typescriptEnabled
|
|
206
|
+
},
|
|
207
|
+
extensions: this.#extensionCollector,
|
|
208
|
+
resolveCache: this.#resolveCacheCollector,
|
|
209
|
+
fileDiscovery: this.#fileDiscoveryCollector
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
static async write(baseDir, manifest) {
|
|
213
|
+
const dir = path.join(baseDir, ".egg");
|
|
214
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
215
|
+
const manifestPath = path.join(dir, "manifest.json");
|
|
216
|
+
await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
217
|
+
debug("manifest written to %s", manifestPath);
|
|
218
|
+
}
|
|
219
|
+
static clean(baseDir) {
|
|
220
|
+
const manifestPath = path.join(baseDir, ".egg", "manifest.json");
|
|
221
|
+
try {
|
|
222
|
+
fs.unlinkSync(manifestPath);
|
|
223
|
+
debug("manifest removed: %s", manifestPath);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
if (err.code !== "ENOENT") throw err;
|
|
226
|
+
}
|
|
227
|
+
ManifestStore.cleanCompileCache(baseDir);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Enable Node.js module compile cache for the current process.
|
|
231
|
+
* Sets NODE_COMPILE_CACHE and NODE_COMPILE_CACHE_PORTABLE env vars
|
|
232
|
+
* so forked child processes also inherit compile cache.
|
|
233
|
+
*/
|
|
234
|
+
static enableCompileCache(baseDir) {
|
|
235
|
+
if (process.env.NODE_COMPILE_CACHE || process.env.NODE_DISABLE_COMPILE_CACHE) return;
|
|
236
|
+
const cacheDir = path.join(baseDir, ".egg", "compile-cache");
|
|
237
|
+
process.env.NODE_COMPILE_CACHE = cacheDir;
|
|
238
|
+
process.env.NODE_COMPILE_CACHE_PORTABLE = "1";
|
|
239
|
+
try {
|
|
240
|
+
const result = BuiltinModule.enableCompileCache?.(cacheDir);
|
|
241
|
+
debug("compile cache enabled: %o", result);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
debug("compile cache enable failed: %o", err);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Flush accumulated compile cache entries to disk.
|
|
248
|
+
*/
|
|
249
|
+
static flushCompileCache() {
|
|
250
|
+
try {
|
|
251
|
+
BuiltinModule.flushCompileCache?.();
|
|
252
|
+
debug("compile cache flushed");
|
|
253
|
+
} catch (err) {
|
|
254
|
+
debug("compile cache flush failed: %o", err);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Remove the compile cache directory.
|
|
259
|
+
*/
|
|
260
|
+
static cleanCompileCache(baseDir) {
|
|
261
|
+
const compileCacheDir = path.join(baseDir, ".egg", "compile-cache");
|
|
262
|
+
fs.rmSync(compileCacheDir, {
|
|
263
|
+
recursive: true,
|
|
264
|
+
force: true
|
|
265
|
+
});
|
|
266
|
+
debug("compile cache removed: %s", compileCacheDir);
|
|
267
|
+
}
|
|
268
|
+
#toRelative(absPath) {
|
|
269
|
+
return (path.isAbsolute(absPath) ? path.relative(this.baseDir, absPath) : absPath).replaceAll(path.sep, "/");
|
|
270
|
+
}
|
|
271
|
+
#toAbsolute(relPath) {
|
|
272
|
+
if (path.isAbsolute(relPath)) return relPath;
|
|
273
|
+
return path.join(this.baseDir, relPath);
|
|
274
|
+
}
|
|
275
|
+
#resolveFromFileDiscovery(relKey) {
|
|
276
|
+
let matchedDir;
|
|
277
|
+
for (const dir of Object.keys(this.data.fileDiscovery)) if ((relKey === dir || relKey.startsWith(dir + "/")) && (!matchedDir || dir.length > matchedDir.length)) matchedDir = dir;
|
|
278
|
+
if (!matchedDir || relKey === matchedDir) return;
|
|
279
|
+
const request = relKey.slice(matchedDir.length + 1);
|
|
280
|
+
const matchedFile = this.data.fileDiscovery[matchedDir].find((file) => {
|
|
281
|
+
if (file === request) return true;
|
|
282
|
+
const ext = path.posix.extname(file);
|
|
283
|
+
if (!ext || ext === ".map") return false;
|
|
284
|
+
const extensionlessFile = file.slice(0, -ext.length);
|
|
285
|
+
return extensionlessFile === request || extensionlessFile === `${request}/index`;
|
|
286
|
+
});
|
|
287
|
+
return matchedFile ? this.#toAbsolute(path.posix.join(matchedDir, matchedFile)) : void 0;
|
|
288
|
+
}
|
|
289
|
+
static #statFingerprint(filepath) {
|
|
290
|
+
try {
|
|
291
|
+
const stat$1 = fs.statSync(filepath);
|
|
292
|
+
return `${stat$1.mtimeMs}:${stat$1.size}`;
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
static #lockfileFingerprint(baseDir) {
|
|
298
|
+
for (const name of LOCKFILE_NAMES) {
|
|
299
|
+
const fp = ManifestStore.#statFingerprint(path.join(baseDir, name));
|
|
300
|
+
if (fp) return `${name}:${fp}`;
|
|
301
|
+
}
|
|
302
|
+
return "";
|
|
303
|
+
}
|
|
304
|
+
static #directoryFingerprint(dirpath) {
|
|
305
|
+
const hash = createHash("md5");
|
|
306
|
+
const visited = /* @__PURE__ */ new Set();
|
|
307
|
+
ManifestStore.#fingerprintRecursive(dirpath, hash, visited);
|
|
308
|
+
return hash.digest("hex");
|
|
309
|
+
}
|
|
310
|
+
static #fingerprintRecursive(dirpath, hash, visited) {
|
|
311
|
+
let realPath;
|
|
312
|
+
try {
|
|
313
|
+
realPath = fs.realpathSync(dirpath);
|
|
314
|
+
} catch {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (visited.has(realPath)) return;
|
|
318
|
+
visited.add(realPath);
|
|
319
|
+
let entries;
|
|
320
|
+
try {
|
|
321
|
+
entries = fs.readdirSync(dirpath, { withFileTypes: true });
|
|
322
|
+
} catch {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
if (entry.isSymbolicLink()) continue;
|
|
328
|
+
const fullPath = path.join(dirpath, entry.name);
|
|
329
|
+
if (entry.isDirectory()) {
|
|
330
|
+
hash.update(`dir:${entry.name}\n`);
|
|
331
|
+
ManifestStore.#fingerprintRecursive(fullPath, hash, visited);
|
|
332
|
+
} else if (entry.isFile()) {
|
|
333
|
+
const fp = ManifestStore.#statFingerprint(fullPath);
|
|
334
|
+
hash.update(`file:${entry.name}:${fp ?? "missing"}\n`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
//#endregion
|
|
341
|
+
export { ManifestStore };
|