From bb8605080aecfd2386c695464faec0e43cc3224f Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Mon, 6 Jul 2026 10:47:24 +0800 Subject: [PATCH 1/2] fix(core): FileLoader support .mjs as first-class (scan .mjs files) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The default FileLoader match glob only includes `.js`/`.ts` (and `.js` when TypeScript is off), so `.mjs` files are silently skipped. egg4 plugins built as ESM (e.g. via tsdown/tsc emitting `.mjs`) put their `app/middleware`, `app/extend`, `config` etc. in `.mjs`, which then never get loaded — e.g. `app.middlewares` ends up missing plugin middleware, and any lookup like `app.middlewares[name]` throws "Middleware xxx not found". Add `.mjs` to the default match so ESM plugins shipping `.mjs` are scanned the same as `.js`. In `type: module` packages `.js` is already ESM, but many build toolchains emit `.mjs` explicitly; treating `.mjs` as first-class avoids requiring every ESM plugin to re-emit as `.js`. --- packages/core/src/loader/file_loader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/loader/file_loader.ts b/packages/core/src/loader/file_loader.ts index 4b6c475971..b34748399a 100644 --- a/packages/core/src/loader/file_loader.ts +++ b/packages/core/src/loader/file_loader.ts @@ -64,7 +64,7 @@ export interface FileLoaderParseItem { type NormalizedFileLoaderOptions = FileLoaderOptions & Required>; function getDefaultFileLoaderMatch(): string[] { - return isSupportTypeScript() ? ['**/*.(js|ts)', '!**/*.d.ts'] : ['**/*.js']; + return isSupportTypeScript() ? ['**/*.(js|ts|mjs)', '!**/*.d.ts'] : ['**/*.{js,mjs}']; } /** From 4a2c566ccb90678ea28ba7f2508ad2161b076ce4 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Mon, 6 Jul 2026 11:10:13 +0800 Subject: [PATCH 2/2] fix(core): support .cjs too, and let .ts source win over compiled .mjs/.cjs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `.cjs` to the default FileLoader match alongside `.mjs`. - Extend the source-wins de-dup (previously `.js`-only) to `.mjs`/`.cjs`: when a `.ts` source and its compiled `.js`/`.mjs`/`.cjs` sibling coexist in the same directory (e.g. tsc/tsdown output kept beside the source during development), the `.ts` source wins and the compiled sibling is ignored — otherwise both are attached to the same property and `can't overwrite property` is thrown. (per @gemini-code-assist review) Priority when several extensions coexist for one basename: `.ts` (source) > compiled `.js`/`.mjs`/`.cjs`. Adds fixtures + a test covering standalone .mjs/.cjs loading and the .ts-over-.mjs / .ts-over-.cjs de-dup. --- packages/core/src/loader/file_loader.ts | 10 ++++++--- packages/core/test/egg-ts.test.ts | 22 +++++++++++++++++++ .../fixtures/egg-ts-js/app/service/dual.mjs | 7 ++++++ .../fixtures/egg-ts-js/app/service/dual.ts | 8 +++++++ .../fixtures/egg-ts-js/app/service/dualc.cjs | 7 ++++++ .../fixtures/egg-ts-js/app/service/dualc.ts | 7 ++++++ .../egg-ts-js/app/service/pureCjs.cjs | 6 +++++ .../egg-ts-js/app/service/pureMjs.mjs | 6 +++++ 8 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 packages/core/test/fixtures/egg-ts-js/app/service/dual.mjs create mode 100644 packages/core/test/fixtures/egg-ts-js/app/service/dual.ts create mode 100644 packages/core/test/fixtures/egg-ts-js/app/service/dualc.cjs create mode 100644 packages/core/test/fixtures/egg-ts-js/app/service/dualc.ts create mode 100644 packages/core/test/fixtures/egg-ts-js/app/service/pureCjs.cjs create mode 100644 packages/core/test/fixtures/egg-ts-js/app/service/pureMjs.mjs diff --git a/packages/core/src/loader/file_loader.ts b/packages/core/src/loader/file_loader.ts index b34748399a..7723a204b6 100644 --- a/packages/core/src/loader/file_loader.ts +++ b/packages/core/src/loader/file_loader.ts @@ -64,7 +64,7 @@ export interface FileLoaderParseItem { type NormalizedFileLoaderOptions = FileLoaderOptions & Required>; function getDefaultFileLoaderMatch(): string[] { - return isSupportTypeScript() ? ['**/*.(js|ts|mjs)', '!**/*.d.ts'] : ['**/*.{js,mjs}']; + return isSupportTypeScript() ? ['**/*.(js|ts|mjs|cjs)', '!**/*.d.ts'] : ['**/*.{js,mjs,cjs}']; } /** @@ -220,8 +220,12 @@ export class FileLoader { for (const filepath of filepaths) { const fullpath = path.join(directory, filepath); if (!this.options.loaderFS.stat(fullpath).isFile()) continue; - if (filepath.endsWith('.js')) { - const filepathTs = filepath.replace(/\.js$/, '.ts'); + // A compiled `.js` / `.mjs` / `.cjs` sitting next to its `.ts` source (e.g. `tsc` + // or `tsdown` output kept in the same directory during development) must not be + // loaded on top of the source file — otherwise both are attached to the same + // property and `can't overwrite property` is thrown. The `.ts` source always wins. + if (filepath.endsWith('.js') || filepath.endsWith('.mjs') || filepath.endsWith('.cjs')) { + const filepathTs = filepath.replace(/\.(?:js|mjs|cjs)$/, '.ts'); if (filepaths.includes(filepathTs)) { debug('[parse] ignore %s, because %s exists', fullpath, filepathTs); continue; diff --git a/packages/core/test/egg-ts.test.ts b/packages/core/test/egg-ts.test.ts index 69720e7fa9..82ce5379ad 100644 --- a/packages/core/test/egg-ts.test.ts +++ b/packages/core/test/egg-ts.test.ts @@ -127,6 +127,28 @@ describe('test/egg-ts.test.ts', () => { assert(app.serviceClasses.test); }); + it('should load mjs/cjs and prefer the ts source over its compiled mjs/cjs', async () => { + mm(process.env, 'EGG_TYPESCRIPT', 'true'); + app = createApp('egg-ts-js'); + + // loadService must not throw "can't overwrite property" even though + // `dual.ts`+`dual.mjs` and `dualc.ts`+`dualc.cjs` coexist in the directory. + await app.loader.loadService(); + + // standalone `.mjs` / `.cjs` are scanned and loaded as first-class files + assert(app.serviceClasses.pureMjs); + assert.equal(app.serviceClasses.pureMjs.loadedFrom, 'mjs'); + assert(app.serviceClasses.pureCjs); + assert.equal(app.serviceClasses.pureCjs.loadedFrom, 'cjs'); + + // priority: when a `.ts` source and its compiled `.mjs`/`.cjs` coexist, + // the `.ts` source wins and the compiled sibling is ignored. + assert(app.serviceClasses.dual); + assert.equal(app.serviceClasses.dual.loadedFrom, 'ts'); + assert(app.serviceClasses.dualc); + assert.equal(app.serviceClasses.dualc.loadedFrom, 'ts'); + }); + it('should auto require tsconfig-paths', async () => { mm(process.env, 'EGG_TYPESCRIPT', 'true'); app = createApp('egg-ts-js-tsconfig-paths'); diff --git a/packages/core/test/fixtures/egg-ts-js/app/service/dual.mjs b/packages/core/test/fixtures/egg-ts-js/app/service/dual.mjs new file mode 100644 index 0000000000..e71d17e63d --- /dev/null +++ b/packages/core/test/fixtures/egg-ts-js/app/service/dual.mjs @@ -0,0 +1,7 @@ +// Compiled output of `dual.ts`. Must be ignored while `dual.ts` exists. +export default class DualServiceMjs { + static loadedFrom = 'mjs'; + which() { + return 'mjs'; + } +} diff --git a/packages/core/test/fixtures/egg-ts-js/app/service/dual.ts b/packages/core/test/fixtures/egg-ts-js/app/service/dual.ts new file mode 100644 index 0000000000..09d07bfa75 --- /dev/null +++ b/packages/core/test/fixtures/egg-ts-js/app/service/dual.ts @@ -0,0 +1,8 @@ +// `dual.ts` + `dual.mjs` coexist (compiled output kept beside the source). +// The `.ts` source must win; `dual.mjs` must be ignored. +module.exports = class DualService { + static loadedFrom = 'ts'; + which() { + return 'ts'; + } +}; diff --git a/packages/core/test/fixtures/egg-ts-js/app/service/dualc.cjs b/packages/core/test/fixtures/egg-ts-js/app/service/dualc.cjs new file mode 100644 index 0000000000..7c972a8e1d --- /dev/null +++ b/packages/core/test/fixtures/egg-ts-js/app/service/dualc.cjs @@ -0,0 +1,7 @@ +// Compiled output of `dualc.ts`. Must be ignored while `dualc.ts` exists. +module.exports = class DualcServiceCjs { + static loadedFrom = 'cjs'; + which() { + return 'cjs'; + } +}; diff --git a/packages/core/test/fixtures/egg-ts-js/app/service/dualc.ts b/packages/core/test/fixtures/egg-ts-js/app/service/dualc.ts new file mode 100644 index 0000000000..154ac3148b --- /dev/null +++ b/packages/core/test/fixtures/egg-ts-js/app/service/dualc.ts @@ -0,0 +1,7 @@ +// `dualc.ts` + `dualc.cjs` coexist. The `.ts` source must win; `dualc.cjs` ignored. +module.exports = class DualcService { + static loadedFrom = 'ts'; + which() { + return 'ts'; + } +}; diff --git a/packages/core/test/fixtures/egg-ts-js/app/service/pureCjs.cjs b/packages/core/test/fixtures/egg-ts-js/app/service/pureCjs.cjs new file mode 100644 index 0000000000..c10bb003dd --- /dev/null +++ b/packages/core/test/fixtures/egg-ts-js/app/service/pureCjs.cjs @@ -0,0 +1,6 @@ +module.exports = class PureCjsService { + static loadedFrom = 'cjs'; + cjs() { + return 'from cjs service'; + } +}; diff --git a/packages/core/test/fixtures/egg-ts-js/app/service/pureMjs.mjs b/packages/core/test/fixtures/egg-ts-js/app/service/pureMjs.mjs new file mode 100644 index 0000000000..f31b5a247a --- /dev/null +++ b/packages/core/test/fixtures/egg-ts-js/app/service/pureMjs.mjs @@ -0,0 +1,6 @@ +export default class PureMjsService { + static loadedFrom = 'mjs'; + mjs() { + return 'from mjs service'; + } +}