Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/core/src/loader/file_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment on lines +95 to +98
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard inferred manifest shape before assigning it

At Line 97, options.inject.loader?.manifest is assigned without validating it is a real manifest store. If inject contains an unrelated loader.manifest value, parse() will later fail when calling globFiles(...).

Proposed fix
-    if (!options.manifest && options.inject) {
-      options.manifest = options.inject.loader?.manifest;
-    }
+    if (!options.manifest && options.inject) {
+      const candidate = (options.inject as { loader?: { manifest?: unknown } }).loader?.manifest;
+      if (candidate && typeof (candidate as ManifestStore).globFiles === 'function') {
+        options.manifest = candidate as ManifestStore;
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/loader/file_loader.ts` around lines 95 - 98, The code
auto-assigns options.manifest from options.inject.loader?.manifest without
validating shape, which can cause parse() (and its call to globFiles(...)) to
fail; fix by checking the inferred manifest is the expected manifest store
before assignment—ensure options.inject.loader?.manifest is an object and
implements the required API (e.g., has a callable globFiles method) and only
then set options.manifest; otherwise leave it undefined so parse() can handle
missing manifest gracefully.

this.options = {
caseStyle: CaseStyle.camel,
call: true,
Expand Down
103 changes: 103 additions & 0 deletions packages/core/test/loader/manifest_coverage.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading