From b0c707f627a2483686cdb18b2359848231724d14 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Thu, 19 Dec 2024 17:33:47 +0800 Subject: [PATCH] fix: extend support Class --- example/middleware/hello.ts | 15 ++++++ package.json | 2 +- src/base_context_class.ts | 6 +-- src/egg.ts | 51 ++++++++++++++++--- src/loader/context_loader.ts | 2 +- src/loader/egg_loader.ts | 21 +++++--- .../extend-with-class/app/controller/home.js | 13 +++++ .../app/extend/application.js | 7 +++ .../extend-with-class/app/extend/context.js | 11 ++++ .../extend-with-class/app/extend/request.js | 7 +++ .../extend-with-class/app/extend/response.js | 17 +++++++ test/fixtures/extend-with-class/app/router.js | 3 ++ test/fixtures/extend-with-class/package.json | 4 ++ test/loader/mixin/load_extend_class.test.ts | 38 ++++++++++++++ 14 files changed, 176 insertions(+), 21 deletions(-) create mode 100644 example/middleware/hello.ts create mode 100644 test/fixtures/extend-with-class/app/controller/home.js create mode 100644 test/fixtures/extend-with-class/app/extend/application.js create mode 100644 test/fixtures/extend-with-class/app/extend/context.js create mode 100644 test/fixtures/extend-with-class/app/extend/request.js create mode 100644 test/fixtures/extend-with-class/app/extend/response.js create mode 100644 test/fixtures/extend-with-class/app/router.js create mode 100644 test/fixtures/extend-with-class/package.json create mode 100644 test/loader/mixin/load_extend_class.test.ts diff --git a/example/middleware/hello.ts b/example/middleware/hello.ts new file mode 100644 index 00000000..5649fb8d --- /dev/null +++ b/example/middleware/hello.ts @@ -0,0 +1,15 @@ +import { MiddlewareFunc } from '../../src/index.js'; + +export const hello: MiddlewareFunc = async (ctx, next) => { + console.log('Hello middleware'); + console.log(ctx.app.BaseContextClass); + console.log(ctx.app.Service); + console.log(ctx.service); + console.log(ctx.app.timing); + console.log(ctx.app.lifecycle); + console.log(ctx.request.ctx.app.timing); + console.log(ctx.request.app.timing); + console.log(ctx.request.response.app.timing); + console.log(ctx.response.request.app.timing); + await next(); +}; diff --git a/package.json b/package.json index b334b095..0683cf16 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ }, "homepage": "https://github.com/eggjs/egg-core#readme", "dependencies": { - "@eggjs/koa": "^2.19.2", + "@eggjs/koa": "^2.20.1", "@eggjs/router": "^3.0.5", "@eggjs/utils": "^4.0.2", "egg-logger": "^3.5.0", diff --git a/src/base_context_class.ts b/src/base_context_class.ts index 349982e9..ee9f542f 100644 --- a/src/base_context_class.ts +++ b/src/base_context_class.ts @@ -1,4 +1,4 @@ -import type { EggCore, EggCoreContext } from './egg.js'; +import type { EggCore, ContextDelegation } from './egg.js'; /** * BaseContextClass is a base class that can be extended, @@ -6,7 +6,7 @@ import type { EggCore, EggCoreContext } from './egg.js'; * {@link Helper}, {@link Service} is extending it. */ export class BaseContextClass { - ctx: EggCoreContext; + ctx: ContextDelegation; app: EggCore; config: Record; service: BaseContextClass; @@ -14,7 +14,7 @@ export class BaseContextClass { /** * @since 1.0.0 */ - constructor(ctx: EggCoreContext) { + constructor(ctx: ContextDelegation) { /** * @member {Context} BaseContextClass#ctx * @since 1.0.0 diff --git a/src/egg.ts b/src/egg.ts index f6e48bc4..79d8fd56 100644 --- a/src/egg.ts +++ b/src/egg.ts @@ -1,8 +1,15 @@ /* eslint-disable prefer-spread */ import assert from 'node:assert'; import { debuglog } from 'node:util'; -import { Application as KoaApplication } from '@eggjs/koa'; -import type { ContextDelegation, MiddlewareFunc } from '@eggjs/koa'; +import { + Application as KoaApplication, Context as KoaContext, + Request as KoaRequest, Response as KoaResponse, +} from '@eggjs/koa'; +import type { + ContextDelegation as KoaContextDelegation, + MiddlewareFunc as KoaMiddlewareFunc, + Next, +} from '@eggjs/koa'; import { EggConsoleLogger } from 'egg-logger'; import { RegisterOptions, ResourcesController, EggRouter as Router } from '@eggjs/router'; import type { ReadyFunctionArg } from 'get-ready'; @@ -15,7 +22,7 @@ import utils from './utils/index.js'; const debug = debuglog('@eggjs/core:egg'); -const EGG_LOADER = Symbol.for('egg#loader'); +export const EGG_LOADER = Symbol.for('egg#loader'); export interface EggCoreOptions { baseDir: string; @@ -27,12 +34,40 @@ export interface EggCoreOptions { export type EggCoreInitOptions = Partial; -export type { ContextDelegation, MiddlewareFunc, Next } from '@eggjs/koa'; +// export @eggjs/koa classes +export { + KoaRequest, KoaResponse, KoaContext, KoaApplication, +}; + +// export @eggjs/koa types +export type { + Next, KoaMiddlewareFunc, KoaContextDelegation, +}; + +// export @eggjs/core classes +export class Request extends KoaRequest { + declare app: EggCore; + declare response: Response; + declare ctx: ContextDelegation; +} + +export class Response extends KoaResponse { + declare app: EggCore; + declare request: Request; + declare ctx: ContextDelegation; +} -export interface EggCoreContext extends ContextDelegation { - app: EggCore; +export class Context extends KoaContext { + declare app: EggCore; + declare request: Request; + declare response: Response; + declare service: BaseContextClass; } +// export @eggjs/core types +export type ContextDelegation = KoaContextDelegation & Context; +export type MiddlewareFunc = KoaMiddlewareFunc; + export class EggCore extends KoaApplication { options: EggCoreOptions; timing: Timing; @@ -159,7 +194,7 @@ export class EggCore extends KoaApplication { use(fn: MiddlewareFunc) { assert(typeof fn === 'function', 'app.use() requires a function'); debug('[use] add middleware: %o', fn._name || fn.name || '-'); - this.middleware.push(fn); + this.middleware.push(fn as unknown as KoaMiddlewareFunc); return this; } @@ -229,7 +264,7 @@ export class EggCore extends KoaApplication { * * @see https://eggjs.org/en/advanced/loader.html#beforestart * - * @param {Function|AsyncFunction} scope function will execute before app start + * @param {Function} scope function will execute before app start * @param {string} [name] scope name, default is empty string */ beforeStart(scope: Fun, name?: string) { diff --git a/src/loader/context_loader.ts b/src/loader/context_loader.ts index 9222425c..532f6c50 100644 --- a/src/loader/context_loader.ts +++ b/src/loader/context_loader.ts @@ -1,7 +1,7 @@ import assert from 'node:assert'; -import { type ContextDelegation } from '@eggjs/koa'; import { isClass, isPrimitive } from 'is-type-of'; import { FileLoader, EXPORTS, type FileLoaderOptions } from './file_loader.js'; +import type { ContextDelegation } from '../egg.js'; const CLASS_LOADER = Symbol('classLoader'); diff --git a/src/loader/egg_loader.ts b/src/loader/egg_loader.ts index 5f2ef4cc..16b6c177 100644 --- a/src/loader/egg_loader.ts +++ b/src/loader/egg_loader.ts @@ -15,10 +15,10 @@ import { ContextLoader, ContextLoaderOptions } from './context_loader.js'; import utils, { Fun } from '../utils/index.js'; import sequencify from '../utils/sequencify.js'; import { Timing } from '../utils/timing.js'; -import type { EggCoreContext, EggCore, MiddlewareFunc } from '../egg.js'; +import type { ContextDelegation, EggCore, MiddlewareFunc } from '../egg.js'; import { BaseContextClass } from '../base_context_class.js'; -const debug = debuglog('@eggjs/core:egg_loader'); +const debug = debuglog('@eggjs/core/loader/egg_loader'); const originalPrototypes: Record = { request: Request.prototype, @@ -1064,7 +1064,7 @@ export class EggLoader { for (const rawFilepath of filepaths) { const filepath = this.resolveModule(rawFilepath)!; if (!filepath) { - debug('loadExtend %o not found', rawFilepath); + // debug('loadExtend %o not found', rawFilepath); continue; } if (filepath.endsWith('/index.js')) { @@ -1073,9 +1073,14 @@ export class EggLoader { this.app.deprecate(`app/extend/${name}/index.ts is deprecated, use app/extend/${name}.ts instead`); } - const ext = await this.requireFile(filepath); + let ext = await this.requireFile(filepath); + // if extend object is Class, should use Class.prototype instead + if (isClass(ext)) { + ext = ext.prototype; + } const properties = Object.getOwnPropertyNames(ext) - .concat(Object.getOwnPropertySymbols(ext) as any[]); + .concat(Object.getOwnPropertySymbols(ext) as any[]) + .filter(name => name !== 'constructor'); // ignore class constructor for extend for (const property of properties) { if (mergeRecord.has(property)) { @@ -1108,7 +1113,7 @@ export class EggLoader { Object.defineProperty(proto, property, descriptor!); mergeRecord.set(property, filepath); } - debug('merge %j to %s from %s', Object.keys(ext), name, filepath); + debug('merge %j to %s from %s', properties, name, filepath); } this.timing.end(`Load extend/${name}.js`); } @@ -1682,7 +1687,7 @@ function wrapControllerClass(Controller: typeof BaseContextClass, fullPath: stri } function controllerMethodToMiddleware(Controller: typeof BaseContextClass, key: string) { - return function classControllerMiddleware(this: EggCoreContext, ...args: any[]) { + return function classControllerMiddleware(this: ContextDelegation, ...args: any[]) { const controller: any = new Controller(this); if (!this.app.config.controller?.supportParams) { args = [ this ]; @@ -1718,7 +1723,7 @@ function wrapObject(obj: Record, fullPath: string, prefix?: string) } function objectFunctionToMiddleware(func: Fun) { - async function objectControllerMiddleware(this: EggCoreContext, ...args: any[]) { + async function objectControllerMiddleware(this: ContextDelegation, ...args: any[]) { if (!this.app.config.controller?.supportParams) { args = [ this ]; } diff --git a/test/fixtures/extend-with-class/app/controller/home.js b/test/fixtures/extend-with-class/app/controller/home.js new file mode 100644 index 00000000..68be9621 --- /dev/null +++ b/test/fixtures/extend-with-class/app/controller/home.js @@ -0,0 +1,13 @@ +export default async function() { + const status = Number(this.query.status || 200); + this.status = status; + this.etag = '2.2.2.2'; + this.body = { + returnAppContext: this.appContext, + returnAppRequest: this.request.appRequest, + returnAppResponse: this.response.appResponse, + returnAppApplication: this.app.appApplication, + status: this.status, + etag: this.etag, + }; +}; diff --git a/test/fixtures/extend-with-class/app/extend/application.js b/test/fixtures/extend-with-class/app/extend/application.js new file mode 100644 index 00000000..f8e99e20 --- /dev/null +++ b/test/fixtures/extend-with-class/app/extend/application.js @@ -0,0 +1,7 @@ +import { EggCore } from '../../../../../src/index.js' + +export default class Application extends EggCore { + get appApplication() { + return 'app application'; + } +}; diff --git a/test/fixtures/extend-with-class/app/extend/context.js b/test/fixtures/extend-with-class/app/extend/context.js new file mode 100644 index 00000000..4abeccd0 --- /dev/null +++ b/test/fixtures/extend-with-class/app/extend/context.js @@ -0,0 +1,11 @@ +import { Context } from '../../../../../src/index.js' + +export default class MyContext extends Context { + get appContext() { + return this.app ? 'app context' : 'no app context'; + } + + ajax() { + return 'app ajax patch'; + } +} diff --git a/test/fixtures/extend-with-class/app/extend/request.js b/test/fixtures/extend-with-class/app/extend/request.js new file mode 100644 index 00000000..1ed931b0 --- /dev/null +++ b/test/fixtures/extend-with-class/app/extend/request.js @@ -0,0 +1,7 @@ +import { Request } from '../../../../../src/index.js' + +export default class AppRequest extends Request { + get appRequest() { + return this.response.app.timing ? 'app request' : 'no app request'; + } +} diff --git a/test/fixtures/extend-with-class/app/extend/response.js b/test/fixtures/extend-with-class/app/extend/response.js new file mode 100644 index 00000000..e380911d --- /dev/null +++ b/test/fixtures/extend-with-class/app/extend/response.js @@ -0,0 +1,17 @@ +import { Response } from '../../../../../src/index.js' + +export default class AppResponse extends Response { + get appResponse() { + return this.app.timing ? 'app response' : 'no app response'; + } + + set status(code) { + this._explicitStatus = true; + this.res.statusCode = code; + this.res.statusMessage = 'http status code ' + code; + } + + get etag() { + return 'etag ok'; + } +} diff --git a/test/fixtures/extend-with-class/app/router.js b/test/fixtures/extend-with-class/app/router.js new file mode 100644 index 00000000..f98396bc --- /dev/null +++ b/test/fixtures/extend-with-class/app/router.js @@ -0,0 +1,3 @@ +export default (app) => { + app.get('/', app.controller.home); +}; diff --git a/test/fixtures/extend-with-class/package.json b/test/fixtures/extend-with-class/package.json new file mode 100644 index 00000000..55f7f253 --- /dev/null +++ b/test/fixtures/extend-with-class/package.json @@ -0,0 +1,4 @@ +{ + "name": "extend-with-class", + "type": "module" +} diff --git a/test/loader/mixin/load_extend_class.test.ts b/test/loader/mixin/load_extend_class.test.ts new file mode 100644 index 00000000..dd7e92cd --- /dev/null +++ b/test/loader/mixin/load_extend_class.test.ts @@ -0,0 +1,38 @@ +import { strict as assert } from 'node:assert'; +import request from 'supertest'; +import mm from 'mm'; +import { Application, createApp } from '../../helper.js'; + +describe('test/loader/mixin/load_extend_class.test.ts', () => { + let app: Application; + before(async () => { + app = createApp('extend-with-class'); + await app.loader.loadPlugin(); + await app.loader.loadConfig(); + await app.loader.loadRequestExtend(); + await app.loader.loadResponseExtend(); + await app.loader.loadApplicationExtend(); + await app.loader.loadContextExtend(); + await app.loader.loadController(); + await app.loader.loadRouter(); + await app.loader.loadMiddleware(); + }); + after(() => app.close()); + afterEach(mm.restore); + + it('should load app.context app.request app.response', () => { + assert((app as any).appApplication); + + return request(app.callback()) + .get('/') + .expect({ + returnAppContext: 'app context', + returnAppRequest: 'app request', + returnAppResponse: 'app response', + returnAppApplication: 'app application', + status: 200, + etag: 'etag ok', + }) + .expect(200); + }); +});