diff --git a/.gitignore b/.gitignore index cd6dbe2e05..0a7c571c03 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ site/dist .vercel package-lock.json .tshy* +dist diff --git a/package.json b/package.json index deb5a43d97..4893d04f5f 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "koa-is-json": "^1.0.0", "koa-override": "^4.0.0", "ms": "^2.1.3", - "on-finished": "^2.4.1", "onelogger": "^1.0.0", "sendmessage": "^3.0.0", "urllib": "^4.0.0", @@ -129,7 +128,6 @@ } }, "exports": { - "./package.json": "./package.json", ".": { "import": { "source": "./src/index.ts", @@ -141,8 +139,10 @@ "types": "./dist/commonjs/index.d.ts", "default": "./dist/commonjs/index.js" } - } + }, + "./package.json": "./package.json" }, "types": "./dist/commonjs/index.d.ts", - "main": "./dist/commonjs/index.js" + "main": "./dist/commonjs/index.js", + "module": "./dist/esm/index.js" } diff --git a/src/index.ts b/src/index.ts index e404fd688a..5c68571aff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,8 @@ import { BaseContextClass } from './lib/core/base_context_class.js'; * Start egg application with cluster mode * @since 1.0.0 */ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore export { startCluster } from 'egg-cluster'; /** diff --git a/src/lib/agent.ts b/src/lib/agent.ts index 985d5717c6..42f4d86383 100644 --- a/src/lib/agent.ts +++ b/src/lib/agent.ts @@ -15,7 +15,7 @@ export class Agent extends EggApplication { * @class * @param {Object} options - see {@link EggApplication} */ - constructor(options?: EggApplicationOptions) { + constructor(options?: Omit) { super({ ...options, type: 'agent', diff --git a/src/lib/application.ts b/src/lib/application.ts index 085373a4c8..dc0770d9cd 100644 --- a/src/lib/application.ts +++ b/src/lib/application.ts @@ -1,24 +1,17 @@ import path from 'node:path'; import fs from 'node:fs'; import http from 'node:http'; -import ms from 'ms'; -import is from 'is-type-of'; +import { Socket } from 'node:net'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore import graceful from 'graceful'; -import cluster from 'cluster-client'; -import onFinished from 'on-finished'; import { assign } from 'utility'; -const eggUtils = require('egg-core').utils; -import { EggApplication } from './egg.js'; -import { AppWorkerLoader } from './loader'; +import { utils as eggUtils } from '@eggjs/core'; +import { EggApplication, EggApplicationContext, EggApplicationOptions } from './egg.js'; +import { AppWorkerLoader } from './loader/index.js'; +import { BaseContextClass } from './core/base_context_class.js'; -const KEYS = Symbol('Application#keys'); -const HELPER = Symbol('Application#Helper'); -const LOCALS = Symbol('Application#locals'); -const BIND_EVENTS = Symbol('Application#bindEvents'); -const WARN_CONFUSED_CONFIG = Symbol('Application#warnConfusedConfig'); const EGG_LOADER = Symbol.for('egg#loader'); -const CLUSTER_CLIENTS = Symbol.for('egg#clusterClients'); -const RESPONSE_RAW = Symbol('Application#responseRaw'); // client error => 400 Bad Request // Refs: https://nodejs.org/dist/latest-v8.x/docs/api/http.html#http_event_clienterror @@ -35,55 +28,60 @@ const DEFAULT_BAD_REQUEST_RESPONSE = `\r\n\r\n${DEFAULT_BAD_REQUEST_HTML}`; // Refs: https://github.com/nodejs/node/blob/b38c81/lib/_http_outgoing.js#L706-L710 -function escapeHeaderValue(value) { +function escapeHeaderValue(value: string) { // Protect against response splitting. The regex test is there to // minimize the performance impact in the common case. return /[\r\n]/.test(value) ? value.replace(/[\r\n]+[ \t]*/g, '') : value; } -// Refs: https://github.com/nodejs/node/blob/b38c81/lib/_http_outgoing.js#L706-L710 +/** + * The Helper class which can be used as utility function. + * We support developers to extend Helper through ${baseDir}/app/extend/helper.js , + * then you can use all method on `ctx.helper` that is a instance of Helper. + */ +class HelperClass extends BaseContextClass {} + /** * Singleton instance in App Worker, extend {@link EggApplication} * @augments EggApplication */ export class Application extends EggApplication { + // will auto set after 'server' event emit + server?: http.Server; + #locals: Record = {}; + /** + * reference to {@link Helper} + * @member {Helper} Application#Helper + */ + Helper = HelperClass; /** * @class * @param {Object} options - see {@link EggApplication} */ - constructor(options = {}) { - options.type = 'application'; - super(options); - - // will auto set after 'server' event emit - this.server = null; - - try { - this.loader.load(); - } catch (e) { - // close gracefully - this[CLUSTER_CLIENTS].forEach(cluster.close); - throw e; - } - - // dump config after loaded, ensure all the dynamic modifications will be recorded - const dumpStartTime = Date.now(); - this.dumpConfig(); - this.coreLogger.info('[egg:core] dump config after load, %s', ms(Date.now() - dumpStartTime)); + constructor(options?: Omit) { + super({ + ...options, + type: 'application', + }); + } - this[WARN_CONFUSED_CONFIG](); - this[BIND_EVENTS](); + protected async load() { + await super.load(); + this.#warnConfusedConfig(); + this.#bindEvents(); } get [EGG_LOADER]() { return AppWorkerLoader; } - [RESPONSE_RAW](socket, raw) { + #responseRaw(socket: Socket, raw?: any) { /* istanbul ignore next */ if (!socket.writable) return; - if (!raw) return socket.end(DEFAULT_BAD_REQUEST_RESPONSE); + if (!raw) { + return socket.end(DEFAULT_BAD_REQUEST_RESPONSE); + } const body = (raw.body == null) ? DEFAULT_BAD_REQUEST_HTML : raw.body; const headers = raw.headers || {}; @@ -107,10 +105,10 @@ export class Application extends EggApplication { socket.end(`${firstLine}\r\n${responseHeaderLines}\r\n${body.toString()}`); } - onClientError(err, socket) { + onClientError(err: any, socket: Socket) { // ignore when there is no http body, it almost like an ECONNRESET if (err.rawPacket) { - this.logger.warn('A client (%s:%d) error [%s] occurred: %s', + this.logger.warn('[egg:application] A client (%s:%d) error [%s] occurred: %s', socket.remoteAddress, socket.remotePort, err.code, @@ -136,19 +134,19 @@ export class Application extends EggApplication { // + headers: {} // + status: 400 p.then(ret => { - this[RESPONSE_RAW](socket, ret || {}); + this.#responseRaw(socket, ret || {}); }).catch(err => { this.logger.error(err); - this[RESPONSE_RAW](socket); + this.#responseRaw(socket); }); } else { // because it's a raw socket object, we should return the raw HTTP response // packet. - this[RESPONSE_RAW](socket); + this.#responseRaw(socket); } } - onServer(server) { + onServer(server: http.Server) { // expose app.server this.server = server; // set ignore code @@ -157,14 +155,14 @@ export class Application extends EggApplication { /* istanbul ignore next */ graceful({ server: [ server ], - error: (err, throwErrorCount) => { + error: (err: Error, throwErrorCount: number) => { const originMessage = err.message; if (originMessage) { // shouldjs will override error property but only getter // https://github.com/shouldjs/should.js/blob/889e22ebf19a06bc2747d24cf34b25cc00b37464/lib/assertion-error.js#L26 Object.defineProperty(err, 'message', { get() { - return originMessage + ' (uncaughtException throw ' + throwErrorCount + ' times on pid:' + process.pid + ')'; + return `${originMessage} (uncaughtException throw ${throwErrorCount} times on pid: ${process.pid})`; }, configurable: true, enumerable: false, @@ -175,10 +173,12 @@ export class Application extends EggApplication { ignoreCode: serverGracefulIgnoreCode, }); - server.on('clientError', (err, socket) => this.onClientError(err, socket)); + server.on('clientError', (err, socket) => this.onClientError(err, socket as Socket)); // server timeout - if (is.number(this.config.serverTimeout)) server.setTimeout(this.config.serverTimeout); + if (typeof this.config.serverTimeout === 'number') { + server.setTimeout(this.config.serverTimeout); + } } /** @@ -187,24 +187,11 @@ export class Application extends EggApplication { * @see Context#locals */ get locals() { - if (!this[LOCALS]) { - this[LOCALS] = {}; - } - return this[LOCALS]; - } - - set locals(val) { - if (!this[LOCALS]) { - this[LOCALS] = {}; - } - - assign(this[LOCALS], val); + return this.#locals; } - handleRequest(ctx, fnMiddleware) { - this.emit('request', ctx); - onFinished(ctx.res, () => this.emit('response', ctx)); - return super.handleRequest(ctx, fnMiddleware); + set locals(val: Record) { + assign(this.#locals, val); } /** @@ -227,11 +214,11 @@ export class Application extends EggApplication { paramNames: layer.paramNames, path: layer.path, regexp: layer.regexp.toString(), - stack: layer.stack.map(stack => stack[FULLPATH] || stack._name || stack.name || 'anonymous'), + stack: layer.stack.map((stack: any) => stack[FULLPATH] || stack._name || stack.name || 'anonymous'), }); } fs.writeFileSync(dumpRouterFile, JSON.stringify(routers, null, 2)); - } catch (err) { + } catch (err: any) { this.coreLogger.warn(`dumpConfig router.json error: ${err.message}`); } } @@ -241,9 +228,11 @@ export class Application extends EggApplication { * @see Context#runInBackground * @param {Function} scope - the first args is an anonymous ctx */ - runInBackground(scope) { + runInBackground(scope: (ctx: EggApplicationContext) => void) { const ctx = this.createAnonymousContext(); - if (!scope.name) scope._name = eggUtils.getCalleeFromStack(true); + if (!scope.name) { + Reflect.set(scope, '_name', eggUtils.getCalleeFromStack(true)); + } this.ctxStorage.run(ctx, () => { ctx.runInBackground(scope); }); @@ -255,11 +244,13 @@ export class Application extends EggApplication { * @param {Function} scope - the first args is an anonymous ctx, scope should be async function * @param {Request} [req] - if you want to mock request like querystring, you can pass an object to this function. */ - async runInAnonymousContextScope(scope, req) { + async runInAnonymousContextScope(scope: (ctx: EggApplicationContext) => Promise, req?: unknown) { const ctx = this.createAnonymousContext(req); - if (!scope.name) scope._name = eggUtils.getCalleeFromStack(true); + if (!scope.name) { + Reflect.set(scope, '_name', eggUtils.getCalleeFromStack(true)); + } return await this.ctxStorage.run(ctx, async () => { - return await scope(ctx); + return await scope(ctx as EggApplicationContext); }); } @@ -268,7 +259,7 @@ export class Application extends EggApplication { * @member {String} Application#keys */ get keys() { - if (!this[KEYS]) { + if (!this._keys) { if (!this.config.keys) { if (this.config.env === 'local' || this.config.env === 'unittest') { const configPath = path.join(this.config.baseDir, 'config/config.default.js'); @@ -277,31 +268,9 @@ export class Application extends EggApplication { } throw new Error('Please set config.keys first'); } - - this[KEYS] = this.config.keys.split(',').map(s => s.trim()); - } - return this[KEYS]; - } - - set keys(_) { - // ignore - } - - /** - * reference to {@link Helper} - * @member {Helper} Application#Helper - */ - get Helper() { - if (!this[HELPER]) { - /** - * The Helper class which can be used as utility function. - * We support developers to extend Helper through ${baseDir}/app/extend/helper.js , - * then you can use all method on `ctx.helper` that is a instance of Helper. - */ - class Helper extends this.BaseContextClass {} - this[HELPER] = Helper; + this._keys = this.config.keys.split(',').map(s => s.trim()); } - return this[HELPER]; + return this._keys; } /** @@ -309,17 +278,15 @@ export class Application extends EggApplication { * * @private */ - [BIND_EVENTS]() { + #bindEvents() { // Browser Cookie Limits: http://browsercookielimits.squawky.net/ this.on('cookieLimitExceed', ({ name, value, ctx }) => { const err = new Error(`cookie ${name}'s length(${value.length}) exceed the limit(4093)`); err.name = 'CookieLimitExceedError'; - err.key = name; - err.cookie = value; ctx.coreLogger.error(err); }); // expose server to support websocket - this.once('server', server => this.onServer(server)); + this.once('server', (server: http.Server) => this.onServer(server)); } /** @@ -327,11 +294,11 @@ export class Application extends EggApplication { * * @private */ - [WARN_CONFUSED_CONFIG]() { + #warnConfusedConfig() { const confusedConfigurations = this.config.confusedConfigurations; Object.keys(confusedConfigurations).forEach(key => { if (this.config[key] !== undefined) { - this.logger.warn('Unexpected config key `%s` exists, Please use `%s` instead.', + this.logger.warn('[egg:application] Unexpected config key `%o` exists, Please use `%o` instead.', key, confusedConfigurations[key]); } }); diff --git a/src/lib/core/base_context_class.ts b/src/lib/core/base_context_class.ts index 7e12e75efb..14a43dc4f2 100644 --- a/src/lib/core/base_context_class.ts +++ b/src/lib/core/base_context_class.ts @@ -10,7 +10,7 @@ import { BaseContextLogger } from './base_context_logger.js'; export class BaseContextClass extends EggCoreBaseContextClass { declare ctx: EggApplicationContext; protected pathName?: string; - #logger: BaseContextLogger; + #logger?: BaseContextLogger; get logger() { if (!this.#logger) { diff --git a/src/lib/core/base_context_logger.ts b/src/lib/core/base_context_logger.ts index 07df7bc8b6..70181aaa3e 100644 --- a/src/lib/core/base_context_logger.ts +++ b/src/lib/core/base_context_logger.ts @@ -19,11 +19,13 @@ export class BaseContextLogger { this.#pathName = pathName; } - protected _log(method: string, args: any[]) { + protected _log(method: 'info' | 'warn' | 'error' | 'debug', args: any[]) { // add `[${pathName}]` in log if (this.#pathName && typeof args[0] === 'string') { args[0] = `[${this.#pathName}] ${args[0]}`; } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore this.#ctx.app.logger[method](...args); } diff --git a/src/lib/core/httpclient.ts b/src/lib/core/httpclient.ts index 5e0a1bc431..f04b872b8c 100644 --- a/src/lib/core/httpclient.ts +++ b/src/lib/core/httpclient.ts @@ -46,7 +46,7 @@ export class HttpClient extends RawHttpClient { function normalizeConfig(app: EggApplication) { const config = app.config.httpclient; - if (typeof config.request.timeout === 'string') { + if (typeof config.request?.timeout === 'string') { config.request.timeout = ms(config.request.timeout as string); } } diff --git a/src/lib/core/messenger/IMessenger.ts b/src/lib/core/messenger/IMessenger.ts index 1c749a3a99..4ca184aef3 100644 --- a/src/lib/core/messenger/IMessenger.ts +++ b/src/lib/core/messenger/IMessenger.ts @@ -53,4 +53,6 @@ export interface IMessenger extends EventEmitter { send(action: string, data: unknown | undefined, to: string): IMessenger; close(): void; + + onMessage(message: any): void; } diff --git a/src/lib/core/messenger/ipc.ts b/src/lib/core/messenger/ipc.ts index 8bfb1a44a4..48ddd4337a 100644 --- a/src/lib/core/messenger/ipc.ts +++ b/src/lib/core/messenger/ipc.ts @@ -23,10 +23,10 @@ export class Messenger extends EventEmitter implements IMessenger { this.on('egg-pids', pids => { this.opids = pids; }); - this._onMessage = this._onMessage.bind(this); - process.on('message', this._onMessage); + this.onMessage = this.onMessage.bind(this); + process.on('message', this.onMessage); if (!workerThreads.isMainThread) { - workerThreads.parentPort!.on('message', this._onMessage); + workerThreads.parentPort!.on('message', this.onMessage); } } @@ -116,7 +116,7 @@ export class Messenger extends EventEmitter implements IMessenger { return this; } - _onMessage(message: any) { + onMessage(message: any) { if (typeof message?.action === 'string') { debug('[%s] got message %s with %j, receiverPid: %s', this.pid, message.action, message.data, message.receiverPid); @@ -125,7 +125,7 @@ export class Messenger extends EventEmitter implements IMessenger { } close() { - process.removeListener('message', this._onMessage); + process.removeListener('message', this.onMessage); this.removeAllListeners(); } diff --git a/src/lib/core/messenger/local.ts b/src/lib/core/messenger/local.ts index 462a81c02a..0d673787b9 100644 --- a/src/lib/core/messenger/local.ts +++ b/src/lib/core/messenger/local.ts @@ -115,20 +115,20 @@ export class Messenger extends EventEmitter implements IMessenger { } if (application && application.messenger && (to === 'application' || to === 'both')) { - application.messenger._onMessage({ action, data }); + application.messenger.onMessage({ action, data }); } if (agent && agent.messenger && (to === 'agent' || to === 'both')) { - agent.messenger._onMessage({ action, data }); + agent.messenger.onMessage({ action, data }); } if (opposite && opposite.messenger && to === 'opposite') { - opposite.messenger._onMessage({ action, data }); + opposite.messenger.onMessage({ action, data }); } }); return this; } - _onMessage(message: any) { + onMessage(message: any) { if (typeof message?.action === 'string') { debug('[%s] got message %s with %j', this.pid, message.action, message.data); this.emit(message.action, message.data); diff --git a/src/lib/core/singleton.ts b/src/lib/core/singleton.ts index f736c272d9..1c0d584792 100644 --- a/src/lib/core/singleton.ts +++ b/src/lib/core/singleton.ts @@ -22,7 +22,7 @@ export class Singleton { assert(options.name, '[egg:singleton] Singleton#constructor options.name is required'); assert(options.app, '[egg:singleton] Singleton#constructor options.app is required'); assert(options.create, '[egg:singleton] Singleton#constructor options.create is required'); - assert(!options.app[options.name], `${options.name} is already exists in app`); + assert(!(options.name in options.app), `[egg:singleton] ${options.name} is already exists in app`); this.app = options.app; this.name = options.name; this.create = options.create; @@ -36,12 +36,12 @@ export class Singleton { initSync() { const options = this.options; assert(!(options.client && options.clients), - `egg:singleton ${this.name} can not set options.client and options.clients both`); + `[egg:singleton] ${this.name} can not set options.client and options.clients both`); // alias app[name] as client, but still support createInstance method if (options.client) { const client = this.createInstance(options.client, options.name); - this.app[this.name] = client; + this.#setClientToApp(client); this.#extendDynamicMethods(client); return; } @@ -52,23 +52,23 @@ export class Singleton { const client = this.createInstance(options.clients[id], id); this.clients.set(id, client); }); - this.app[this.name] = this; + this.#setClientToApp(this); return; } // no config.clients and config.client - this.app[this.name] = this; + this.#setClientToApp(this); } async initAsync() { const options = this.options; assert(!(options.client && options.clients), - `egg:singleton ${this.name} can not set options.client and options.clients both`); + `[egg:singleton] ${this.name} can not set options.client and options.clients both`); // alias app[name] as client, but still support createInstance method if (options.client) { const client = await this.createInstanceAsync(options.client, options.name); - this.app[this.name] = client; + this.#setClientToApp(client); this.#extendDynamicMethods(client); return; } @@ -79,12 +79,16 @@ export class Singleton { return this.createInstanceAsync(options.clients[id], id) .then(client => this.clients.set(id, client)); })); - this.app[this.name] = this; + this.#setClientToApp(this); return; } // no config.clients and config.client - this.app[this.name] = this; + this.#setClientToApp(this); + } + + #setClientToApp(client: unknown) { + Reflect.set(this.app, this.name, client); } get(id: string) { @@ -131,10 +135,10 @@ export class Singleton { extendable.createInstance = this.createInstance.bind(this); extendable.createInstanceAsync = this.createInstanceAsync.bind(this); } catch (err) { - this.app.logger.warn( - 'egg:singleton %s dynamic create is disabled because of client is un-extendable', + this.app.coreLogger.warn( + '[egg:singleton] %s dynamic create is disabled because of client is un-extendable', this.name); - this.app.logger.warn(err); + this.app.coreLogger.warn(err); } } } diff --git a/src/lib/egg.ts b/src/lib/egg.ts index f175a9170e..8cabdefd8a 100644 --- a/src/lib/egg.ts +++ b/src/lib/egg.ts @@ -256,7 +256,7 @@ export class EggApplication extends EggCore { env: this.config.env, }; - function delegate(res, app, keys) { + function delegate(res: any, app: any, keys: string[]) { for (const key of keys) { if (app[key]) { res[key] = app[key]; @@ -264,7 +264,7 @@ export class EggApplication extends EggCore { } } - function abbr(res, app, keys) { + function abbr(res: any, app: any, keys: string[]) { for (const key of keys) { if (app[key]) { res[key] = ``; diff --git a/src/lib/loader/agent_worker_loader.ts b/src/lib/loader/AgentWorkerLoader.ts similarity index 100% rename from src/lib/loader/agent_worker_loader.ts rename to src/lib/loader/AgentWorkerLoader.ts diff --git a/src/lib/loader/app_worker_loader.ts b/src/lib/loader/AppWorkerLoader.ts similarity index 100% rename from src/lib/loader/app_worker_loader.ts rename to src/lib/loader/AppWorkerLoader.ts diff --git a/src/lib/loader/index.ts b/src/lib/loader/index.ts index 3c3a934832..0c4b332a11 100644 --- a/src/lib/loader/index.ts +++ b/src/lib/loader/index.ts @@ -1,3 +1,3 @@ export { EggApplicationLoader } from './EggApplicationLoader.js'; -export * from './app_worker_loader.js'; -export * from './agent_worker_loader.js'; +export * from './AppWorkerLoader.js'; +export * from './AgentWorkerLoader.js'; diff --git a/src/lib/start.ts b/src/lib/start.ts index 6d07d58f77..ebdf7576aa 100644 --- a/src/lib/start.ts +++ b/src/lib/start.ts @@ -38,9 +38,13 @@ export async function startEgg(options: StartOptions = {}) { ApplicationClass = framework.Application; } - const agent = new AgentClass(Object.assign({}, options)); + const agent = new AgentClass({ + ...options, + }); await agent.ready(); - const application = new ApplicationClass(Object.assign({}, options)); + const application = new ApplicationClass({ + ...options, + }); application.agent = agent; agent.application = application; await application.ready();