From 0fd269bf5d55b5987361e1aa120d80fe66c5bf19 Mon Sep 17 00:00:00 2001 From: killagu Date: Wed, 1 Apr 2026 22:02:45 +0800 Subject: [PATCH] feat(core): auto-inject manifest into FileLoader/ContextLoader getters FileLoader now auto-resolves manifest from inject.loader?.manifest when not explicitly provided. This replaces the previous anonymous subclass approach in EggLoader getters, making the code simpler and more elegant while ensuring all file discovery and resolution is captured for snapshot/manifest generation. Co-Authored-By: Claude Opus 4.6 --- packages/core/src/loader/file_loader.ts | 4 + .../test/loader/manifest_coverage.test.ts | 103 ++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 packages/core/test/loader/manifest_coverage.test.ts diff --git a/packages/core/src/loader/file_loader.ts b/packages/core/src/loader/file_loader.ts index afb9e664e9..9a55d1e8b0 100644 --- a/packages/core/src/loader/file_loader.ts +++ b/packages/core/src/loader/file_loader.ts @@ -92,6 +92,10 @@ export class FileLoader { constructor(options: FileLoaderOptions) { assert(options.directory, 'options.directory is required'); assert(options.target, 'options.target is required'); + // Auto-resolve manifest from inject (the app) when not explicitly provided + if (!options.manifest && options.inject) { + options.manifest = options.inject.loader?.manifest; + } this.options = { caseStyle: CaseStyle.camel, call: true, diff --git a/packages/core/test/loader/manifest_coverage.test.ts b/packages/core/test/loader/manifest_coverage.test.ts new file mode 100644 index 0000000000..7b8cec42e8 --- /dev/null +++ b/packages/core/test/loader/manifest_coverage.test.ts @@ -0,0 +1,103 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; + +import { describe, it, afterAll } from 'vitest'; + +import { createApp, getFilepath, type Application } from '../helper.js'; + +describe('ManifestStore coverage: FileLoader getter auto-injects manifest', () => { + let app: Application; + + afterAll(() => app?.close()); + + it('should collect fileDiscovery when plugin uses app.loader.FileLoader', async () => { + app = createApp('middleware-override'); + await app.loader.loadPlugin(); + await app.loader.loadConfig(); + await app.loader.loadCustomApp(); + await app.loader.loadMiddleware(); + await app.loader.loadController(); + await app.loader.loadRouter(); + + const manifest = app.loader.generateManifest(); + + // resolveCache should contain resolved files (extend files, router, configs, boot hooks) + assert.ok(Object.keys(manifest.resolveCache).length > 0, 'resolveCache should have entries'); + + // fileDiscovery should contain directory scans from middleware, controller loading + assert.ok(Object.keys(manifest.fileDiscovery).length > 0, 'fileDiscovery should have entries'); + + // All resolveCache keys should be relative paths + for (const key of Object.keys(manifest.resolveCache)) { + assert.ok(!path.isAbsolute(key), `resolveCache key should be relative: ${key}`); + const value = manifest.resolveCache[key]; + if (value !== null) { + assert.ok(!path.isAbsolute(value), `resolveCache value should be relative: ${value}`); + } + } + + // All fileDiscovery keys should be relative paths + for (const key of Object.keys(manifest.fileDiscovery)) { + assert.ok(!path.isAbsolute(key), `fileDiscovery key should be relative: ${key}`); + } + }); + + it('should auto-inject manifest into FileLoader created via app.loader.FileLoader', async () => { + const testApp = createApp('context-loader'); + await testApp.loader.loadPlugin(); + await testApp.loader.loadConfig(); + await testApp.loader.loadCustomApp(); + + // Use app.loader.FileLoader (the getter) to create a loader like plugins do + const CustomLoader = testApp.loader.FileLoader; + const target = {}; + const directory = getFilepath('context-loader/app/service'); + const loader = new CustomLoader({ + directory, + target, + inject: testApp, + }); + await loader.load(); + + // Generate manifest and verify fileDiscovery captured the directory scan + const manifest = testApp.loader.generateManifest(); + const relDir = path.relative(testApp.loader.options.baseDir, directory).replaceAll(path.sep, '/'); + + assert.ok( + relDir in manifest.fileDiscovery, + `fileDiscovery should contain '${relDir}', got keys: ${Object.keys(manifest.fileDiscovery).join(', ')}`, + ); + assert.ok(Array.isArray(manifest.fileDiscovery[relDir])); + assert.ok(manifest.fileDiscovery[relDir].length > 0, 'fileDiscovery should have files'); + + await testApp.close(); + }); + + it('should auto-inject manifest into ContextLoader created via app.loader.ContextLoader', async () => { + const testApp = createApp('context-loader'); + await testApp.loader.loadPlugin(); + await testApp.loader.loadConfig(); + await testApp.loader.loadCustomApp(); + + // Use app.loader.ContextLoader (the getter) + const CustomContextLoader = testApp.loader.ContextLoader; + const directory = getFilepath('context-loader/app/service'); + const loader = new CustomContextLoader({ + directory, + property: 'testService', + inject: testApp, + }); + await loader.load(); + + // Generate manifest and verify fileDiscovery captured the directory scan + const manifest = testApp.loader.generateManifest(); + const relDir = path.relative(testApp.loader.options.baseDir, directory).replaceAll(path.sep, '/'); + + assert.ok( + relDir in manifest.fileDiscovery, + `fileDiscovery should contain '${relDir}', got keys: ${Object.keys(manifest.fileDiscovery).join(', ')}`, + ); + + await testApp.close(); + }); +});