@eggjs/core 6.5.0 → 6.6.0-beta.2

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.
Files changed (87) hide show
  1. package/README.md +1 -5
  2. package/dist/base_context_class.d.ts +21 -0
  3. package/dist/base_context_class.js +40 -0
  4. package/dist/egg.d.ts +248 -0
  5. package/dist/egg.js +358 -0
  6. package/dist/index.d.ts +12 -0
  7. package/dist/index.js +12 -0
  8. package/dist/lifecycle.d.ts +84 -0
  9. package/dist/lifecycle.js +280 -0
  10. package/dist/loader/context_loader.d.ts +39 -0
  11. package/dist/loader/context_loader.js +79 -0
  12. package/dist/loader/egg_loader.d.ts +374 -0
  13. package/dist/loader/egg_loader.js +1184 -0
  14. package/dist/loader/file_loader.d.ts +105 -0
  15. package/dist/loader/file_loader.js +198 -0
  16. package/dist/singleton.d.ts +33 -0
  17. package/dist/singleton.js +107 -0
  18. package/{src/types.ts → dist/types.d.ts} +7 -7
  19. package/dist/utils/index.d.ts +19 -0
  20. package/dist/utils/index.js +103 -0
  21. package/dist/utils/sequencify.d.ts +16 -0
  22. package/dist/utils/sequencify.js +46 -0
  23. package/dist/utils/timing.d.ts +24 -0
  24. package/dist/utils/timing.js +85 -0
  25. package/package.json +34 -72
  26. package/dist/commonjs/base_context_class.d.ts +0 -16
  27. package/dist/commonjs/base_context_class.js +0 -41
  28. package/dist/commonjs/egg.d.ts +0 -246
  29. package/dist/commonjs/egg.js +0 -401
  30. package/dist/commonjs/index.d.ts +0 -12
  31. package/dist/commonjs/index.js +0 -32
  32. package/dist/commonjs/lifecycle.d.ts +0 -78
  33. package/dist/commonjs/lifecycle.js +0 -315
  34. package/dist/commonjs/loader/context_loader.d.ts +0 -35
  35. package/dist/commonjs/loader/context_loader.js +0 -110
  36. package/dist/commonjs/loader/egg_loader.d.ts +0 -369
  37. package/dist/commonjs/loader/egg_loader.js +0 -1558
  38. package/dist/commonjs/loader/file_loader.d.ts +0 -100
  39. package/dist/commonjs/loader/file_loader.js +0 -260
  40. package/dist/commonjs/package.json +0 -3
  41. package/dist/commonjs/singleton.d.ts +0 -29
  42. package/dist/commonjs/singleton.js +0 -124
  43. package/dist/commonjs/types.d.ts +0 -53
  44. package/dist/commonjs/types.js +0 -3
  45. package/dist/commonjs/utils/index.d.ts +0 -17
  46. package/dist/commonjs/utils/index.js +0 -117
  47. package/dist/commonjs/utils/sequencify.d.ts +0 -13
  48. package/dist/commonjs/utils/sequencify.js +0 -64
  49. package/dist/commonjs/utils/timing.d.ts +0 -21
  50. package/dist/commonjs/utils/timing.js +0 -106
  51. package/dist/esm/base_context_class.d.ts +0 -16
  52. package/dist/esm/base_context_class.js +0 -37
  53. package/dist/esm/egg.d.ts +0 -246
  54. package/dist/esm/egg.js +0 -388
  55. package/dist/esm/index.d.ts +0 -12
  56. package/dist/esm/index.js +0 -12
  57. package/dist/esm/lifecycle.d.ts +0 -78
  58. package/dist/esm/lifecycle.js +0 -308
  59. package/dist/esm/loader/context_loader.d.ts +0 -35
  60. package/dist/esm/loader/context_loader.js +0 -102
  61. package/dist/esm/loader/egg_loader.d.ts +0 -369
  62. package/dist/esm/loader/egg_loader.js +0 -1551
  63. package/dist/esm/loader/file_loader.d.ts +0 -100
  64. package/dist/esm/loader/file_loader.js +0 -253
  65. package/dist/esm/package.json +0 -3
  66. package/dist/esm/singleton.d.ts +0 -29
  67. package/dist/esm/singleton.js +0 -117
  68. package/dist/esm/types.d.ts +0 -53
  69. package/dist/esm/types.js +0 -2
  70. package/dist/esm/utils/index.d.ts +0 -17
  71. package/dist/esm/utils/index.js +0 -112
  72. package/dist/esm/utils/sequencify.d.ts +0 -13
  73. package/dist/esm/utils/sequencify.js +0 -61
  74. package/dist/esm/utils/timing.d.ts +0 -21
  75. package/dist/esm/utils/timing.js +0 -99
  76. package/dist/package.json +0 -4
  77. package/src/base_context_class.ts +0 -39
  78. package/src/egg.ts +0 -617
  79. package/src/index.ts +0 -14
  80. package/src/lifecycle.ts +0 -438
  81. package/src/loader/context_loader.ts +0 -123
  82. package/src/loader/egg_loader.ts +0 -1984
  83. package/src/loader/file_loader.ts +0 -349
  84. package/src/singleton.ts +0 -187
  85. package/src/utils/index.ts +0 -132
  86. package/src/utils/sequencify.ts +0 -105
  87. package/src/utils/timing.ts +0 -128
@@ -1,1984 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import assert from 'node:assert';
4
- import { debuglog, inspect } from 'node:util';
5
-
6
- import { homedir } from 'node-homedir';
7
- import {
8
- isAsyncFunction,
9
- isClass,
10
- isGeneratorFunction,
11
- isObject,
12
- isPromise,
13
- } from 'is-type-of';
14
- import type { Logger } from 'egg-logger';
15
- import { getParamNames, readJSONSync, readJSON, exists } from 'utility';
16
- import { extend } from 'extend2';
17
- import {
18
- Request,
19
- Response,
20
- Application,
21
- Context as KoaContext,
22
- } from '@eggjs/koa';
23
- import { register as tsconfigPathsRegister } from 'tsconfig-paths';
24
- import { isESM, isSupportTypeScript } from '@eggjs/utils';
25
- import { pathMatching, type PathMatchingOptions } from 'egg-path-matching';
26
- import { now, diff } from 'performance-ms';
27
-
28
- import {
29
- type FileLoaderOptions,
30
- CaseStyle,
31
- FULLPATH,
32
- FileLoader,
33
- } from './file_loader.js';
34
- import { type ContextLoaderOptions, ContextLoader } from './context_loader.js';
35
- import utils, { type Fun } from '../utils/index.js';
36
- import { sequencify } from '../utils/sequencify.js';
37
- import { Timing } from '../utils/timing.js';
38
- import type { Context, EggCore, MiddlewareFunc } from '../egg.js';
39
- import type { BaseContextClass } from '../base_context_class.js';
40
- import type { EggAppConfig, EggAppInfo, EggPluginInfo } from '../types.js';
41
-
42
- const debug = debuglog('@eggjs/core/loader/egg_loader');
43
-
44
- const originalPrototypes: Record<string, unknown> = {
45
- request: Request.prototype,
46
- response: Response.prototype,
47
- context: KoaContext.prototype,
48
- application: Application.prototype,
49
- };
50
-
51
- export interface EggLoaderOptions {
52
- /** server env */
53
- env: string;
54
- /** Application instance */
55
- app: EggCore;
56
- EggCoreClass?: typeof EggCore;
57
- /** the directory of application */
58
- baseDir: string;
59
- /** egg logger */
60
- logger: Logger;
61
- /** server scope */
62
- serverScope?: string;
63
- /** custom plugins */
64
- plugins?: Record<string, EggPluginInfo>;
65
- }
66
-
67
- export type EggDirInfoType = 'app' | 'plugin' | 'framework';
68
-
69
- export interface EggDirInfo {
70
- path: string;
71
- type: EggDirInfoType;
72
- }
73
-
74
- export class EggLoader {
75
- #requiredCount = 0;
76
- readonly options: EggLoaderOptions;
77
- readonly timing: Timing;
78
- readonly pkg: Record<string, any>;
79
- readonly eggPaths: string[];
80
- readonly serverEnv: string;
81
- readonly serverScope: string;
82
- readonly appInfo: EggAppInfo;
83
- dirs?: EggDirInfo[];
84
-
85
- /**
86
- * @class
87
- * @param {Object} options - options
88
- * @param {String} options.baseDir - the directory of application
89
- * @param {EggCore} options.app - Application instance
90
- * @param {Logger} options.logger - logger
91
- * @param {Object} [options.plugins] - custom plugins
92
- * @since 1.0.0
93
- */
94
- constructor(options: EggLoaderOptions) {
95
- this.options = options;
96
- assert(
97
- fs.existsSync(this.options.baseDir),
98
- `${this.options.baseDir} not exists`
99
- );
100
- assert(this.options.app, 'options.app is required');
101
- assert(this.options.logger, 'options.logger is required');
102
-
103
- this.timing = this.app.timing || new Timing();
104
-
105
- /**
106
- * @member {Object} EggLoader#pkg
107
- * @see {@link AppInfo#pkg}
108
- * @since 1.0.0
109
- */
110
- this.pkg = readJSONSync(path.join(this.options.baseDir, 'package.json'));
111
-
112
- // auto require('tsconfig-paths/register') on typescript app
113
- // support env.EGG_TYPESCRIPT = true or { "egg": { "typescript": true } } on package.json
114
- if (
115
- process.env.EGG_TYPESCRIPT === 'true' ||
116
- (this.pkg.egg && this.pkg.egg.typescript)
117
- ) {
118
- // skip require tsconfig-paths if tsconfig.json not exists
119
- const tsConfigFile = path.join(this.options.baseDir, 'tsconfig.json');
120
- if (fs.existsSync(tsConfigFile)) {
121
- // @ts-expect-error only cwd is required
122
- tsconfigPathsRegister({ cwd: this.options.baseDir });
123
- } else {
124
- this.logger.info(
125
- '[@eggjs/core/egg_loader] skip register "tsconfig-paths" because tsconfig.json not exists at %s',
126
- tsConfigFile
127
- );
128
- }
129
- }
130
-
131
- /**
132
- * All framework directories.
133
- *
134
- * You can extend Application of egg, the entry point is options.app,
135
- *
136
- * loader will find all directories from the prototype of Application,
137
- * you should define `Symbol.for('egg#eggPath')` property.
138
- *
139
- * ```ts
140
- * // src/example.ts
141
- * import { Application } from 'egg';
142
- * class ExampleApplication extends Application {
143
- * get [Symbol.for('egg#eggPath')]() {
144
- * return baseDir;
145
- * }
146
- * }
147
- * ```
148
- * @member {Array} EggLoader#eggPaths
149
- * @see EggLoader#getEggPaths
150
- * @since 1.0.0
151
- */
152
- this.eggPaths = this.getEggPaths();
153
- debug('Loaded eggPaths %j', this.eggPaths);
154
-
155
- /**
156
- * @member {String} EggLoader#serverEnv
157
- * @see AppInfo#env
158
- * @since 1.0.0
159
- */
160
- this.serverEnv = this.getServerEnv();
161
- debug('Loaded serverEnv %j', this.serverEnv);
162
-
163
- /**
164
- * @member {String} EggLoader#serverScope
165
- * @see AppInfo#serverScope
166
- */
167
- this.serverScope = options.serverScope ?? this.getServerScope();
168
-
169
- /**
170
- * @member {AppInfo} EggLoader#appInfo
171
- * @since 1.0.0
172
- */
173
- this.appInfo = this.getAppInfo();
174
- }
175
-
176
- get app() {
177
- return this.options.app;
178
- }
179
-
180
- get lifecycle() {
181
- return this.app.lifecycle;
182
- }
183
-
184
- get logger() {
185
- return this.options.logger;
186
- }
187
-
188
- /**
189
- * Get {@link AppInfo#env}
190
- * @returns {String} env
191
- * @see AppInfo#env
192
- * @private
193
- * @since 1.0.0
194
- */
195
- protected getServerEnv(): string {
196
- let serverEnv = this.options.env;
197
-
198
- const envPath = path.join(this.options.baseDir, 'config/env');
199
- if (!serverEnv && fs.existsSync(envPath)) {
200
- serverEnv = fs.readFileSync(envPath, 'utf8').trim();
201
- }
202
-
203
- if (!serverEnv && process.env.EGG_SERVER_ENV) {
204
- serverEnv = process.env.EGG_SERVER_ENV;
205
- }
206
-
207
- if (serverEnv) {
208
- serverEnv = serverEnv.trim();
209
- } else {
210
- // oxlint-disable-next-line eslint/no-lonely-if
211
- if (process.env.NODE_ENV === 'test') {
212
- serverEnv = 'unittest';
213
- } else if (process.env.NODE_ENV === 'production') {
214
- serverEnv = 'prod';
215
- } else {
216
- serverEnv = 'local';
217
- }
218
- }
219
-
220
- return serverEnv;
221
- }
222
-
223
- /**
224
- * Get {@link AppInfo#scope}
225
- * @returns {String} serverScope
226
- * @private
227
- */
228
- protected getServerScope(): string {
229
- return process.env.EGG_SERVER_SCOPE ?? '';
230
- }
231
-
232
- /**
233
- * Get {@link AppInfo#name}
234
- * @returns {String} appname
235
- * @private
236
- * @since 1.0.0
237
- */
238
- getAppname(): string {
239
- if (this.pkg.name) {
240
- debug('Loaded appname(%s) from package.json', this.pkg.name);
241
- return this.pkg.name;
242
- }
243
- const pkg = path.join(this.options.baseDir, 'package.json');
244
- throw new Error(`name is required from ${pkg}`);
245
- }
246
-
247
- /**
248
- * Get home directory
249
- * @returns {String} home directory
250
- * @since 3.4.0
251
- */
252
- getHomedir(): string {
253
- // EGG_HOME for test
254
- return process.env.EGG_HOME || homedir() || '/home/admin';
255
- }
256
-
257
- /**
258
- * Get app info
259
- * @returns {AppInfo} appInfo
260
- * @since 1.0.0
261
- */
262
- protected getAppInfo(): EggAppInfo {
263
- const env = this.serverEnv;
264
- const scope = this.serverScope;
265
- const home = this.getHomedir();
266
- const baseDir = this.options.baseDir;
267
-
268
- /**
269
- * Meta information of the application
270
- * @class AppInfo
271
- */
272
- return {
273
- /**
274
- * The name of the application, retrieve from the name property in `package.json`.
275
- * @member {String} AppInfo#name
276
- */
277
- name: this.getAppname(),
278
-
279
- /**
280
- * The current directory, where the application code is.
281
- * @member {String} AppInfo#baseDir
282
- */
283
- baseDir,
284
-
285
- /**
286
- * The environment of the application, **it's not NODE_ENV**
287
- *
288
- * 1. from `$baseDir/config/env`
289
- * 2. from EGG_SERVER_ENV
290
- * 3. from NODE_ENV
291
- *
292
- * env | description
293
- * --- | ---
294
- * test | system integration testing
295
- * prod | production
296
- * local | local on your own computer
297
- * unittest | unit test
298
- *
299
- * @member {String} AppInfo#env
300
- * @see https://eggjs.org/zh-cn/basics/env.html
301
- */
302
- env,
303
-
304
- /**
305
- * @member {String} AppInfo#scope
306
- */
307
- scope,
308
-
309
- /**
310
- * The use directory, same as `process.env.HOME`
311
- * @member {String} AppInfo#HOME
312
- */
313
- HOME: home,
314
-
315
- /**
316
- * parsed from `package.json`
317
- * @member {Object} AppInfo#pkg
318
- */
319
- pkg: this.pkg,
320
-
321
- /**
322
- * The directory whether is baseDir or HOME depend on env.
323
- * it's good for test when you want to write some file to HOME,
324
- * but don't want to write to the real directory,
325
- * so use root to write file to baseDir instead of HOME when unittest.
326
- * keep root directory in baseDir when local and unittest
327
- * @member {String} AppInfo#root
328
- */
329
- root: env === 'local' || env === 'unittest' ? baseDir : home,
330
- };
331
- }
332
-
333
- /**
334
- * Get {@link EggLoader#eggPaths}
335
- * @returns {Array} framework directories
336
- * @see {@link EggLoader#eggPaths}
337
- * @private
338
- * @since 1.0.0
339
- */
340
- protected getEggPaths(): string[] {
341
- // avoid require recursively
342
- const EggCore = this.options.EggCoreClass;
343
- const eggPaths: string[] = [];
344
-
345
- let proto = this.app;
346
-
347
- // Loop for the prototype chain
348
- while (proto) {
349
- proto = Object.getPrototypeOf(proto);
350
- // stop the loop if
351
- // - object extends Object
352
- // - object extends EggCore
353
- if (proto === Object.prototype || proto === EggCore?.prototype) {
354
- break;
355
- }
356
- const eggPath = Reflect.get(proto, Symbol.for('egg#eggPath'));
357
- if (!eggPath) {
358
- // if (EggCore) {
359
- // throw new TypeError('Symbol.for(\'egg#eggPath\') is required on Application');
360
- // }
361
- continue;
362
- }
363
- assert(
364
- typeof eggPath === 'string',
365
- "Symbol.for('egg#eggPath') should be string"
366
- );
367
- assert(fs.existsSync(eggPath), `${eggPath} not exists`);
368
- const realpath = fs.realpathSync(eggPath);
369
- if (!eggPaths.includes(realpath)) {
370
- eggPaths.unshift(realpath);
371
- }
372
- }
373
- return eggPaths;
374
- }
375
-
376
- /** start Plugin loader */
377
- lookupDirs: Set<string>;
378
- eggPlugins: Record<string, EggPluginInfo>;
379
- appPlugins: Record<string, EggPluginInfo>;
380
- customPlugins: Record<string, EggPluginInfo>;
381
- allPlugins: Record<string, EggPluginInfo>;
382
- orderPlugins: EggPluginInfo[];
383
- /** enable plugins */
384
- plugins: Record<string, EggPluginInfo>;
385
-
386
- /**
387
- * Load config/plugin.js from {EggLoader#loadUnits}
388
- *
389
- * plugin.js is written below
390
- *
391
- * ```js
392
- * {
393
- * 'xxx-client': {
394
- * enable: true,
395
- * package: 'xxx-client',
396
- * dep: [],
397
- * env: [],
398
- * },
399
- * // short hand
400
- * 'rds': false,
401
- * 'depd': {
402
- * enable: true,
403
- * path: 'path/to/depd'
404
- * }
405
- * }
406
- * ```
407
- *
408
- * If the plugin has path, Loader will find the module from it.
409
- *
410
- * Otherwise Loader will lookup follow the order by packageName
411
- *
412
- * 1. $APP_BASE/node_modules/${package}
413
- * 2. $EGG_BASE/node_modules/${package}
414
- *
415
- * You can call `loader.plugins` that retrieve enabled plugins.
416
- *
417
- * ```js
418
- * loader.plugins['xxx-client'] = {
419
- * name: 'xxx-client', // the plugin name, it can be used in `dep`
420
- * package: 'xxx-client', // the package name of plugin
421
- * enable: true, // whether enabled
422
- * path: 'path/to/xxx-client', // the directory of the plugin package
423
- * dep: [], // the dependent plugins, you can use the plugin name
424
- * env: [ 'local', 'unittest' ], // specify the serverEnv that only enable the plugin in it
425
- * }
426
- * ```
427
- *
428
- * `loader.allPlugins` can be used when retrieve all plugins.
429
- * @function EggLoader#loadPlugin
430
- * @since 1.0.0
431
- */
432
- async loadPlugin() {
433
- this.timing.start('Load Plugin');
434
-
435
- this.lookupDirs = this.getLookupDirs();
436
- this.allPlugins = {};
437
- this.eggPlugins = await this.loadEggPlugins();
438
- this.appPlugins = await this.loadAppPlugins();
439
- this.customPlugins = this.loadCustomPlugins();
440
-
441
- this.#extendPlugins(this.allPlugins, this.eggPlugins);
442
- this.#extendPlugins(this.allPlugins, this.appPlugins);
443
- this.#extendPlugins(this.allPlugins, this.customPlugins);
444
-
445
- const enabledPluginNames: string[] = []; // enabled plugins that configured explicitly
446
- const plugins: Record<string, EggPluginInfo> = {};
447
- const env = this.serverEnv;
448
- for (const name in this.allPlugins) {
449
- const plugin = this.allPlugins[name];
450
-
451
- // resolve the real plugin.path based on plugin or package
452
- plugin.path = this.getPluginPath(plugin);
453
-
454
- // read plugin information from ${plugin.path}/package.json
455
- await this.#mergePluginConfig(plugin);
456
-
457
- // disable the plugin that not match the serverEnv
458
- if (env && plugin.env.length > 0 && !plugin.env.includes(env)) {
459
- this.logger.info(
460
- '[@eggjs/core] Plugin %o is disabled by env unmatched, require env(%o) but got env is %o',
461
- name,
462
- plugin.env,
463
- env
464
- );
465
- plugin.enable = false;
466
- continue;
467
- }
468
-
469
- plugins[name] = plugin;
470
- if (plugin.enable) {
471
- enabledPluginNames.push(name);
472
- }
473
- }
474
-
475
- // retrieve the ordered plugins
476
- this.orderPlugins = this.getOrderPlugins(
477
- plugins,
478
- enabledPluginNames,
479
- this.appPlugins
480
- );
481
-
482
- const enablePlugins: Record<string, EggPluginInfo> = {};
483
- for (const plugin of this.orderPlugins) {
484
- enablePlugins[plugin.name] = plugin;
485
- }
486
- debug('Loaded enable plugins: %j', Object.keys(enablePlugins));
487
-
488
- /**
489
- * Retrieve enabled plugins
490
- * @member {Object} EggLoader#plugins
491
- * @since 1.0.0
492
- */
493
- this.plugins = enablePlugins;
494
- this.timing.end('Load Plugin');
495
- }
496
-
497
- protected async loadAppPlugins() {
498
- // loader plugins from application
499
- const appPlugins = await this.readPluginConfigs(
500
- path.join(this.options.baseDir, 'config/plugin.default')
501
- );
502
- debug(
503
- 'Loaded app plugins: %j',
504
- Object.keys(appPlugins).map(k => `${k}:${appPlugins[k].enable}`)
505
- );
506
- return appPlugins;
507
- }
508
-
509
- protected async loadEggPlugins() {
510
- // loader plugins from framework
511
- const eggPluginConfigPaths = this.eggPaths.map(eggPath =>
512
- path.join(eggPath, 'config/plugin.default')
513
- );
514
- const eggPlugins = await this.readPluginConfigs(eggPluginConfigPaths);
515
- debug(
516
- 'Loaded egg plugins: %j',
517
- Object.keys(eggPlugins).map(k => `${k}:${eggPlugins[k].enable}`)
518
- );
519
- return eggPlugins;
520
- }
521
-
522
- protected loadCustomPlugins() {
523
- // loader plugins from process.env.EGG_PLUGINS
524
- let customPlugins: Record<string, EggPluginInfo> = {};
525
- const configPaths: string[] = [];
526
- if (process.env.EGG_PLUGINS) {
527
- try {
528
- customPlugins = JSON.parse(process.env.EGG_PLUGINS);
529
- configPaths.push('<process.env.EGG_PLUGINS>');
530
- } catch (e) {
531
- debug('parse EGG_PLUGINS failed, %s', e);
532
- }
533
- }
534
-
535
- // loader plugins from options.plugins
536
- if (this.options.plugins) {
537
- customPlugins = {
538
- ...customPlugins,
539
- ...this.options.plugins,
540
- };
541
- configPaths.push('<options.plugins>');
542
- }
543
-
544
- if (customPlugins) {
545
- const configPath = configPaths.join(' or ');
546
- for (const name in customPlugins) {
547
- this.#normalizePluginConfig(customPlugins, name, configPath);
548
- }
549
- debug('Loaded custom plugins: %o', customPlugins);
550
- }
551
- return customPlugins;
552
- }
553
-
554
- /*
555
- * Read plugin.js from multiple directory
556
- */
557
- protected async readPluginConfigs(configPaths: string[] | string) {
558
- if (!Array.isArray(configPaths)) {
559
- configPaths = [configPaths];
560
- }
561
-
562
- // Get all plugin configurations
563
- // plugin.default.js
564
- // plugin.${scope}.js
565
- // plugin.${env}.js
566
- // plugin.${scope}_${env}.js
567
- const newConfigPaths: string[] = [];
568
- for (const filename of this.getTypeFiles('plugin')) {
569
- for (let configPath of configPaths) {
570
- configPath = path.join(path.dirname(configPath), filename);
571
- newConfigPaths.push(configPath);
572
- }
573
- }
574
-
575
- const plugins: Record<string, EggPluginInfo> = {};
576
- for (const configPath of newConfigPaths) {
577
- let filepath = this.resolveModule(configPath);
578
-
579
- // let plugin.js compatible
580
- if (configPath.endsWith('plugin.default') && !filepath) {
581
- filepath = this.resolveModule(
582
- configPath.replace(/plugin\.default$/, 'plugin')
583
- );
584
- }
585
-
586
- if (!filepath) {
587
- continue;
588
- }
589
-
590
- const config = (await utils.loadFile(filepath)) as Record<
591
- string,
592
- EggPluginInfo
593
- >;
594
- for (const name in config) {
595
- this.#normalizePluginConfig(config, name, filepath);
596
- }
597
- this.#extendPlugins(plugins, config);
598
- }
599
-
600
- return plugins;
601
- }
602
-
603
- #normalizePluginConfig(
604
- plugins: Record<string, EggPluginInfo | boolean>,
605
- name: string,
606
- configPath: string
607
- ) {
608
- const plugin = plugins[name];
609
-
610
- // plugin_name: false
611
- if (typeof plugin === 'boolean') {
612
- plugins[name] = {
613
- name,
614
- enable: plugin,
615
- dependencies: [],
616
- optionalDependencies: [],
617
- env: [],
618
- from: configPath,
619
- } satisfies EggPluginInfo;
620
- return;
621
- }
622
-
623
- if (!('enable' in plugin)) {
624
- Reflect.set(plugin, 'enable', true);
625
- }
626
- plugin.name = name;
627
- plugin.dependencies = plugin.dependencies || [];
628
- plugin.optionalDependencies = plugin.optionalDependencies || [];
629
- plugin.env = plugin.env || [];
630
- plugin.from = plugin.from || configPath;
631
- depCompatible(plugin);
632
- }
633
-
634
- // Read plugin information from package.json and merge
635
- // {
636
- // eggPlugin: {
637
- // "name": "", plugin name, must be same as name in config/plugin.js
638
- // "dep": [], dependent plugins
639
- // "env": "" env
640
- // "strict": true, whether check plugin name, default to true.
641
- // }
642
- // }
643
- async #mergePluginConfig(plugin: EggPluginInfo) {
644
- let pkg;
645
- let config;
646
- const pluginPackage = path.join(plugin.path as string, 'package.json');
647
- if (await utils.existsPath(pluginPackage)) {
648
- pkg = await readJSON(pluginPackage);
649
- config = pkg.eggPlugin;
650
- if (pkg.version) {
651
- plugin.version = pkg.version;
652
- }
653
- // support commonjs and esm dist files
654
- plugin.path = await this.#formatPluginPathFromPackageJSON(
655
- plugin.path as string,
656
- pkg
657
- );
658
- }
659
-
660
- const logger = this.options.logger;
661
- if (!config) {
662
- logger.warn(
663
- `[@eggjs/core/egg_loader] pkg.eggPlugin is missing in ${pluginPackage}`
664
- );
665
- return;
666
- }
667
-
668
- if (config.name && config.strict !== false && config.name !== plugin.name) {
669
- // pluginName is configured in config/plugin.js
670
- // pluginConfigName is pkg.eggPlugin.name
671
- logger.warn(
672
- `[@eggjs/core/egg_loader] pluginName(${plugin.name}) is different from pluginConfigName(${config.name})`
673
- );
674
- }
675
-
676
- // dep compatible
677
- depCompatible(config);
678
-
679
- for (const key of ['dependencies', 'optionalDependencies', 'env']) {
680
- const values = config[key];
681
- const existsValues = Reflect.get(plugin, key);
682
- if (Array.isArray(values) && !existsValues?.length) {
683
- Reflect.set(plugin, key, values);
684
- }
685
- }
686
- }
687
-
688
- protected getOrderPlugins(
689
- allPlugins: Record<string, EggPluginInfo>,
690
- enabledPluginNames: string[],
691
- appPlugins: Record<string, EggPluginInfo>
692
- ) {
693
- // no plugins enabled
694
- if (enabledPluginNames.length === 0) {
695
- return [];
696
- }
697
-
698
- const result = sequencify(allPlugins, enabledPluginNames);
699
- debug('Got plugins %j after sequencify', result);
700
-
701
- // catch error when result.sequence is empty
702
- if (result.sequence.length === 0) {
703
- const err = new Error(
704
- `sequencify plugins has problem, missing: [${result.missingTasks}], recursive: [${result.recursiveDependencies}]`
705
- );
706
- // find plugins which is required by the missing plugin
707
- for (const missName of result.missingTasks) {
708
- const requires = [];
709
- for (const name in allPlugins) {
710
- if (allPlugins[name].dependencies.includes(missName)) {
711
- requires.push(name);
712
- }
713
- }
714
- err.message += `\n\t>> Plugin [${missName}] is disabled or missed, but is required by [${requires}]`;
715
- }
716
-
717
- err.name = 'PluginSequencifyError';
718
- throw err;
719
- }
720
-
721
- // log the plugins that be enabled implicitly
722
- const implicitEnabledPlugins: string[] = [];
723
- const requireMap: Record<string, string[]> = {};
724
- for (const name of result.sequence) {
725
- for (const depName of allPlugins[name].dependencies) {
726
- if (!requireMap[depName]) {
727
- requireMap[depName] = [];
728
- }
729
- requireMap[depName].push(name);
730
- }
731
-
732
- if (!allPlugins[name].enable) {
733
- implicitEnabledPlugins.push(name);
734
- allPlugins[name].enable = true;
735
- allPlugins[name].implicitEnable = true;
736
- }
737
- }
738
-
739
- for (const [name, dependents] of Object.entries(requireMap)) {
740
- // note:`dependents` will not includes `optionalDependencies`
741
- allPlugins[name].dependents = dependents;
742
- }
743
-
744
- // Following plugins will be enabled implicitly.
745
- // - configclient required by [rpcClient]
746
- // - monitor required by [rpcClient]
747
- // - diamond required by [rpcClient]
748
- if (implicitEnabledPlugins.length > 0) {
749
- let message = implicitEnabledPlugins
750
- .map(name => ` - ${name} required by [${requireMap[name]}]`)
751
- .join('\n');
752
- this.options.logger.info(
753
- `Following plugins will be enabled implicitly.\n${message}`
754
- );
755
-
756
- // should warn when the plugin is disabled by app
757
- const disabledPlugins = implicitEnabledPlugins.filter(
758
- name => appPlugins[name] && appPlugins[name].enable === false
759
- );
760
- if (disabledPlugins.length > 0) {
761
- message = disabledPlugins
762
- .map(name => ` - ${name} required by [${requireMap[name]}]`)
763
- .join('\n');
764
- this.options.logger.warn(
765
- `Following plugins will be enabled implicitly that is disabled by application.\n${message}`
766
- );
767
- }
768
- }
769
-
770
- return result.sequence.map(name => allPlugins[name]);
771
- }
772
-
773
- protected getLookupDirs() {
774
- const lookupDirs = new Set<string>();
775
-
776
- // try to locate the plugin in the following directories's node_modules
777
- // -> {APP_PATH} -> {EGG_PATH} -> $CWD
778
- lookupDirs.add(this.options.baseDir);
779
-
780
- // try to locate the plugin at framework from upper to lower
781
- for (let i = this.eggPaths.length - 1; i >= 0; i--) {
782
- const eggPath = this.eggPaths[i];
783
- lookupDirs.add(eggPath);
784
- }
785
-
786
- // should find the $cwd when test the plugins under npm3
787
- lookupDirs.add(process.cwd());
788
- return lookupDirs;
789
- }
790
-
791
- // Get the real plugin path
792
- protected getPluginPath(plugin: EggPluginInfo) {
793
- if (plugin.path) {
794
- return plugin.path;
795
- }
796
-
797
- if (plugin.package) {
798
- assert(
799
- isValidatePackageName(plugin.package),
800
- `plugin ${plugin.name} invalid, use 'path' instead of package: "${plugin.package}"`
801
- );
802
- }
803
- return this.#resolvePluginPath(plugin);
804
- }
805
-
806
- #resolvePluginPath(plugin: EggPluginInfo) {
807
- const name = plugin.package || plugin.name;
808
- try {
809
- // should find the plugin directory
810
- // pnpm will lift the node_modules to the sibling directory
811
- // 'node_modules/.pnpm/yadan@2.0.0/node_modules/yadan/node_modules',
812
- // 'node_modules/.pnpm/yadan@2.0.0/node_modules', <- this is the sibling directory
813
- // 'node_modules/.pnpm/egg@2.33.1/node_modules/egg/node_modules',
814
- // 'node_modules/.pnpm/egg@2.33.1/node_modules', <- this is the sibling directory
815
- const pluginPkgFile = utils.resolvePath(`${name}/package.json`, {
816
- paths: [...this.lookupDirs],
817
- });
818
- return path.dirname(pluginPkgFile);
819
- } catch (err) {
820
- debug('[resolvePluginPath] error: %o, plugin info: %o', err, plugin);
821
- throw new Error(
822
- `Can not find plugin ${name} in "${[...this.lookupDirs].join(', ')}"`,
823
- {
824
- cause: err,
825
- }
826
- );
827
- }
828
- }
829
-
830
- async #formatPluginPathFromPackageJSON(
831
- pluginPath: string,
832
- pluginPkg: {
833
- eggPlugin?: {
834
- exports?: {
835
- import?: string;
836
- require?: string;
837
- typescript?: string;
838
- };
839
- };
840
- }
841
- ): Promise<string> {
842
- let realPluginPath = pluginPath;
843
- const exports = pluginPkg.eggPlugin?.exports;
844
- if (exports) {
845
- if (isESM) {
846
- if (exports.import) {
847
- realPluginPath = path.join(pluginPath, exports.import);
848
- }
849
- } else if (exports.require) {
850
- realPluginPath = path.join(pluginPath, exports.require);
851
- }
852
- if (
853
- exports.typescript &&
854
- isSupportTypeScript() &&
855
- !(await exists(realPluginPath))
856
- ) {
857
- // if require/import path not exists, use typescript path for development stage
858
- realPluginPath = path.join(pluginPath, exports.typescript);
859
- debug(
860
- '[formatPluginPathFromPackageJSON] use typescript path %o',
861
- realPluginPath
862
- );
863
- }
864
- }
865
- return realPluginPath;
866
- }
867
-
868
- #extendPlugins(
869
- targets: Record<string, EggPluginInfo>,
870
- plugins: Record<string, EggPluginInfo>
871
- ) {
872
- if (!plugins) {
873
- return;
874
- }
875
- for (const name in plugins) {
876
- const plugin = plugins[name];
877
- let targetPlugin = targets[name];
878
- if (!targetPlugin) {
879
- targetPlugin = {} as EggPluginInfo;
880
- targets[name] = targetPlugin;
881
- }
882
- if (targetPlugin.package && targetPlugin.package === plugin.package) {
883
- this.logger.warn(
884
- '[@eggjs/core] plugin %s has been defined that is %j, but you define again in %s',
885
- name,
886
- targetPlugin,
887
- plugin.from
888
- );
889
- }
890
- if (plugin.path || plugin.package) {
891
- delete targetPlugin.path;
892
- delete targetPlugin.package;
893
- }
894
- for (const [prop, value] of Object.entries(plugin)) {
895
- if (value === undefined) {
896
- continue;
897
- }
898
- if (
899
- Reflect.get(targetPlugin, prop) &&
900
- Array.isArray(value) &&
901
- value.length === 0
902
- ) {
903
- continue;
904
- }
905
- Reflect.set(targetPlugin, prop, value);
906
- }
907
- }
908
- }
909
- /** end Plugin loader */
910
-
911
- /** start Config loader */
912
- configMeta: Record<string, any>;
913
- config: EggAppConfig;
914
-
915
- /**
916
- * Load config/config.js
917
- *
918
- * Will merge config.default.js 和 config.${env}.js
919
- *
920
- * @function EggLoader#loadConfig
921
- * @since 1.0.0
922
- */
923
- async loadConfig() {
924
- this.timing.start('Load Config');
925
- this.configMeta = {};
926
-
927
- const target: EggAppConfig = {
928
- middleware: [],
929
- coreMiddleware: [],
930
- };
931
-
932
- // Load Application config first
933
- const appConfig = await this.#preloadAppConfig();
934
-
935
- // plugin config.default
936
- // framework config.default
937
- // app config.default
938
- // plugin config.{env}
939
- // framework config.{env}
940
- // app config.{env}
941
- for (const filename of this.getTypeFiles('config')) {
942
- for (const unit of this.getLoadUnits()) {
943
- const isApp = unit.type === 'app';
944
- const config = await this.#loadConfig(
945
- unit.path,
946
- filename,
947
- isApp ? undefined : appConfig,
948
- unit.type
949
- );
950
- if (!config) {
951
- continue;
952
- }
953
- debug(
954
- '[loadConfig] Loaded config %s/%s, %j',
955
- unit.path,
956
- filename,
957
- config
958
- );
959
- extend(true, target, config);
960
- }
961
- }
962
-
963
- // load env from process.env.EGG_APP_CONFIG
964
- const envConfig = this.#loadConfigFromEnv();
965
- debug('[loadConfig] Loaded config from env, %j', envConfig);
966
- extend(true, target, envConfig);
967
-
968
- // You can manipulate the order of app.config.coreMiddleware and app.config.appMiddleware in app.js
969
- target.coreMiddleware = target.coreMiddleware || [];
970
- // alias for coreMiddleware
971
- target.coreMiddlewares = target.coreMiddleware;
972
-
973
- target.appMiddleware = target.middleware || [];
974
- // alias for appMiddleware
975
- target.appMiddlewares = target.appMiddleware;
976
-
977
- this.config = target;
978
- debug('[loadConfig] all config: %o', this.config);
979
- this.timing.end('Load Config');
980
- }
981
-
982
- async #preloadAppConfig() {
983
- const names = ['config.default', `config.${this.serverEnv}`];
984
- const target: Record<string, any> = {};
985
- for (const filename of names) {
986
- const config = await this.#loadConfig(
987
- this.options.baseDir,
988
- filename,
989
- undefined,
990
- 'app'
991
- );
992
- if (!config) {
993
- continue;
994
- }
995
- extend(true, target, config);
996
- }
997
- return target;
998
- }
999
-
1000
- async #loadConfig(
1001
- dirpath: string,
1002
- filename: string,
1003
- extraInject: object | undefined,
1004
- type: EggDirInfoType
1005
- ) {
1006
- const isPlugin = type === 'plugin';
1007
- const isApp = type === 'app';
1008
-
1009
- let filepath = this.resolveModule(path.join(dirpath, 'config', filename));
1010
- // let config.js compatible
1011
- if (filename === 'config.default' && !filepath) {
1012
- filepath = this.resolveModule(path.join(dirpath, 'config/config'));
1013
- }
1014
- if (!filepath) {
1015
- return;
1016
- }
1017
- const config: Record<string, any> = await this.loadFile(
1018
- filepath,
1019
- this.appInfo,
1020
- extraInject
1021
- );
1022
- if (!config) return;
1023
- if (isPlugin || isApp) {
1024
- assert(
1025
- !config.coreMiddleware,
1026
- 'Can not define coreMiddleware in app or plugin'
1027
- );
1028
- }
1029
- if (!isApp) {
1030
- assert(!config.middleware, 'Can not define middleware in ' + filepath);
1031
- }
1032
- // store config meta, check where is the property of config come from.
1033
- this.#setConfigMeta(config, filepath);
1034
- return config;
1035
- }
1036
-
1037
- #loadConfigFromEnv() {
1038
- const envConfigStr = process.env.EGG_APP_CONFIG;
1039
- if (!envConfigStr) return;
1040
- try {
1041
- const envConfig: Record<string, unknown> = JSON.parse(envConfigStr);
1042
- this.#setConfigMeta(envConfig, '<process.env.EGG_APP_CONFIG>');
1043
- return envConfig;
1044
- } catch {
1045
- this.options.logger.warn(
1046
- '[egg-loader] process.env.EGG_APP_CONFIG is not invalid JSON: %s',
1047
- envConfigStr
1048
- );
1049
- }
1050
- }
1051
-
1052
- #setConfigMeta(config: Record<string, unknown>, filepath: string) {
1053
- config = extend(true, {}, config);
1054
- this.#setConfig(config, filepath);
1055
- extend(true, this.configMeta, config);
1056
- }
1057
-
1058
- #setConfig(obj: Record<string, any>, filepath: string) {
1059
- for (const key of Object.keys(obj)) {
1060
- const val = obj[key];
1061
- // ignore console
1062
- if (
1063
- key === 'console' &&
1064
- val &&
1065
- typeof val.Console === 'function' &&
1066
- val.Console === console.Console
1067
- ) {
1068
- obj[key] = filepath;
1069
- continue;
1070
- }
1071
- if (
1072
- val &&
1073
- Object.getPrototypeOf(val) === Object.prototype &&
1074
- Object.keys(val).length > 0
1075
- ) {
1076
- this.#setConfig(val, filepath);
1077
- continue;
1078
- }
1079
- obj[key] = filepath;
1080
- }
1081
- }
1082
- /** end Config loader */
1083
-
1084
- /** start Extend loader */
1085
- /**
1086
- * mixin Agent.prototype
1087
- * @function EggLoader#loadAgentExtend
1088
- * @since 1.0.0
1089
- */
1090
- async loadAgentExtend() {
1091
- await this.loadExtend('agent', this.app);
1092
- }
1093
-
1094
- /**
1095
- * mixin Application.prototype
1096
- * @function EggLoader#loadApplicationExtend
1097
- * @since 1.0.0
1098
- */
1099
- async loadApplicationExtend() {
1100
- await this.loadExtend('application', this.app);
1101
- }
1102
-
1103
- /**
1104
- * mixin Request.prototype
1105
- * @function EggLoader#loadRequestExtend
1106
- * @since 1.0.0
1107
- */
1108
- async loadRequestExtend() {
1109
- await this.loadExtend('request', this.app.request);
1110
- }
1111
-
1112
- /**
1113
- * mixin Response.prototype
1114
- * @function EggLoader#loadResponseExtend
1115
- * @since 1.0.0
1116
- */
1117
- async loadResponseExtend() {
1118
- await this.loadExtend('response', this.app.response);
1119
- }
1120
-
1121
- /**
1122
- * mixin Context.prototype
1123
- * @function EggLoader#loadContextExtend
1124
- * @since 1.0.0
1125
- */
1126
- async loadContextExtend() {
1127
- await this.loadExtend('context', this.app.context);
1128
- }
1129
-
1130
- /**
1131
- * mixin app.Helper.prototype
1132
- * @function EggLoader#loadHelperExtend
1133
- * @since 1.0.0
1134
- */
1135
- async loadHelperExtend() {
1136
- if (this.app.Helper) {
1137
- await this.loadExtend('helper', this.app.Helper.prototype);
1138
- }
1139
- }
1140
-
1141
- /**
1142
- * Find all extend file paths by name
1143
- * can be override in top level framework to support load `app/extends/{name}.js`
1144
- *
1145
- * @param {String} name - filename which may be `app/extend/{name}.js`
1146
- * @returns {Array} filepaths extend file paths
1147
- * @private
1148
- */
1149
- protected getExtendFilePaths(name: string): string[] {
1150
- return this.getLoadUnits().map(unit =>
1151
- path.join(unit.path, 'app/extend', name)
1152
- );
1153
- }
1154
-
1155
- /**
1156
- * Loader app/extend/xx.js to `prototype`,
1157
- * @function loadExtend
1158
- * @param {String} name - filename which may be `app/extend/{name}.js`
1159
- * @param {Object} proto - prototype that mixed
1160
- * @since 1.0.0
1161
- */
1162
- async loadExtend(name: string, proto: object) {
1163
- this.timing.start(`Load extend/${name}.js`);
1164
- // All extend files
1165
- const filepaths = this.getExtendFilePaths(name);
1166
- // if use mm.env and serverEnv is not unittest
1167
- const needUnittest =
1168
- 'EGG_MOCK_SERVER_ENV' in process.env && this.serverEnv !== 'unittest';
1169
- const length = filepaths.length;
1170
- for (let i = 0; i < length; i++) {
1171
- const filepath = filepaths[i];
1172
- filepaths.push(filepath + `.${this.serverEnv}`);
1173
- if (needUnittest) {
1174
- filepaths.push(filepath + '.unittest');
1175
- }
1176
- }
1177
- debug('loadExtend %s, filepaths: %j', name, filepaths);
1178
-
1179
- const mergeRecord = new Map();
1180
- for (const rawFilepath of filepaths) {
1181
- const filepath = this.resolveModule(rawFilepath);
1182
- if (!filepath) {
1183
- // debug('loadExtend %o not found', rawFilepath);
1184
- continue;
1185
- }
1186
- if (filepath.endsWith('/index.js')) {
1187
- this.app.deprecate(
1188
- `app/extend/${name}/index.js is deprecated, use app/extend/${name}.js instead`
1189
- );
1190
- } else if (filepath.endsWith('/index.ts')) {
1191
- this.app.deprecate(
1192
- `app/extend/${name}/index.ts is deprecated, use app/extend/${name}.ts instead`
1193
- );
1194
- }
1195
-
1196
- let ext = await this.requireFile(filepath);
1197
- // if extend object is Class, should use Class.prototype instead
1198
- if (isClass(ext)) {
1199
- ext = ext.prototype;
1200
- }
1201
- const properties = Object.getOwnPropertyNames(ext)
1202
- .concat(Object.getOwnPropertySymbols(ext) as unknown as string[])
1203
- .filter(name => name !== 'constructor'); // ignore class constructor for extend
1204
-
1205
- for (const property of properties) {
1206
- if (mergeRecord.has(property)) {
1207
- debug(
1208
- 'Property: "%s" already exists in "%s",it will be redefined by "%s"',
1209
- property,
1210
- mergeRecord.get(property),
1211
- filepath
1212
- );
1213
- }
1214
-
1215
- // Copy descriptor
1216
- let descriptor = Object.getOwnPropertyDescriptor(
1217
- ext,
1218
- property
1219
- ) as PropertyDescriptor;
1220
- let originalDescriptor = Object.getOwnPropertyDescriptor(
1221
- proto,
1222
- property
1223
- );
1224
- if (!originalDescriptor) {
1225
- // try to get descriptor from originalPrototypes
1226
- const originalProto = originalPrototypes[name];
1227
- if (originalProto) {
1228
- originalDescriptor = Object.getOwnPropertyDescriptor(
1229
- originalProto,
1230
- property
1231
- );
1232
- }
1233
- }
1234
- if (originalDescriptor) {
1235
- // don't override descriptor
1236
- descriptor = {
1237
- ...descriptor,
1238
- };
1239
- if (!descriptor.set && originalDescriptor.set) {
1240
- descriptor.set = originalDescriptor.set;
1241
- }
1242
- if (!descriptor.get && originalDescriptor.get) {
1243
- descriptor.get = originalDescriptor.get;
1244
- }
1245
- }
1246
- Object.defineProperty(proto, property, descriptor);
1247
- mergeRecord.set(property, filepath);
1248
- }
1249
- debug('merge %j to %s from %s', properties, name, filepath);
1250
- }
1251
- this.timing.end(`Load extend/${name}.js`);
1252
- }
1253
- /** end Extend loader */
1254
-
1255
- /** start Custom loader */
1256
- /**
1257
- * load app.js
1258
- *
1259
- * @example
1260
- * - old:
1261
- *
1262
- * ```js
1263
- * module.exports = function(app) {
1264
- * doSomething();
1265
- * }
1266
- * ```
1267
- *
1268
- * - new:
1269
- *
1270
- * ```js
1271
- * module.exports = class Boot {
1272
- * constructor(app) {
1273
- * this.app = app;
1274
- * }
1275
- * configDidLoad() {
1276
- * doSomething();
1277
- * }
1278
- * }
1279
- * @since 1.0.0
1280
- */
1281
- async loadCustomApp() {
1282
- await this.#loadBootHook('app');
1283
- this.lifecycle.triggerConfigWillLoad();
1284
- }
1285
-
1286
- /**
1287
- * Load agent.js, same as {@link EggLoader#loadCustomApp}
1288
- */
1289
- async loadCustomAgent() {
1290
- await this.#loadBootHook('agent');
1291
- this.lifecycle.triggerConfigWillLoad();
1292
- }
1293
-
1294
- // FIXME: no logger used after egg removed
1295
- loadBootHook() {
1296
- // do nothing
1297
- }
1298
-
1299
- async #loadBootHook(fileName: string) {
1300
- this.timing.start(`Load ${fileName}.js`);
1301
- for (const unit of this.getLoadUnits()) {
1302
- const bootFile = path.join(unit.path, fileName);
1303
- const bootFilePath = this.resolveModule(bootFile);
1304
- if (!bootFilePath) {
1305
- // debug('[loadBootHook] %o not found', bootFile);
1306
- continue;
1307
- }
1308
- const bootHook = await this.requireFile(bootFilePath);
1309
- if (isClass(bootHook)) {
1310
- bootHook.prototype.fullPath = bootFilePath;
1311
- // if is boot class, add to lifecycle
1312
- this.lifecycle.addBootHook(bootHook);
1313
- debug('[loadBootHook] add BootHookClass from %o', bootFilePath);
1314
- } else if (typeof bootHook === 'function') {
1315
- // if is boot function, wrap to class
1316
- // for compatibility
1317
- this.lifecycle.addFunctionAsBootHook(bootHook, bootFilePath);
1318
- debug('[loadBootHook] add bootHookFunction from %o', bootFilePath);
1319
- } else {
1320
- this.options.logger.warn(
1321
- '[@eggjs/core/egg_loader] %s must exports a boot class',
1322
- bootFilePath
1323
- );
1324
- }
1325
- }
1326
- // init boots
1327
- this.lifecycle.init();
1328
- this.timing.end(`Load ${fileName}.js`);
1329
- }
1330
- /** end Custom loader */
1331
-
1332
- /** start Service loader */
1333
- /**
1334
- * Load app/service
1335
- * @function EggLoader#loadService
1336
- * @param {Object} options - LoaderOptions
1337
- * @since 1.0.0
1338
- */
1339
- async loadService(options?: Partial<ContextLoaderOptions>) {
1340
- this.timing.start('Load Service');
1341
- // 载入到 app.serviceClasses
1342
- const servicePaths = this.getLoadUnits().map(unit =>
1343
- path.join(unit.path, 'app/service')
1344
- );
1345
- options = {
1346
- call: true,
1347
- caseStyle: CaseStyle.lower,
1348
- fieldClass: 'serviceClasses',
1349
- directory: servicePaths,
1350
- ...options,
1351
- };
1352
- debug('[loadService] options: %o', options);
1353
- await this.loadToContext(
1354
- servicePaths,
1355
- 'service',
1356
- options as ContextLoaderOptions
1357
- );
1358
- this.timing.end('Load Service');
1359
- }
1360
- /** end Service loader */
1361
-
1362
- /** start Middleware loader */
1363
- /**
1364
- * Load app/middleware
1365
- *
1366
- * app.config.xx is the options of the middleware xx that has same name as config
1367
- *
1368
- * @function EggLoader#loadMiddleware
1369
- * @param {Object} opt - LoaderOptions
1370
- * @example
1371
- * ```js
1372
- * // app/middleware/status.js
1373
- * module.exports = function(options, app) {
1374
- * // options == app.config.status
1375
- * return async next => {
1376
- * await next();
1377
- * }
1378
- * }
1379
- * ```
1380
- * @since 1.0.0
1381
- */
1382
- async loadMiddleware(opt?: Partial<FileLoaderOptions>) {
1383
- this.timing.start('Load Middleware');
1384
- const app = this.app;
1385
-
1386
- // load middleware to app.middleware
1387
- const middlewarePaths = this.getLoadUnits().map(unit =>
1388
- path.join(unit.path, 'app/middleware')
1389
- );
1390
- opt = {
1391
- call: false,
1392
- override: true,
1393
- caseStyle: CaseStyle.lower,
1394
- directory: middlewarePaths,
1395
- ...opt,
1396
- };
1397
- await this.loadToApp(
1398
- middlewarePaths,
1399
- 'middlewares',
1400
- opt as FileLoaderOptions
1401
- );
1402
-
1403
- for (const name in app.middlewares) {
1404
- Object.defineProperty(app.middleware, name, {
1405
- get() {
1406
- return app.middlewares[name];
1407
- },
1408
- enumerable: false,
1409
- configurable: false,
1410
- });
1411
- }
1412
-
1413
- this.options.logger.info(
1414
- 'Use coreMiddleware order: %j',
1415
- this.config.coreMiddleware
1416
- );
1417
- this.options.logger.info(
1418
- 'Use appMiddleware order: %j',
1419
- this.config.appMiddleware
1420
- );
1421
-
1422
- // use middleware ordered by app.config.coreMiddleware and app.config.appMiddleware
1423
- const middlewareNames = this.config.coreMiddleware.concat(
1424
- this.config.appMiddleware
1425
- );
1426
- debug('[loadMiddleware] middlewareNames: %j', middlewareNames);
1427
- const middlewaresMap = new Map<string, boolean>();
1428
- for (const name of middlewareNames) {
1429
- const createMiddleware = app.middlewares[name];
1430
- if (!createMiddleware) {
1431
- throw new TypeError(`Middleware ${name} not found`);
1432
- }
1433
- if (middlewaresMap.has(name)) {
1434
- throw new TypeError(`Middleware ${name} redefined`);
1435
- }
1436
- middlewaresMap.set(name, true);
1437
- const options = this.config[name] || {};
1438
- let mw: MiddlewareFunc | null = createMiddleware(options, app);
1439
- assert(
1440
- typeof mw === 'function',
1441
- `Middleware ${name} must be a function, but actual is ${inspect(mw)}`
1442
- );
1443
- if (isGeneratorFunction(mw)) {
1444
- const fullpath = Reflect.get(createMiddleware, FULLPATH);
1445
- throw new TypeError(
1446
- `Support for generators was removed, middleware: ${name}, fullpath: ${fullpath}`
1447
- );
1448
- }
1449
- mw._name = name;
1450
- // middlewares support options.enable, options.ignore and options.match
1451
- mw = wrapMiddleware(mw, options);
1452
- if (mw) {
1453
- if (debug.enabled) {
1454
- // show mw debug log on every request
1455
- mw = debugMiddlewareWrapper(mw);
1456
- }
1457
- app.use(mw);
1458
- debug(
1459
- '[loadMiddleware] Use middleware: %s with options: %j',
1460
- name,
1461
- options
1462
- );
1463
- this.options.logger.info(
1464
- '[@eggjs/core/egg_loader] Use middleware: %s',
1465
- name
1466
- );
1467
- } else {
1468
- this.options.logger.info(
1469
- '[@eggjs/core/egg_loader] Disable middleware: %s',
1470
- name
1471
- );
1472
- }
1473
- }
1474
-
1475
- this.options.logger.info(
1476
- '[@eggjs/core/egg_loader] Loaded middleware from %j',
1477
- middlewarePaths
1478
- );
1479
- this.timing.end('Load Middleware');
1480
-
1481
- // add router middleware, make sure router is the last middleware
1482
- const mw = this.app.router.middleware();
1483
- Reflect.set(mw, '_name', 'routerMiddleware');
1484
- this.app.use(mw);
1485
- }
1486
- /** end Middleware loader */
1487
-
1488
- /** start Controller loader */
1489
- /**
1490
- * Load app/controller
1491
- * @param {Object} opt - LoaderOptions
1492
- * @since 1.0.0
1493
- */
1494
- async loadController(opt?: Partial<FileLoaderOptions>) {
1495
- this.timing.start('Load Controller');
1496
- const controllerBase = path.join(this.options.baseDir, 'app/controller');
1497
- opt = {
1498
- caseStyle: CaseStyle.lower,
1499
- directory: controllerBase,
1500
- initializer: (obj, opt) => {
1501
- // return class if it exports a function
1502
- // ```js
1503
- // module.exports = app => {
1504
- // return class HomeController extends app.Controller {};
1505
- // }
1506
- // ```
1507
- if (isGeneratorFunction(obj)) {
1508
- throw new TypeError(
1509
- `Support for generators was removed, fullpath: ${opt.path}`
1510
- );
1511
- }
1512
- if (
1513
- !isClass(obj) &&
1514
- !isAsyncFunction(obj) &&
1515
- typeof obj === 'function'
1516
- ) {
1517
- obj = obj(this.app);
1518
- debug('[loadController] after init(app) => %o, meta: %j', obj, opt);
1519
- if (isGeneratorFunction(obj)) {
1520
- throw new TypeError(
1521
- `Support for generators was removed, fullpath: ${opt.path}`
1522
- );
1523
- }
1524
- }
1525
- if (isClass(obj)) {
1526
- obj.prototype.pathName = opt.pathName;
1527
- obj.prototype.fullPath = opt.path;
1528
- return wrapControllerClass(obj, opt.path);
1529
- }
1530
- if (isObject(obj)) {
1531
- return wrapObject(obj, opt.path);
1532
- }
1533
- if (isAsyncFunction(obj)) {
1534
- return wrapObject({ 'module.exports': obj }, opt.path)[
1535
- 'module.exports'
1536
- ];
1537
- }
1538
- return obj;
1539
- },
1540
- ...opt,
1541
- };
1542
- await this.loadToApp(
1543
- controllerBase,
1544
- 'controller',
1545
- opt as FileLoaderOptions
1546
- );
1547
- debug('[loadController] app.controller => %o', this.app.controller);
1548
- this.options.logger.info(
1549
- '[@eggjs/core/egg_loader] Controller loaded: %s',
1550
- controllerBase
1551
- );
1552
- this.timing.end('Load Controller');
1553
- }
1554
- /** end Controller loader */
1555
-
1556
- /** start Router loader */
1557
- /**
1558
- * Load app/router.js
1559
- * @function EggLoader#loadRouter
1560
- * @since 1.0.0
1561
- */
1562
- async loadRouter() {
1563
- this.timing.start('Load Router');
1564
- await this.loadFile(path.join(this.options.baseDir, 'app/router'));
1565
- this.timing.end('Load Router');
1566
- }
1567
- /** end Router loader */
1568
-
1569
- /** start CustomLoader loader */
1570
- async loadCustomLoader() {
1571
- assert(this.config, 'should loadConfig first');
1572
- const customLoader = this.config.customLoader || {};
1573
-
1574
- for (const property of Object.keys(customLoader)) {
1575
- const loaderConfig = {
1576
- ...customLoader[property],
1577
- };
1578
- assert(
1579
- loaderConfig.directory,
1580
- `directory is required for config.customLoader.${property}`
1581
- );
1582
- let directory: string | string[];
1583
- if (loaderConfig.loadunit === true) {
1584
- directory = this.getLoadUnits().map(unit =>
1585
- path.join(unit.path, loaderConfig.directory)
1586
- );
1587
- } else {
1588
- directory = path.join(this.appInfo.baseDir, loaderConfig.directory);
1589
- }
1590
- const inject = loaderConfig.inject || 'app';
1591
- debug(
1592
- '[loadCustomLoader] loaderConfig: %o, inject: %o, directory: %o',
1593
- loaderConfig,
1594
- inject,
1595
- directory
1596
- );
1597
-
1598
- switch (inject) {
1599
- case 'ctx': {
1600
- assert(
1601
- !(property in this.app.context),
1602
- `customLoader should not override ctx.${property}`
1603
- );
1604
- const options = {
1605
- caseStyle: CaseStyle.lower,
1606
- fieldClass: `${property}Classes`,
1607
- ...loaderConfig,
1608
- directory,
1609
- };
1610
- await this.loadToContext(directory, property, options);
1611
- break;
1612
- }
1613
- case 'app': {
1614
- assert(
1615
- !(property in this.app),
1616
- `customLoader should not override app.${property}`
1617
- );
1618
- const options = {
1619
- caseStyle: CaseStyle.lower,
1620
- initializer: (Clazz: unknown) => {
1621
- return isClass(Clazz) ? new Clazz(this.app) : Clazz;
1622
- },
1623
- ...loaderConfig,
1624
- directory,
1625
- };
1626
- await this.loadToApp(directory, property, options);
1627
- break;
1628
- }
1629
- default:
1630
- throw new Error('inject only support app or ctx');
1631
- }
1632
- }
1633
- }
1634
- /** end CustomLoader loader */
1635
-
1636
- // Low Level API
1637
-
1638
- /**
1639
- * Load single file, will invoke when export is function
1640
- *
1641
- * @param {String} filepath - fullpath
1642
- * @param {Array} inject - pass rest arguments into the function when invoke
1643
- * @returns {Object} exports
1644
- * @example
1645
- * ```js
1646
- * app.loader.loadFile(path.join(app.options.baseDir, 'config/router.js'));
1647
- * ```
1648
- * @since 1.0.0
1649
- */
1650
- async loadFile(filepath: string, ...inject: unknown[]) {
1651
- const fullpath = filepath && this.resolveModule(filepath);
1652
- if (!fullpath) {
1653
- return null;
1654
- }
1655
-
1656
- // function(arg1, args, ...) {}
1657
- if (inject.length === 0) {
1658
- inject = [this.app];
1659
- }
1660
- let mod = await this.requireFile(fullpath);
1661
- if (typeof mod === 'function' && !isClass(mod)) {
1662
- mod = mod(...inject);
1663
- if (isPromise(mod)) {
1664
- mod = await mod;
1665
- }
1666
- }
1667
- return mod;
1668
- }
1669
-
1670
- /**
1671
- * @param {String} filepath - fullpath
1672
- * @returns {Object} exports
1673
- * @private
1674
- */
1675
- async requireFile(filepath: string) {
1676
- const timingKey = `Require(${this.#requiredCount++}) ${utils.getResolvedFilename(filepath, this.options.baseDir)}`;
1677
- this.timing.start(timingKey);
1678
- const mod = await utils.loadFile(filepath);
1679
- this.timing.end(timingKey);
1680
- return mod;
1681
- }
1682
-
1683
- /**
1684
- * Get all loadUnit
1685
- *
1686
- * loadUnit is a directory that can be loaded by EggLoader, it has the same structure.
1687
- * loadUnit has a path and a type(app, framework, plugin).
1688
- *
1689
- * The order of the loadUnits:
1690
- *
1691
- * 1. plugin
1692
- * 2. framework
1693
- * 3. app
1694
- *
1695
- * @returns {Array} loadUnits
1696
- * @since 1.0.0
1697
- */
1698
- getLoadUnits(): EggDirInfo[] {
1699
- if (this.dirs) {
1700
- return this.dirs;
1701
- }
1702
-
1703
- this.dirs = [];
1704
- if (this.orderPlugins) {
1705
- for (const plugin of this.orderPlugins) {
1706
- this.dirs.push({
1707
- path: plugin.path as string,
1708
- type: 'plugin',
1709
- });
1710
- }
1711
- }
1712
-
1713
- // framework or egg path
1714
- for (const eggPath of this.eggPaths) {
1715
- this.dirs.push({
1716
- path: eggPath,
1717
- type: 'framework',
1718
- });
1719
- }
1720
-
1721
- // application
1722
- this.dirs.push({
1723
- path: this.options.baseDir,
1724
- type: 'app',
1725
- });
1726
-
1727
- debug('Loaded dirs %j', this.dirs);
1728
- return this.dirs;
1729
- }
1730
-
1731
- /**
1732
- * Load files using {@link FileLoader}, inject to {@link Application}
1733
- * @param {String|Array} directory - see {@link FileLoader}
1734
- * @param {String} property - see {@link FileLoader}
1735
- * @param {Object} options - see {@link FileLoader}
1736
- * @since 1.0.0
1737
- */
1738
- async loadToApp(
1739
- directory: string | string[],
1740
- property: string | symbol,
1741
- options?: Omit<FileLoaderOptions, 'inject' | 'target'>
1742
- ) {
1743
- const target = {};
1744
- Reflect.set(this.app, property, target);
1745
- const loadOptions: FileLoaderOptions = {
1746
- ...options,
1747
- directory: options?.directory ?? directory,
1748
- target,
1749
- inject: this.app,
1750
- };
1751
-
1752
- const timingKey = `Load "${String(property)}" to Application`;
1753
- this.timing.start(timingKey);
1754
- await new FileLoader(loadOptions).load();
1755
- this.timing.end(timingKey);
1756
- }
1757
-
1758
- /**
1759
- * Load files using {@link ContextLoader}
1760
- * @param {String|Array} directory - see {@link ContextLoader}
1761
- * @param {String} property - see {@link ContextLoader}
1762
- * @param {Object} options - see {@link ContextLoader}
1763
- * @since 1.0.0
1764
- */
1765
- async loadToContext(
1766
- directory: string | string[],
1767
- property: string | symbol,
1768
- options?: Omit<ContextLoaderOptions, 'inject' | 'property'>
1769
- ) {
1770
- const loadOptions: ContextLoaderOptions = {
1771
- ...options,
1772
- directory: options?.directory || directory,
1773
- property,
1774
- inject: this.app,
1775
- };
1776
-
1777
- const timingKey = `Load "${String(property)}" to Context`;
1778
- this.timing.start(timingKey);
1779
- await new ContextLoader(loadOptions).load();
1780
- this.timing.end(timingKey);
1781
- }
1782
-
1783
- /**
1784
- * @member {FileLoader} EggLoader#FileLoader
1785
- * @since 1.0.0
1786
- */
1787
- get FileLoader() {
1788
- return FileLoader;
1789
- }
1790
-
1791
- /**
1792
- * @member {ContextLoader} EggLoader#ContextLoader
1793
- * @since 1.0.0
1794
- */
1795
- get ContextLoader() {
1796
- return ContextLoader;
1797
- }
1798
-
1799
- getTypeFiles(filename: string) {
1800
- const files = [`${filename}.default`];
1801
- if (this.serverScope) files.push(`${filename}.${this.serverScope}`);
1802
- if (this.serverEnv === 'default') return files;
1803
- files.push(`${filename}.${this.serverEnv}`);
1804
- if (this.serverScope) {
1805
- files.push(`${filename}.${this.serverScope}_${this.serverEnv}`);
1806
- }
1807
- return files;
1808
- }
1809
-
1810
- resolveModule(filepath: string) {
1811
- let fullPath;
1812
- try {
1813
- fullPath = utils.resolvePath(filepath);
1814
- } catch {
1815
- // debug('[resolveModule] Module %o resolve error: %s', filepath, err.stack);
1816
- return undefined;
1817
- }
1818
- // if (process.env.EGG_TYPESCRIPT !== 'true' && fullPath.endsWith('.ts')) {
1819
- // return undefined;
1820
- // }
1821
- return fullPath;
1822
- }
1823
- }
1824
-
1825
- function depCompatible(plugin: EggPluginInfo & { dep?: string[] }) {
1826
- if (
1827
- plugin.dep &&
1828
- !(Array.isArray(plugin.dependencies) && plugin.dependencies.length > 0)
1829
- ) {
1830
- plugin.dependencies = plugin.dep;
1831
- delete plugin.dep;
1832
- }
1833
- }
1834
-
1835
- function isValidatePackageName(name: string) {
1836
- // only check file path style
1837
- if (name.startsWith('.')) return false;
1838
- if (name.startsWith('/')) return false;
1839
- if (name.includes(':')) return false;
1840
- return true;
1841
- }
1842
-
1843
- // support pathMatching on middleware
1844
- function wrapMiddleware(
1845
- mw: MiddlewareFunc,
1846
- options: PathMatchingOptions & { enable?: boolean }
1847
- ): MiddlewareFunc | null {
1848
- // support options.enable
1849
- if (options.enable === false) {
1850
- return null;
1851
- }
1852
-
1853
- // support options.match and options.ignore
1854
- if (!options.match && !options.ignore) {
1855
- return mw;
1856
- }
1857
- const match = pathMatching(options);
1858
-
1859
- const fn: MiddlewareFunc = (ctx, next) => {
1860
- if (!match(ctx)) return next();
1861
- return mw(ctx, next);
1862
- };
1863
- fn._name = `${mw._name}middlewareWrapper`;
1864
- return fn;
1865
- }
1866
-
1867
- function debugMiddlewareWrapper(mw: MiddlewareFunc): MiddlewareFunc {
1868
- const fn: MiddlewareFunc = async (ctx, next) => {
1869
- const startTime = now();
1870
- debug(
1871
- '[debugMiddlewareWrapper] [%s %s] enter middleware: %s',
1872
- ctx.method,
1873
- ctx.url,
1874
- mw._name
1875
- );
1876
- await mw(ctx, next);
1877
- const rt = diff(startTime);
1878
- debug(
1879
- '[debugMiddlewareWrapper] [%s %s] after middleware: %s [%sms]',
1880
- ctx.method,
1881
- ctx.url,
1882
- mw._name,
1883
- rt
1884
- );
1885
- };
1886
- fn._name = `${mw._name}DebugWrapper`;
1887
- return fn;
1888
- }
1889
-
1890
- // wrap the controller class, yield a object with middlewares
1891
- function wrapControllerClass(
1892
- Controller: typeof BaseContextClass,
1893
- fullPath: string
1894
- ) {
1895
- let proto = Controller.prototype;
1896
- const ret: Record<string, any> = {};
1897
- // tracing the prototype chain
1898
- while (proto !== Object.prototype) {
1899
- const keys = Object.getOwnPropertyNames(proto);
1900
- for (const key of keys) {
1901
- // getOwnPropertyNames will return constructor
1902
- // that should be ignored
1903
- if (key === 'constructor') {
1904
- continue;
1905
- }
1906
- // skip getter, setter & non-function properties
1907
- const d = Object.getOwnPropertyDescriptor(proto, key);
1908
- // prevent to override sub method
1909
- if (typeof d?.value === 'function' && !Object.hasOwn(ret, key)) {
1910
- const controllerMethodName = `${Controller.name}.${key}`;
1911
- if (isGeneratorFunction(d.value)) {
1912
- throw new TypeError(
1913
- `Support for generators was removed, controller \`${controllerMethodName}\`, fullpath: ${fullPath}`
1914
- );
1915
- }
1916
- ret[key] = controllerMethodToMiddleware(Controller, key);
1917
- ret[key][FULLPATH] = `${fullPath}#${controllerMethodName}()`;
1918
- }
1919
- }
1920
- proto = Object.getPrototypeOf(proto);
1921
- }
1922
- return ret;
1923
- }
1924
-
1925
- function controllerMethodToMiddleware(
1926
- Controller: typeof BaseContextClass,
1927
- key: string
1928
- ) {
1929
- return function classControllerMiddleware(this: Context, ...args: unknown[]) {
1930
- const controller = new Controller(this);
1931
- if (!this.app.config.controller?.supportParams) {
1932
- args = [this];
1933
- }
1934
- // @ts-expect-error key exists
1935
- return controller[key](...args);
1936
- };
1937
- }
1938
-
1939
- // wrap the method of the object, method can receive ctx as it's first argument
1940
- function wrapObject(
1941
- obj: Record<string, any>,
1942
- fullPath: string,
1943
- prefix?: string
1944
- ) {
1945
- const keys = Object.keys(obj);
1946
- const ret: Record<string, any> = {};
1947
- prefix = prefix ?? '';
1948
- for (const key of keys) {
1949
- const controllerMethodName = `${prefix}${key}`;
1950
- const item = obj[key];
1951
- if (isGeneratorFunction(item)) {
1952
- throw new TypeError(
1953
- `Support for generators was removed, controller \`${controllerMethodName}\`, fullpath: ${fullPath}`
1954
- );
1955
- }
1956
- if (typeof item === 'function') {
1957
- const names = getParamNames(item);
1958
- if (names[0] === 'next') {
1959
- throw new Error(
1960
- `controller \`${controllerMethodName}\` should not use next as argument from file ${fullPath}`
1961
- );
1962
- }
1963
- ret[key] = objectFunctionToMiddleware(item);
1964
- ret[key][FULLPATH] = `${fullPath}#${controllerMethodName}()`;
1965
- } else if (isObject(item)) {
1966
- ret[key] = wrapObject(item, fullPath, `${controllerMethodName}.`);
1967
- }
1968
- }
1969
- debug('[wrapObject] fullPath: %s, prefix: %s => %o', fullPath, prefix, ret);
1970
- return ret;
1971
- }
1972
-
1973
- function objectFunctionToMiddleware(func: Fun) {
1974
- async function objectControllerMiddleware(this: Context, ...args: unknown[]) {
1975
- if (!this.app.config.controller?.supportParams) {
1976
- args = [this];
1977
- }
1978
- return await func.apply(this, args);
1979
- }
1980
- for (const key in func) {
1981
- Reflect.set(objectControllerMiddleware, key, Reflect.get(func, key));
1982
- }
1983
- return objectControllerMiddleware;
1984
- }