@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 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 };
@@ -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.triggerConfigWillLoad();
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.triggerConfigWillLoad();
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 filepaths = globby.sync(files, { cwd: directory });
131
- debug("[parse] globby files: %o, cwd: %o => %o", files, directory, filepaths);
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.5",
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.5",
45
- "@eggjs/koa": "3.1.2-beta.5",
46
- "@eggjs/router": "4.0.2-beta.5",
47
- "@eggjs/utils": "5.0.2-beta.5",
48
- "@eggjs/path-matching": "3.0.2-beta.5"
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.5",
61
- "@eggjs/tsconfig": "3.1.2-beta.5",
62
- "@eggjs/supertest": "9.0.2-beta.5"
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"