@eggjs/core 7.0.2-beta.5 → 7.0.2-beta.7
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 +2 -0
- package/dist/egg.js +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/lifecycle.d.ts +7 -0
- package/dist/lifecycle.js +21 -1
- package/dist/loader/egg_loader.d.ts +10 -0
- package/dist/loader/egg_loader.js +26 -4
- package/dist/loader/file_loader.d.ts +3 -0
- package/dist/loader/file_loader.js +3 -2
- package/dist/loader/manifest.d.ts +64 -0
- package/dist/loader/manifest.js +245 -0
- package/package.json +9 -9
package/dist/egg.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ interface EggCoreOptions {
|
|
|
18
18
|
plugins?: any;
|
|
19
19
|
serverScope?: string;
|
|
20
20
|
env?: string;
|
|
21
|
+
/** Skip lifecycle hooks, only trigger loadMetadata for manifest generation */
|
|
22
|
+
metadataOnly?: boolean;
|
|
21
23
|
}
|
|
22
24
|
type EggCoreInitOptions = Partial<EggCoreOptions>;
|
|
23
25
|
declare class Request$1 extends KoaRequest {
|
package/dist/egg.js
CHANGED
|
@@ -130,7 +130,8 @@ var EggCore = class EggCore extends KoaApplication {
|
|
|
130
130
|
logger: this.console,
|
|
131
131
|
serverScope: options.serverScope,
|
|
132
132
|
env: options.env ?? "",
|
|
133
|
-
EggCoreClass: EggCore
|
|
133
|
+
EggCoreClass: EggCore,
|
|
134
|
+
metadataOnly: options.metadataOnly
|
|
134
135
|
});
|
|
135
136
|
}
|
|
136
137
|
get logger() {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,11 @@ import { BaseContextClass } from "./base_context_class.js";
|
|
|
3
3
|
import { Timing, TimingItem } from "./utils/timing.js";
|
|
4
4
|
import { BootImplClass, FunWithFullPath, ILifecycleBoot, Lifecycle, LifecycleOptions } from "./lifecycle.js";
|
|
5
5
|
import { CustomLoaderConfigItem, EggAppConfig, EggAppInfo, EggPluginInfo } from "./types.js";
|
|
6
|
+
import { ManifestGenerateOptions, ManifestInvalidation, ManifestStore, StartupManifest } from "./loader/manifest.js";
|
|
6
7
|
import { CaseStyle, CaseStyleFunction, EXPORTS, FULLPATH, FileLoader, FileLoaderFilter, FileLoaderInitializer, FileLoaderOptions, FileLoaderParseItem } from "./loader/file_loader.js";
|
|
7
8
|
import { ClassLoader, ClassLoaderOptions, ContextLoader, ContextLoaderOptions } from "./loader/context_loader.js";
|
|
8
9
|
import { EggDirInfo, EggDirInfoType, EggLoader, EggLoaderOptions } from "./loader/egg_loader.js";
|
|
9
10
|
import { Singleton, SingletonCreateMethod, SingletonOptions } from "./singleton.js";
|
|
10
11
|
import { Context, EGG_LOADER, EggCore, EggCoreInitOptions, EggCoreOptions, KoaApplication, KoaContext, KoaMiddlewareFunc, KoaRequest, KoaResponse, MiddlewareFunc, Next, Request, Response, Router } from "./egg.js";
|
|
11
12
|
import { SequencifyResult, SequencifyTask, sequencify } from "./utils/sequencify.js";
|
|
12
|
-
export { BaseContextClass, BootImplClass, CaseStyle, CaseStyleFunction, ClassLoader, ClassLoaderOptions, Context, ContextLoader, ContextLoaderOptions, CustomLoaderConfigItem, EGG_LOADER, EXPORTS, EggAppConfig, EggAppInfo, EggCore, EggCoreInitOptions, EggCoreOptions, EggDirInfo, EggDirInfoType, EggLoader, EggLoaderOptions, EggPluginInfo, FULLPATH, FileLoader, FileLoaderFilter, FileLoaderInitializer, FileLoaderOptions, FileLoaderParseItem, FunWithFullPath, ILifecycleBoot, KoaApplication, KoaContext, KoaMiddlewareFunc, KoaRequest, KoaResponse, Lifecycle, LifecycleOptions, MiddlewareFunc, Next, Request, Response, Router, SequencifyResult, SequencifyTask, Singleton, SingletonCreateMethod, SingletonOptions, Timing, TimingItem, sequencify, utils };
|
|
13
|
+
export { BaseContextClass, BootImplClass, CaseStyle, CaseStyleFunction, ClassLoader, ClassLoaderOptions, Context, ContextLoader, ContextLoaderOptions, CustomLoaderConfigItem, EGG_LOADER, EXPORTS, EggAppConfig, EggAppInfo, EggCore, EggCoreInitOptions, EggCoreOptions, EggDirInfo, EggDirInfoType, EggLoader, EggLoaderOptions, EggPluginInfo, FULLPATH, FileLoader, FileLoaderFilter, FileLoaderInitializer, FileLoaderOptions, FileLoaderParseItem, FunWithFullPath, ILifecycleBoot, KoaApplication, KoaContext, KoaMiddlewareFunc, KoaRequest, KoaResponse, Lifecycle, LifecycleOptions, ManifestGenerateOptions, ManifestInvalidation, ManifestStore, MiddlewareFunc, Next, Request, Response, Router, SequencifyResult, SequencifyTask, Singleton, SingletonCreateMethod, SingletonOptions, StartupManifest, Timing, TimingItem, sequencify, utils };
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,9 @@ import { sequencify } from "./utils/sequencify.js";
|
|
|
5
5
|
import { Timing } from "./utils/timing.js";
|
|
6
6
|
import { CaseStyle, EXPORTS, FULLPATH, FileLoader } from "./loader/file_loader.js";
|
|
7
7
|
import { ClassLoader, ContextLoader } from "./loader/context_loader.js";
|
|
8
|
+
import { ManifestStore } from "./loader/manifest.js";
|
|
8
9
|
import { EggLoader } from "./loader/egg_loader.js";
|
|
9
10
|
import { Singleton } from "./singleton.js";
|
|
10
11
|
import { Context, EGG_LOADER, EggCore, KoaApplication, KoaContext, KoaRequest, KoaResponse, Request, Response, Router } from "./egg.js";
|
|
11
12
|
|
|
12
|
-
export { BaseContextClass, CaseStyle, ClassLoader, Context, ContextLoader, EGG_LOADER, EXPORTS, EggCore, EggLoader, FULLPATH, FileLoader, KoaApplication, KoaContext, KoaRequest, KoaResponse, Lifecycle, Request, Response, Router, Singleton, Timing, sequencify, utils_default as utils };
|
|
13
|
+
export { BaseContextClass, CaseStyle, ClassLoader, Context, ContextLoader, EGG_LOADER, EXPORTS, EggCore, EggLoader, FULLPATH, FileLoader, KoaApplication, KoaContext, KoaRequest, KoaResponse, Lifecycle, ManifestStore, Request, Response, Router, Singleton, Timing, sequencify, utils_default as utils };
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -40,6 +40,12 @@ interface ILifecycleBoot {
|
|
|
40
40
|
* Do some thing before app close
|
|
41
41
|
*/
|
|
42
42
|
beforeClose?(): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Collect metadata for manifest generation (metadataOnly mode).
|
|
45
|
+
* Called instead of configWillLoad/configDidLoad/didLoad/willReady
|
|
46
|
+
* when the application is started with metadataOnly: true.
|
|
47
|
+
*/
|
|
48
|
+
loadMetadata?(): Promise<void> | void;
|
|
43
49
|
}
|
|
44
50
|
type BootImplClass<T = ILifecycleBoot> = new (...args: any[]) => T;
|
|
45
51
|
interface LifecycleOptions {
|
|
@@ -78,6 +84,7 @@ declare class Lifecycle extends EventEmitter {
|
|
|
78
84
|
triggerWillReady(): void;
|
|
79
85
|
triggerDidReady(err?: Error): Promise<void>;
|
|
80
86
|
triggerServerDidReady(): Promise<void>;
|
|
87
|
+
triggerLoadMetadata(): Promise<void>;
|
|
81
88
|
}
|
|
82
89
|
//#endregion
|
|
83
90
|
export { BootImplClass, FunWithFullPath, ILifecycleBoot, Lifecycle, LifecycleOptions };
|
package/dist/lifecycle.js
CHANGED
|
@@ -15,6 +15,7 @@ var Lifecycle = class extends EventEmitter {
|
|
|
15
15
|
#bootHooks;
|
|
16
16
|
#boots;
|
|
17
17
|
#isClosed;
|
|
18
|
+
#metadataOnly;
|
|
18
19
|
#closeFunctionSet;
|
|
19
20
|
loadReady;
|
|
20
21
|
bootReady;
|
|
@@ -29,6 +30,7 @@ var Lifecycle = class extends EventEmitter {
|
|
|
29
30
|
this.#boots = [];
|
|
30
31
|
this.#closeFunctionSet = /* @__PURE__ */ new Set();
|
|
31
32
|
this.#isClosed = false;
|
|
33
|
+
this.#metadataOnly = false;
|
|
32
34
|
this.#init = false;
|
|
33
35
|
this.timing.start(`${this.options.app.type} Start`);
|
|
34
36
|
const eggReadyTimeoutEnv = Number.parseInt(process.env.EGG_READY_TIMEOUT_ENV || "10000");
|
|
@@ -41,7 +43,7 @@ var Lifecycle = class extends EventEmitter {
|
|
|
41
43
|
this.logger.warn("[egg/core/lifecycle:ready_timeout] %s seconds later %s was still unable to finish.", this.readyTimeout / 1e3, id);
|
|
42
44
|
});
|
|
43
45
|
this.ready((err) => {
|
|
44
|
-
this.triggerDidReady(err);
|
|
46
|
+
if (!this.#metadataOnly) this.triggerDidReady(err);
|
|
45
47
|
debug("app ready");
|
|
46
48
|
this.timing.end(`${this.options.app.type} Start`);
|
|
47
49
|
});
|
|
@@ -221,6 +223,24 @@ var Lifecycle = class extends EventEmitter {
|
|
|
221
223
|
debug("trigger serverDidReady end");
|
|
222
224
|
})();
|
|
223
225
|
}
|
|
226
|
+
async triggerLoadMetadata() {
|
|
227
|
+
this.#metadataOnly = true;
|
|
228
|
+
debug("trigger loadMetadata start");
|
|
229
|
+
let firstError;
|
|
230
|
+
for (const boot of this.#boots) if (typeof boot.loadMetadata === "function") {
|
|
231
|
+
debug("trigger loadMetadata at %o", boot.fullPath);
|
|
232
|
+
try {
|
|
233
|
+
await boot.loadMetadata();
|
|
234
|
+
} catch (err) {
|
|
235
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
236
|
+
if (!firstError) firstError = error;
|
|
237
|
+
debug("trigger loadMetadata error at %o, error: %s", boot.fullPath, error);
|
|
238
|
+
this.emit("error", error);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
debug("trigger loadMetadata end");
|
|
242
|
+
this.ready(firstError ?? true);
|
|
243
|
+
}
|
|
224
244
|
#initReady() {
|
|
225
245
|
debug("loadReady init");
|
|
226
246
|
this.loadReady = new Ready$1({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Timing } from "../utils/timing.js";
|
|
2
2
|
import { Lifecycle } from "../lifecycle.js";
|
|
3
3
|
import { EggAppConfig, EggAppInfo, EggPluginInfo } from "../types.js";
|
|
4
|
+
import { ManifestStore, StartupManifest } from "./manifest.js";
|
|
4
5
|
import { FileLoader, FileLoaderOptions } from "./file_loader.js";
|
|
5
6
|
import { ContextLoader, ContextLoaderOptions } from "./context_loader.js";
|
|
6
7
|
import { EggCore } from "../egg.js";
|
|
@@ -21,6 +22,8 @@ interface EggLoaderOptions {
|
|
|
21
22
|
serverScope?: string;
|
|
22
23
|
/** custom plugins */
|
|
23
24
|
plugins?: Record<string, EggPluginInfo>;
|
|
25
|
+
/** Skip lifecycle hooks, only trigger loadMetadata for manifest generation */
|
|
26
|
+
metadataOnly?: boolean;
|
|
24
27
|
}
|
|
25
28
|
type EggDirInfoType = "app" | "plugin" | "framework";
|
|
26
29
|
interface EggDirInfo {
|
|
@@ -38,6 +41,8 @@ declare class EggLoader {
|
|
|
38
41
|
readonly appInfo: EggAppInfo;
|
|
39
42
|
readonly outDir?: string;
|
|
40
43
|
dirs?: EggDirInfo[];
|
|
44
|
+
/** Startup manifest — loaded from cache or collecting for generation */
|
|
45
|
+
readonly manifest: ManifestStore;
|
|
41
46
|
/**
|
|
42
47
|
* @class
|
|
43
48
|
* @param {Object} options - options
|
|
@@ -369,6 +374,11 @@ declare class EggLoader {
|
|
|
369
374
|
get ContextLoader(): typeof ContextLoader;
|
|
370
375
|
getTypeFiles(filename: string): string[];
|
|
371
376
|
resolveModule(filepath: string): string | undefined;
|
|
377
|
+
/**
|
|
378
|
+
* Generate startup manifest from collected data.
|
|
379
|
+
* Should be called after all loading phases complete.
|
|
380
|
+
*/
|
|
381
|
+
generateManifest(): StartupManifest;
|
|
372
382
|
}
|
|
373
383
|
//#endregion
|
|
374
384
|
export { EggDirInfo, EggDirInfoType, EggLoader, EggLoaderOptions };
|
|
@@ -3,6 +3,7 @@ import { sequencify } from "../utils/sequencify.js";
|
|
|
3
3
|
import { Timing } from "../utils/timing.js";
|
|
4
4
|
import { CaseStyle, FULLPATH, FileLoader } from "./file_loader.js";
|
|
5
5
|
import { ContextLoader } from "./context_loader.js";
|
|
6
|
+
import { ManifestStore } from "./manifest.js";
|
|
6
7
|
import fs from "node:fs";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { debuglog, inspect } from "node:util";
|
|
@@ -36,6 +37,8 @@ var EggLoader = class {
|
|
|
36
37
|
appInfo;
|
|
37
38
|
outDir;
|
|
38
39
|
dirs;
|
|
40
|
+
/** Startup manifest — loaded from cache or collecting for generation */
|
|
41
|
+
manifest;
|
|
39
42
|
/**
|
|
40
43
|
* @class
|
|
41
44
|
* @param {Object} options - options
|
|
@@ -104,6 +107,7 @@ var EggLoader = class {
|
|
|
104
107
|
* @since 1.0.0
|
|
105
108
|
*/
|
|
106
109
|
this.appInfo = this.getAppInfo();
|
|
110
|
+
this.manifest = ManifestStore.load(this.options.baseDir, this.serverEnv, this.serverScope) ?? ManifestStore.createCollector(this.options.baseDir);
|
|
107
111
|
}
|
|
108
112
|
get app() {
|
|
109
113
|
return this.options.app;
|
|
@@ -741,14 +745,16 @@ var EggLoader = class {
|
|
|
741
745
|
*/
|
|
742
746
|
async loadCustomApp() {
|
|
743
747
|
await this.#loadBootHook("app");
|
|
744
|
-
this.lifecycle.
|
|
748
|
+
if (this.options.metadataOnly) await this.lifecycle.triggerLoadMetadata();
|
|
749
|
+
else this.lifecycle.triggerConfigWillLoad();
|
|
745
750
|
}
|
|
746
751
|
/**
|
|
747
752
|
* Load agent.js, same as {@link EggLoader#loadCustomApp}
|
|
748
753
|
*/
|
|
749
754
|
async loadCustomAgent() {
|
|
750
755
|
await this.#loadBootHook("agent");
|
|
751
|
-
this.lifecycle.
|
|
756
|
+
if (this.options.metadataOnly) await this.lifecycle.triggerLoadMetadata();
|
|
757
|
+
else this.lifecycle.triggerConfigWillLoad();
|
|
752
758
|
}
|
|
753
759
|
loadBootHook() {}
|
|
754
760
|
async #loadBootHook(fileName) {
|
|
@@ -1041,7 +1047,8 @@ var EggLoader = class {
|
|
|
1041
1047
|
...options,
|
|
1042
1048
|
directory: options?.directory ?? directory,
|
|
1043
1049
|
target,
|
|
1044
|
-
inject: this.app
|
|
1050
|
+
inject: this.app,
|
|
1051
|
+
manifest: this.manifest
|
|
1045
1052
|
};
|
|
1046
1053
|
const timingKey = `Load "${String(property)}" to Application`;
|
|
1047
1054
|
this.timing.start(timingKey);
|
|
@@ -1060,7 +1067,8 @@ var EggLoader = class {
|
|
|
1060
1067
|
...options,
|
|
1061
1068
|
directory: options?.directory || directory,
|
|
1062
1069
|
property,
|
|
1063
|
-
inject: this.app
|
|
1070
|
+
inject: this.app,
|
|
1071
|
+
manifest: this.manifest
|
|
1064
1072
|
};
|
|
1065
1073
|
const timingKey = `Load "${String(property)}" to Context`;
|
|
1066
1074
|
this.timing.start(timingKey);
|
|
@@ -1090,6 +1098,9 @@ var EggLoader = class {
|
|
|
1090
1098
|
return files;
|
|
1091
1099
|
}
|
|
1092
1100
|
resolveModule(filepath) {
|
|
1101
|
+
return this.manifest.resolveModule(filepath, () => this.#doResolveModule(filepath));
|
|
1102
|
+
}
|
|
1103
|
+
#doResolveModule(filepath) {
|
|
1093
1104
|
let fullPath;
|
|
1094
1105
|
try {
|
|
1095
1106
|
fullPath = utils_default.resolvePath(filepath);
|
|
@@ -1124,6 +1135,17 @@ var EggLoader = class {
|
|
|
1124
1135
|
}
|
|
1125
1136
|
}
|
|
1126
1137
|
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Generate startup manifest from collected data.
|
|
1140
|
+
* Should be called after all loading phases complete.
|
|
1141
|
+
*/
|
|
1142
|
+
generateManifest() {
|
|
1143
|
+
return this.manifest.generateManifest({
|
|
1144
|
+
serverEnv: this.serverEnv,
|
|
1145
|
+
serverScope: this.serverScope,
|
|
1146
|
+
typescriptEnabled: isSupportTypeScript()
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1127
1149
|
};
|
|
1128
1150
|
function depCompatible(plugin) {
|
|
1129
1151
|
if (plugin.dep && !(Array.isArray(plugin.dependencies) && plugin.dependencies.length > 0)) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Fun } from "../utils/index.js";
|
|
2
|
+
import { ManifestStore } from "./manifest.js";
|
|
2
3
|
|
|
3
4
|
//#region src/loader/file_loader.d.ts
|
|
4
5
|
declare const FULLPATH: unique symbol;
|
|
@@ -37,6 +38,8 @@ interface FileLoaderOptions {
|
|
|
37
38
|
/** set property's case when converting a filepath to property list. */
|
|
38
39
|
caseStyle?: CaseStyle | CaseStyleFunction;
|
|
39
40
|
lowercaseFirst?: boolean;
|
|
41
|
+
/** Startup manifest for caching globby scans and collecting results */
|
|
42
|
+
manifest?: ManifestStore;
|
|
40
43
|
}
|
|
41
44
|
interface FileLoaderParseItem {
|
|
42
45
|
fullpath: string;
|
|
@@ -127,8 +127,9 @@ var FileLoader = class {
|
|
|
127
127
|
const items = [];
|
|
128
128
|
debug("[parse] parsing directories: %j", directories);
|
|
129
129
|
for (const directory of directories) {
|
|
130
|
-
const
|
|
131
|
-
|
|
130
|
+
const manifest = this.options.manifest;
|
|
131
|
+
const filepaths = manifest ? manifest.globFiles(directory, () => globby.sync(files, { cwd: directory })) : globby.sync(files, { cwd: directory });
|
|
132
|
+
debug("[parse] files: %o, cwd: %o => %o", files, directory, filepaths);
|
|
132
133
|
for (const filepath of filepaths) {
|
|
133
134
|
const fullpath = path.join(directory, filepath);
|
|
134
135
|
if (!fs.statSync(fullpath).isFile()) continue;
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
* Load and validate manifest from `.egg/manifest.json`.
|
|
27
|
+
* Returns null if manifest doesn't exist or is invalid.
|
|
28
|
+
*/
|
|
29
|
+
static load(baseDir: string, serverEnv: string, serverScope: string): ManifestStore | null;
|
|
30
|
+
/**
|
|
31
|
+
* Create a collector-only ManifestStore (no cached data).
|
|
32
|
+
* Used during normal startup to collect data for future manifest generation.
|
|
33
|
+
*/
|
|
34
|
+
static createCollector(baseDir: string): ManifestStore;
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a module path. Checks cache first, falls back to resolver, collects result.
|
|
37
|
+
*/
|
|
38
|
+
resolveModule(filepath: string, fallback: () => string | undefined): string | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Get file list for a directory. Checks cache first, falls back to globber, collects result.
|
|
41
|
+
*/
|
|
42
|
+
globFiles(directory: string, fallback: () => string[]): string[];
|
|
43
|
+
/**
|
|
44
|
+
* Look up a plugin extension by name.
|
|
45
|
+
*/
|
|
46
|
+
getExtension(name: string): unknown;
|
|
47
|
+
/**
|
|
48
|
+
* Register plugin extension data for manifest generation.
|
|
49
|
+
*/
|
|
50
|
+
setExtension(name: string, data: unknown): void;
|
|
51
|
+
/**
|
|
52
|
+
* Generate a StartupManifest from collected data.
|
|
53
|
+
*/
|
|
54
|
+
generateManifest(options: ManifestGenerateOptions): StartupManifest;
|
|
55
|
+
static write(baseDir: string, manifest: StartupManifest): Promise<void>;
|
|
56
|
+
static clean(baseDir: string): void;
|
|
57
|
+
}
|
|
58
|
+
interface ManifestGenerateOptions {
|
|
59
|
+
serverEnv: string;
|
|
60
|
+
serverScope: string;
|
|
61
|
+
typescriptEnabled: boolean;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { ManifestGenerateOptions, ManifestInvalidation, ManifestStore, StartupManifest };
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { debuglog } from "node:util";
|
|
5
|
+
import { isSupportTypeScript } from "@eggjs/utils";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
//#region src/loader/manifest.ts
|
|
9
|
+
const debug = debuglog("egg/core/loader/manifest");
|
|
10
|
+
const MANIFEST_VERSION = 1;
|
|
11
|
+
const LOCKFILE_NAMES = [
|
|
12
|
+
"pnpm-lock.yaml",
|
|
13
|
+
"package-lock.json",
|
|
14
|
+
"yarn.lock"
|
|
15
|
+
];
|
|
16
|
+
var ManifestStore = class ManifestStore {
|
|
17
|
+
data;
|
|
18
|
+
baseDir;
|
|
19
|
+
#resolveCacheCollector = {};
|
|
20
|
+
#fileDiscoveryCollector = {};
|
|
21
|
+
#extensionCollector = {};
|
|
22
|
+
constructor(data, baseDir) {
|
|
23
|
+
this.data = data;
|
|
24
|
+
this.baseDir = baseDir;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Load and validate manifest from `.egg/manifest.json`.
|
|
28
|
+
* Returns null if manifest doesn't exist or is invalid.
|
|
29
|
+
*/
|
|
30
|
+
static load(baseDir, serverEnv, serverScope) {
|
|
31
|
+
if (serverEnv === "local" && process.env.EGG_MANIFEST !== "true") {
|
|
32
|
+
debug("skip manifest in local env (set EGG_MANIFEST=true to enable)");
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const manifestPath = path.join(baseDir, ".egg", "manifest.json");
|
|
36
|
+
let raw;
|
|
37
|
+
try {
|
|
38
|
+
raw = fs.readFileSync(manifestPath, "utf-8");
|
|
39
|
+
} catch {
|
|
40
|
+
debug("manifest not found at %s", manifestPath);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
let data;
|
|
44
|
+
try {
|
|
45
|
+
data = JSON.parse(raw);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
debug("failed to parse manifest: %s", e);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (!ManifestStore.#validate(data, baseDir, serverEnv, serverScope)) return null;
|
|
51
|
+
debug("manifest loaded successfully");
|
|
52
|
+
return new ManifestStore(data, baseDir);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create a collector-only ManifestStore (no cached data).
|
|
56
|
+
* Used during normal startup to collect data for future manifest generation.
|
|
57
|
+
*/
|
|
58
|
+
static createCollector(baseDir) {
|
|
59
|
+
return new ManifestStore({
|
|
60
|
+
version: MANIFEST_VERSION,
|
|
61
|
+
generatedAt: "",
|
|
62
|
+
invalidation: {
|
|
63
|
+
lockfileFingerprint: "",
|
|
64
|
+
configFingerprint: "",
|
|
65
|
+
serverEnv: "",
|
|
66
|
+
serverScope: "",
|
|
67
|
+
typescriptEnabled: false
|
|
68
|
+
},
|
|
69
|
+
extensions: {},
|
|
70
|
+
resolveCache: {},
|
|
71
|
+
fileDiscovery: {}
|
|
72
|
+
}, baseDir);
|
|
73
|
+
}
|
|
74
|
+
static #validate(data, baseDir, serverEnv, serverScope) {
|
|
75
|
+
if (data.version !== MANIFEST_VERSION) {
|
|
76
|
+
debug("manifest version mismatch: expected %d, got %d", MANIFEST_VERSION, data.version);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const inv = data.invalidation;
|
|
80
|
+
if (!inv) {
|
|
81
|
+
debug("manifest missing invalidation data");
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (inv.serverEnv !== serverEnv) {
|
|
85
|
+
debug("manifest serverEnv mismatch: expected %s, got %s", serverEnv, inv.serverEnv);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (inv.serverScope !== serverScope) {
|
|
89
|
+
debug("manifest serverScope mismatch: expected %s, got %s", serverScope, inv.serverScope);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const currentTypescriptEnabled = isSupportTypeScript();
|
|
93
|
+
if (inv.typescriptEnabled !== currentTypescriptEnabled) {
|
|
94
|
+
debug("manifest typescriptEnabled mismatch: expected %s, got %s", currentTypescriptEnabled, inv.typescriptEnabled);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const currentLockfileFingerprint = ManifestStore.#lockfileFingerprint(baseDir);
|
|
98
|
+
if (inv.lockfileFingerprint !== currentLockfileFingerprint) {
|
|
99
|
+
debug("manifest lockfileFingerprint mismatch");
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const currentConfigFingerprint = ManifestStore.#directoryFingerprint(path.join(baseDir, "config"));
|
|
103
|
+
if (inv.configFingerprint !== currentConfigFingerprint) {
|
|
104
|
+
debug("manifest configFingerprint mismatch");
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve a module path. Checks cache first, falls back to resolver, collects result.
|
|
111
|
+
*/
|
|
112
|
+
resolveModule(filepath, fallback) {
|
|
113
|
+
const relKey = this.#toRelative(filepath);
|
|
114
|
+
const cache = this.data.resolveCache;
|
|
115
|
+
if (cache && relKey in cache) {
|
|
116
|
+
const cached = cache[relKey];
|
|
117
|
+
debug("[resolveModule:manifest] %o => %o", filepath, cached);
|
|
118
|
+
return cached !== null ? this.#toAbsolute(cached) : void 0;
|
|
119
|
+
}
|
|
120
|
+
const result = fallback();
|
|
121
|
+
this.#resolveCacheCollector[relKey] = result !== void 0 ? this.#toRelative(result) : null;
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get file list for a directory. Checks cache first, falls back to globber, collects result.
|
|
126
|
+
*/
|
|
127
|
+
globFiles(directory, fallback) {
|
|
128
|
+
const relKey = this.#toRelative(directory);
|
|
129
|
+
const cache = this.data.fileDiscovery;
|
|
130
|
+
if (cache && relKey in cache) {
|
|
131
|
+
const cached = cache[relKey];
|
|
132
|
+
debug("[globFiles:manifest] using cached files for %o, count: %d", directory, cached.length);
|
|
133
|
+
return cached;
|
|
134
|
+
}
|
|
135
|
+
const result = fallback();
|
|
136
|
+
this.#fileDiscoveryCollector[relKey] = result;
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Look up a plugin extension by name.
|
|
141
|
+
*/
|
|
142
|
+
getExtension(name) {
|
|
143
|
+
return this.data.extensions?.[name];
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Register plugin extension data for manifest generation.
|
|
147
|
+
*/
|
|
148
|
+
setExtension(name, data) {
|
|
149
|
+
this.#extensionCollector[name] = data;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Generate a StartupManifest from collected data.
|
|
153
|
+
*/
|
|
154
|
+
generateManifest(options) {
|
|
155
|
+
return {
|
|
156
|
+
version: MANIFEST_VERSION,
|
|
157
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
158
|
+
invalidation: {
|
|
159
|
+
lockfileFingerprint: ManifestStore.#lockfileFingerprint(this.baseDir),
|
|
160
|
+
configFingerprint: ManifestStore.#directoryFingerprint(path.join(this.baseDir, "config")),
|
|
161
|
+
serverEnv: options.serverEnv,
|
|
162
|
+
serverScope: options.serverScope,
|
|
163
|
+
typescriptEnabled: options.typescriptEnabled
|
|
164
|
+
},
|
|
165
|
+
extensions: this.#extensionCollector,
|
|
166
|
+
resolveCache: this.#resolveCacheCollector,
|
|
167
|
+
fileDiscovery: this.#fileDiscoveryCollector
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
static async write(baseDir, manifest) {
|
|
171
|
+
const dir = path.join(baseDir, ".egg");
|
|
172
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
173
|
+
const manifestPath = path.join(dir, "manifest.json");
|
|
174
|
+
await fsp.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
175
|
+
debug("manifest written to %s", manifestPath);
|
|
176
|
+
}
|
|
177
|
+
static clean(baseDir) {
|
|
178
|
+
const manifestPath = path.join(baseDir, ".egg", "manifest.json");
|
|
179
|
+
try {
|
|
180
|
+
fs.unlinkSync(manifestPath);
|
|
181
|
+
debug("manifest removed: %s", manifestPath);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (err.code !== "ENOENT") throw err;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
#toRelative(absPath) {
|
|
187
|
+
return (path.isAbsolute(absPath) ? path.relative(this.baseDir, absPath) : absPath).replaceAll(path.sep, "/");
|
|
188
|
+
}
|
|
189
|
+
#toAbsolute(relPath) {
|
|
190
|
+
if (path.isAbsolute(relPath)) return relPath;
|
|
191
|
+
return path.join(this.baseDir, relPath);
|
|
192
|
+
}
|
|
193
|
+
static #statFingerprint(filepath) {
|
|
194
|
+
try {
|
|
195
|
+
const stat$1 = fs.statSync(filepath);
|
|
196
|
+
return `${stat$1.mtimeMs}:${stat$1.size}`;
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
static #lockfileFingerprint(baseDir) {
|
|
202
|
+
for (const name of LOCKFILE_NAMES) {
|
|
203
|
+
const fp = ManifestStore.#statFingerprint(path.join(baseDir, name));
|
|
204
|
+
if (fp) return `${name}:${fp}`;
|
|
205
|
+
}
|
|
206
|
+
return "";
|
|
207
|
+
}
|
|
208
|
+
static #directoryFingerprint(dirpath) {
|
|
209
|
+
const hash = createHash("md5");
|
|
210
|
+
const visited = /* @__PURE__ */ new Set();
|
|
211
|
+
ManifestStore.#fingerprintRecursive(dirpath, hash, visited);
|
|
212
|
+
return hash.digest("hex");
|
|
213
|
+
}
|
|
214
|
+
static #fingerprintRecursive(dirpath, hash, visited) {
|
|
215
|
+
let realPath;
|
|
216
|
+
try {
|
|
217
|
+
realPath = fs.realpathSync(dirpath);
|
|
218
|
+
} catch {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (visited.has(realPath)) return;
|
|
222
|
+
visited.add(realPath);
|
|
223
|
+
let entries;
|
|
224
|
+
try {
|
|
225
|
+
entries = fs.readdirSync(dirpath, { withFileTypes: true });
|
|
226
|
+
} catch {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
if (entry.isSymbolicLink()) continue;
|
|
232
|
+
const fullPath = path.join(dirpath, entry.name);
|
|
233
|
+
if (entry.isDirectory()) {
|
|
234
|
+
hash.update(`dir:${entry.name}\n`);
|
|
235
|
+
ManifestStore.#fingerprintRecursive(fullPath, hash, visited);
|
|
236
|
+
} else if (entry.isFile()) {
|
|
237
|
+
const fp = ManifestStore.#statFingerprint(fullPath);
|
|
238
|
+
hash.update(`file:${entry.name}:${fp ?? "missing"}\n`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
//#endregion
|
|
245
|
+
export { ManifestStore };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eggjs/core",
|
|
3
|
-
"version": "7.0.2-beta.
|
|
3
|
+
"version": "7.0.2-beta.7",
|
|
4
4
|
"description": "A core plugin framework based on @eggjs/koa",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"egg",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"ready-callback": "^4.0.0",
|
|
42
42
|
"tsconfig-paths": "^4.2.0",
|
|
43
43
|
"utility": "^2.5.0",
|
|
44
|
-
"@eggjs/extend2": "5.0.2-beta.
|
|
45
|
-
"@eggjs/koa": "3.1.2-beta.
|
|
46
|
-
"@eggjs/
|
|
47
|
-
"@eggjs/
|
|
48
|
-
"@eggjs/
|
|
44
|
+
"@eggjs/extend2": "5.0.2-beta.7",
|
|
45
|
+
"@eggjs/koa": "3.1.2-beta.7",
|
|
46
|
+
"@eggjs/path-matching": "3.0.2-beta.7",
|
|
47
|
+
"@eggjs/router": "4.0.2-beta.7",
|
|
48
|
+
"@eggjs/utils": "5.0.2-beta.7"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/js-yaml": "^4.0.9",
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"mm": "^4.0.2",
|
|
58
58
|
"typescript": "^5.9.3",
|
|
59
59
|
"urllib": "^4.8.2",
|
|
60
|
-
"@eggjs/mock": "7.0.2-beta.
|
|
61
|
-
"@eggjs/tsconfig": "3.1.2-beta.
|
|
62
|
-
"@eggjs/supertest": "9.0.2-beta.
|
|
60
|
+
"@eggjs/mock": "7.0.2-beta.7",
|
|
61
|
+
"@eggjs/tsconfig": "3.1.2-beta.7",
|
|
62
|
+
"@eggjs/supertest": "9.0.2-beta.7"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
|
65
65
|
"node": ">=22.18.0"
|