@eggjs/core 7.0.2-beta.0 → 7.0.2-beta.10

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.
@@ -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";
@@ -11,14 +12,35 @@ import assert from "node:assert";
11
12
  import { Application, Context, Request, Response } from "@eggjs/koa";
12
13
  import { isAsyncFunction, isClass, isGeneratorFunction, isObject, isPromise } from "is-type-of";
13
14
  import { extend } from "@eggjs/extend2";
15
+ import { RealLoaderFS } from "@eggjs/loader-fs";
14
16
  import { pathMatching } from "@eggjs/path-matching";
17
+ import globby from "globby";
15
18
  import { homedir } from "node-homedir";
16
19
  import { diff, now } from "performance-ms";
17
20
  import { register } from "tsconfig-paths";
18
- import { exists, getParamNames, readJSON, readJSONSync } from "utility";
21
+ import { getParamNames } from "utility";
19
22
 
20
23
  //#region src/loader/egg_loader.ts
21
24
  const debug = debuglog("egg/core/loader/egg_loader");
25
+ const CONVENTIONAL_MANIFEST_LOADS = [
26
+ {
27
+ type: "resolve",
28
+ path: ["agent"]
29
+ },
30
+ {
31
+ type: "resolve",
32
+ path: ["app"]
33
+ },
34
+ {
35
+ type: "discover",
36
+ path: ["app", "extend"],
37
+ extensionlessResolve: true
38
+ },
39
+ {
40
+ type: "discover",
41
+ path: ["app", "middleware"]
42
+ }
43
+ ];
22
44
  const originalPrototypes = {
23
45
  request: Request.prototype,
24
46
  response: Response.prototype,
@@ -34,7 +56,11 @@ var EggLoader = class {
34
56
  serverEnv;
35
57
  serverScope;
36
58
  appInfo;
59
+ outDir;
37
60
  dirs;
61
+ /** Startup manifest — loaded from cache or collecting for generation */
62
+ manifest;
63
+ loaderFS;
38
64
  /**
39
65
  * @class
40
66
  * @param {Object} options - options
@@ -46,6 +72,7 @@ var EggLoader = class {
46
72
  */
47
73
  constructor(options) {
48
74
  this.options = options;
75
+ this.loaderFS = this.options.loaderFS ?? new RealLoaderFS();
49
76
  assert(fs.existsSync(this.options.baseDir), `${this.options.baseDir} not exists`);
50
77
  assert(this.options.app, "options.app is required");
51
78
  assert(this.options.logger, "options.logger is required");
@@ -55,7 +82,8 @@ var EggLoader = class {
55
82
  * @see {@link AppInfo#pkg}
56
83
  * @since 1.0.0
57
84
  */
58
- this.pkg = readJSONSync(path.join(this.options.baseDir, "package.json"));
85
+ this.pkg = this.loaderFS.readJSON(path.join(this.options.baseDir, "package.json"));
86
+ this.outDir = this.#resolveOutDir();
59
87
  if (process.env.EGG_TYPESCRIPT === "true" || this.pkg.egg && this.pkg.egg.typescript) {
60
88
  const tsConfigFile = path.join(this.options.baseDir, "tsconfig.json");
61
89
  if (fs.existsSync(tsConfigFile)) register({ cwd: this.options.baseDir });
@@ -102,6 +130,7 @@ var EggLoader = class {
102
130
  * @since 1.0.0
103
131
  */
104
132
  this.appInfo = this.getAppInfo();
133
+ this.manifest = ManifestStore.load(this.options.baseDir, this.serverEnv, this.serverScope) ?? ManifestStore.createCollector(this.options.baseDir);
105
134
  }
106
135
  get app() {
107
136
  return this.options.app;
@@ -382,17 +411,14 @@ var EggLoader = class {
382
411
  let pkg;
383
412
  let eggPluginConfig;
384
413
  const pluginPackage = path.join(plugin.path, "package.json");
385
- if (await utils_default.existsPath(pluginPackage)) {
386
- pkg = await readJSON(pluginPackage);
414
+ if (this.loaderFS.exists(pluginPackage)) {
415
+ pkg = await this.loaderFS.loadFile(pluginPackage);
387
416
  eggPluginConfig = pkg.eggPlugin;
388
417
  if (pkg.version) plugin.version = pkg.version;
389
418
  plugin.path = await this.#formatPluginPathFromPackageJSON(plugin.path, pkg);
390
419
  }
420
+ if (!eggPluginConfig) return;
391
421
  const logger = this.options.logger;
392
- if (!eggPluginConfig) {
393
- logger.warn("[@eggjs/core/egg_loader] pkg.eggPlugin is missing in %s, plugin: %j", pluginPackage, plugin);
394
- return;
395
- }
396
422
  if (eggPluginConfig.name && eggPluginConfig.strict !== false && eggPluginConfig.name !== plugin.name) logger.warn(`[@eggjs/core/egg_loader] pluginName(${plugin.name}) is different from pluginConfigName(${eggPluginConfig.name})`);
397
423
  depCompatible(eggPluginConfig);
398
424
  for (const key of [
@@ -455,10 +481,45 @@ var EggLoader = class {
455
481
  return lookupDirs;
456
482
  }
457
483
  getPluginPath(plugin) {
458
- if (plugin.path) return plugin.path;
484
+ if (plugin.path && !this.#isBundlePluginPathArtifact(plugin.path)) return plugin.path;
459
485
  if (plugin.package) assert(isValidatePackageName(plugin.package), `plugin ${plugin.name} invalid, use 'path' instead of package: "${plugin.package}"`);
486
+ if (plugin.path && this.#isBundlePluginPathArtifact(plugin.path)) return this.#resolveBundlePluginPath(plugin);
460
487
  return this.#resolvePluginPath(plugin);
461
488
  }
489
+ /**
490
+ * In bundle mode a plugin declared via `definePluginFactory({ path: import.meta.dirname })`
491
+ * carries a `path` rewritten by the bundler to the bundle output directory (= baseDir),
492
+ * which does not contain the plugin's own files. Detect that case so the plugin is
493
+ * re-resolved by package name instead.
494
+ */
495
+ #isBundlePluginPathArtifact(pluginPath) {
496
+ const bundleStore = ManifestStore.getBundleStore();
497
+ if (!bundleStore || path.resolve(bundleStore.baseDir) !== path.resolve(this.options.baseDir)) return false;
498
+ return path.resolve(pluginPath) === path.resolve(this.options.baseDir);
499
+ }
500
+ /**
501
+ * Re-resolve a bundle-artifact plugin path to the directory of the plugin package's entry
502
+ * module — the same directory `definePluginFactory` captured via `import.meta.dirname` at
503
+ * build time. Built-in framework plugins only carry a `name` (no `package`), so fall back to
504
+ * the conventional `@eggjs/<name>` package name in addition to the bare name.
505
+ */
506
+ #resolveBundlePluginPath(plugin) {
507
+ const candidates = plugin.package ? [plugin.package] : plugin.name.includes("/") ? [plugin.name] : [plugin.name, `@eggjs/${plugin.name}`];
508
+ let lastErr;
509
+ for (const name$1 of candidates) try {
510
+ const entry = utils_default.resolvePath(name$1, { paths: [...this.lookupDirs] });
511
+ const realDir = path.dirname(entry);
512
+ const segments = realDir.split(/[/\\]/);
513
+ const nmIdx = segments.lastIndexOf("node_modules");
514
+ if (nmIdx !== -1) return path.join(this.options.baseDir, ...segments.slice(nmIdx));
515
+ return realDir;
516
+ } catch (err) {
517
+ lastErr = err;
518
+ }
519
+ const name = plugin.package || plugin.name;
520
+ debug("[resolveBundlePluginPath] error: %o, plugin info: %o", lastErr, plugin);
521
+ throw new Error(`Can not find plugin ${name} in "${[...this.lookupDirs].join(", ")}"`, { cause: lastErr });
522
+ }
462
523
  #resolvePluginPath(plugin) {
463
524
  const name = plugin.package || plugin.name;
464
525
  try {
@@ -476,7 +537,7 @@ var EggLoader = class {
476
537
  if (isESM) {
477
538
  if (exports.import) realPluginPath = path.join(pluginPath, exports.import);
478
539
  } else if (exports.require) realPluginPath = path.join(pluginPath, exports.require);
479
- if (exports.typescript && isSupportTypeScript() && !await exists(realPluginPath)) {
540
+ if (exports.typescript && isSupportTypeScript() && !this.loaderFS.exists(realPluginPath)) {
480
541
  realPluginPath = path.join(pluginPath, exports.typescript);
481
542
  debug("[formatPluginPathFromPackageJSON] use typescript path %o", realPluginPath);
482
543
  }
@@ -742,14 +803,16 @@ var EggLoader = class {
742
803
  */
743
804
  async loadCustomApp() {
744
805
  await this.#loadBootHook("app");
745
- this.lifecycle.triggerConfigWillLoad();
806
+ if (this.options.metadataOnly) await this.lifecycle.triggerLoadMetadata();
807
+ else this.lifecycle.triggerConfigWillLoad();
746
808
  }
747
809
  /**
748
810
  * Load agent.js, same as {@link EggLoader#loadCustomApp}
749
811
  */
750
812
  async loadCustomAgent() {
751
813
  await this.#loadBootHook("agent");
752
- this.lifecycle.triggerConfigWillLoad();
814
+ if (this.options.metadataOnly) await this.lifecycle.triggerLoadMetadata();
815
+ else this.lifecycle.triggerConfigWillLoad();
753
816
  }
754
817
  loadBootHook() {}
755
818
  async #loadBootHook(fileName) {
@@ -1042,7 +1105,9 @@ var EggLoader = class {
1042
1105
  ...options,
1043
1106
  directory: options?.directory ?? directory,
1044
1107
  target,
1045
- inject: this.app
1108
+ inject: this.app,
1109
+ manifest: this.manifest,
1110
+ loaderFS: options?.loaderFS ?? this.loaderFS
1046
1111
  };
1047
1112
  const timingKey = `Load "${String(property)}" to Application`;
1048
1113
  this.timing.start(timingKey);
@@ -1061,7 +1126,9 @@ var EggLoader = class {
1061
1126
  ...options,
1062
1127
  directory: options?.directory || directory,
1063
1128
  property,
1064
- inject: this.app
1129
+ inject: this.app,
1130
+ manifest: this.manifest,
1131
+ loaderFS: options?.loaderFS ?? this.loaderFS
1065
1132
  };
1066
1133
  const timingKey = `Load "${String(property)}" to Context`;
1067
1134
  this.timing.start(timingKey);
@@ -1091,14 +1158,110 @@ var EggLoader = class {
1091
1158
  return files;
1092
1159
  }
1093
1160
  resolveModule(filepath) {
1161
+ return this.manifest.resolveModule(filepath, () => this.#doResolveModule(filepath));
1162
+ }
1163
+ #doResolveModule(filepath) {
1094
1164
  let fullPath;
1095
1165
  try {
1096
1166
  fullPath = utils_default.resolvePath(filepath);
1097
- } catch {
1098
- return;
1099
- }
1167
+ } catch {}
1168
+ if (!fullPath) fullPath = this.#resolveFromOutDir(filepath);
1100
1169
  return fullPath;
1101
1170
  }
1171
+ #resolveOutDir() {
1172
+ if (this.pkg.egg?.outDir) {
1173
+ debug("[resolveOutDir] use pkg.egg.outDir: %o", this.pkg.egg.outDir);
1174
+ return this.pkg.egg.outDir;
1175
+ }
1176
+ const tsConfigFile = path.join(this.options.baseDir, "tsconfig.json");
1177
+ if (fs.existsSync(tsConfigFile)) try {
1178
+ const tsConfig = JSON.parse(fs.readFileSync(tsConfigFile, "utf-8"));
1179
+ if (tsConfig.compilerOptions?.outDir) {
1180
+ debug("[resolveOutDir] use tsconfig.json compilerOptions.outDir: %o", tsConfig.compilerOptions.outDir);
1181
+ return tsConfig.compilerOptions.outDir;
1182
+ }
1183
+ } catch {}
1184
+ }
1185
+ #resolveFromOutDir(filepath) {
1186
+ if (!this.outDir) return;
1187
+ const baseDir = this.options.baseDir;
1188
+ if (!filepath.startsWith(baseDir + path.sep)) return;
1189
+ const relativePath = path.relative(baseDir, filepath);
1190
+ for (const ext of [".js", ".mjs"]) {
1191
+ const outDirPath = path.join(baseDir, this.outDir, relativePath + ext);
1192
+ if (fs.existsSync(outDirPath)) {
1193
+ debug("[resolveModule:outDir] %o => %o", filepath, outDirPath);
1194
+ return outDirPath;
1195
+ }
1196
+ }
1197
+ }
1198
+ /**
1199
+ * Generate startup manifest from collected data.
1200
+ * Should be called after all loading phases complete.
1201
+ */
1202
+ generateManifest() {
1203
+ const manifest = this.manifest.generateManifest({
1204
+ serverEnv: this.serverEnv,
1205
+ serverScope: this.serverScope,
1206
+ typescriptEnabled: isSupportTypeScript()
1207
+ });
1208
+ this.#collectConventionalDynamicFiles(manifest);
1209
+ return manifest;
1210
+ }
1211
+ /**
1212
+ * metadataOnly startup intentionally skips the agent process, but bundled
1213
+ * single-mode workers still load agent boot hooks and agent extends later.
1214
+ * Record convention-based dynamic entry points so the bundle can satisfy
1215
+ * those runtime lookups without running agent lifecycle hooks at manifest
1216
+ * generation time.
1217
+ */
1218
+ #collectConventionalDynamicFiles(manifest) {
1219
+ const resolveCacheTargets = new Set(Object.values(manifest.resolveCache).filter((target) => typeof target === "string"));
1220
+ for (const unit of this.getLoadUnits()) {
1221
+ this.#collectConventionFile(manifest, path.join(unit.path, "package.json"), resolveCacheTargets);
1222
+ for (const load of CONVENTIONAL_MANIFEST_LOADS) {
1223
+ const target = path.join(unit.path, ...load.path);
1224
+ if (load.type === "resolve") this.#collectConventionResolve(manifest, target, resolveCacheTargets);
1225
+ else if ("extensionlessResolve" in load && load.extensionlessResolve) this.#collectConventionFileResolves(manifest, target, resolveCacheTargets);
1226
+ else this.#collectConventionFileDiscovery(manifest, target);
1227
+ }
1228
+ }
1229
+ }
1230
+ #collectConventionResolve(manifest, request, resolveCacheTargets) {
1231
+ const requestKey = this.#toManifestRel(request);
1232
+ if (Object.hasOwn(manifest.resolveCache, requestKey)) return;
1233
+ const resolved = this.#doResolveModule(request);
1234
+ const resolvedKey = resolved ? this.#toManifestRel(resolved) : null;
1235
+ manifest.resolveCache[requestKey] = resolvedKey;
1236
+ if (resolvedKey !== null) resolveCacheTargets.add(resolvedKey);
1237
+ }
1238
+ #collectConventionFileResolves(manifest, directory, resolveCacheTargets) {
1239
+ const files = this.#collectConventionFileDiscovery(manifest, directory);
1240
+ for (const file of files) {
1241
+ const ext = path.extname(file);
1242
+ if (!ext) continue;
1243
+ const request = path.join(directory, file.slice(0, -ext.length));
1244
+ this.#collectConventionResolve(manifest, request, resolveCacheTargets);
1245
+ }
1246
+ }
1247
+ #collectConventionFileDiscovery(manifest, directory) {
1248
+ const dirKey = this.#toManifestRel(directory);
1249
+ if (Object.hasOwn(manifest.fileDiscovery, dirKey)) return manifest.fileDiscovery[dirKey];
1250
+ manifest.fileDiscovery[dirKey] = fs.existsSync(directory) && fs.statSync(directory).isDirectory() ? globby.sync(FileLoader.getDefaultMatch(), { cwd: directory }).sort() : [];
1251
+ return manifest.fileDiscovery[dirKey];
1252
+ }
1253
+ #collectConventionFile(manifest, filepath, resolveCacheTargets) {
1254
+ const fileKey = this.#toManifestRel(filepath);
1255
+ if (resolveCacheTargets.has(fileKey)) return;
1256
+ if (!fs.existsSync(filepath) || !fs.statSync(filepath).isFile()) return;
1257
+ const dirKey = this.#toManifestRel(path.dirname(filepath));
1258
+ const basename = path.basename(filepath);
1259
+ const files = manifest.fileDiscovery[dirKey] ?? [];
1260
+ if (!files.includes(basename)) manifest.fileDiscovery[dirKey] = [...files, basename].sort();
1261
+ }
1262
+ #toManifestRel(filepath) {
1263
+ return (path.isAbsolute(filepath) ? path.relative(this.options.baseDir, filepath) : filepath).replaceAll(path.sep, "/");
1264
+ }
1102
1265
  };
1103
1266
  function depCompatible(plugin) {
1104
1267
  if (plugin.dep && !(Array.isArray(plugin.dependencies) && plugin.dependencies.length > 0)) {
@@ -1,4 +1,5 @@
1
- import { Fun } from "../utils/index.js";
1
+ import { ManifestStore } from "./manifest.js";
2
+ import { LoaderFS } from "@eggjs/loader-fs";
2
3
 
3
4
  //#region src/loader/file_loader.d.ts
4
5
  declare const FULLPATH: unique symbol;
@@ -37,12 +38,17 @@ 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;
43
+ /** Loader-facing filesystem abstraction */
44
+ loaderFS?: LoaderFS;
40
45
  }
41
46
  interface FileLoaderParseItem {
42
47
  fullpath: string;
43
48
  properties: string[];
44
- exports: object | Fun;
49
+ exports: unknown;
45
50
  }
51
+ type NormalizedFileLoaderOptions = FileLoaderOptions & Required<Pick<FileLoaderOptions, "caseStyle" | "loaderFS">>;
46
52
  /**
47
53
  * Load files from directory to target object.
48
54
  * @since 1.0.0
@@ -50,7 +56,8 @@ interface FileLoaderParseItem {
50
56
  declare class FileLoader {
51
57
  static get FULLPATH(): typeof FULLPATH;
52
58
  static get EXPORTS(): typeof EXPORTS;
53
- readonly options: FileLoaderOptions & Required<Pick<FileLoaderOptions, "caseStyle">>;
59
+ static getDefaultMatch(): string[];
60
+ readonly options: NormalizedFileLoaderOptions;
54
61
  /**
55
62
  * @class
56
63
  * @param {Object} options - options
@@ -1,11 +1,10 @@
1
1
  import utils_default from "../utils/index.js";
2
- import fs from "node:fs";
3
2
  import path from "node:path";
4
3
  import { debuglog } from "node:util";
5
4
  import { isSupportTypeScript } from "@eggjs/utils";
6
5
  import assert from "node:assert";
7
6
  import { isAsyncFunction, isClass, isGeneratorFunction, isPrimitive } from "is-type-of";
8
- import globby from "globby";
7
+ import { RealLoaderFS } from "@eggjs/loader-fs";
9
8
 
10
9
  //#region src/loader/file_loader.ts
11
10
  const debug = debuglog("egg/core/file_loader");
@@ -16,17 +15,23 @@ const CaseStyle = {
16
15
  lower: "lower",
17
16
  upper: "upper"
18
17
  };
18
+ function getDefaultFileLoaderMatch() {
19
+ return isSupportTypeScript() ? ["**/*.(js|ts)", "!**/*.d.ts"] : ["**/*.js"];
20
+ }
19
21
  /**
20
22
  * Load files from directory to target object.
21
23
  * @since 1.0.0
22
24
  */
23
- var FileLoader = class {
25
+ var FileLoader = class FileLoader {
24
26
  static get FULLPATH() {
25
27
  return FULLPATH;
26
28
  }
27
29
  static get EXPORTS() {
28
30
  return EXPORTS;
29
31
  }
32
+ static getDefaultMatch() {
33
+ return getDefaultFileLoaderMatch();
34
+ }
30
35
  options;
31
36
  /**
32
37
  * @class
@@ -45,10 +50,12 @@ var FileLoader = class {
45
50
  constructor(options) {
46
51
  assert(options.directory, "options.directory is required");
47
52
  assert(options.target, "options.target is required");
53
+ if (!options.manifest && options.inject) options.manifest = options.inject.loader?.manifest;
48
54
  this.options = {
49
55
  caseStyle: CaseStyle.camel,
50
56
  call: true,
51
57
  override: false,
58
+ loaderFS: new RealLoaderFS(),
52
59
  ...options
53
60
  };
54
61
  if (this.options.lowercaseFirst === true) {
@@ -114,7 +121,7 @@ var FileLoader = class {
114
121
  async parse() {
115
122
  let files = this.options.match;
116
123
  if (files) files = Array.isArray(files) ? files : [files];
117
- else files = isSupportTypeScript() ? ["**/*.(js|ts)", "!**/*.d.ts"] : ["**/*.js"];
124
+ else files = FileLoader.getDefaultMatch();
118
125
  let ignore = this.options.ignore;
119
126
  if (ignore) {
120
127
  ignore = Array.isArray(ignore) ? ignore : [ignore];
@@ -127,11 +134,12 @@ var FileLoader = class {
127
134
  const items = [];
128
135
  debug("[parse] parsing directories: %j", directories);
129
136
  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);
137
+ const manifest = this.options.manifest;
138
+ const filepaths = manifest ? manifest.globFiles(directory, () => this.options.loaderFS.glob(files, { cwd: directory })) : this.options.loaderFS.glob(files, { cwd: directory });
139
+ debug("[parse] files: %o, cwd: %o => %o", files, directory, filepaths);
132
140
  for (const filepath of filepaths) {
133
141
  const fullpath = path.join(directory, filepath);
134
- if (!fs.statSync(fullpath).isFile()) continue;
142
+ if (!this.options.loaderFS.stat(fullpath).isFile()) continue;
135
143
  if (filepath.endsWith(".js")) {
136
144
  const filepathTs = filepath.replace(/\.js$/, ".ts");
137
145
  if (filepaths.includes(filepathTs)) {
@@ -167,7 +175,7 @@ function getProperties(filepath, caseStyle) {
167
175
  return defaultCamelize(filepath, caseStyle);
168
176
  }
169
177
  async function getExports(fullpath, options, pathName) {
170
- let exports = await utils_default.loadFile(fullpath);
178
+ let exports = await options.loaderFS.loadFile(fullpath);
171
179
  if (options.initializer) {
172
180
  exports = options.initializer(exports, {
173
181
  path: fullpath,
@@ -0,0 +1,17 @@
1
+ import { ManifestStore } from "./manifest.js";
2
+ import { Stats } from "node:fs";
3
+ import { LoaderFS as LoaderFS$1, LoaderFSGlobOptions } from "@eggjs/loader-fs";
4
+
5
+ //#region src/loader/loader_fs.d.ts
6
+ declare class ManifestLoaderFS implements LoaderFS$1 {
7
+ #private;
8
+ constructor(manifest: ManifestStore, fallback?: LoaderFS$1);
9
+ exists(filepath: string): boolean;
10
+ stat(filepath: string): Stats;
11
+ realpath(filepath: string): string;
12
+ readJSON<T = unknown>(filepath: string): T;
13
+ glob(patterns: string | string[], options?: LoaderFSGlobOptions): string[];
14
+ loadFile(filepath: string): Promise<unknown>;
15
+ }
16
+ //#endregion
17
+ export { type LoaderFS$1 as LoaderFS, ManifestLoaderFS };