Skip to content

fix(core): auto-inject manifest into FileLoader/ContextLoader getters#5857

Open
killagu wants to merge 1 commit intonextfrom
feat/manifest-complete-coverage
Open

fix(core): auto-inject manifest into FileLoader/ContextLoader getters#5857
killagu wants to merge 1 commit intonextfrom
feat/manifest-complete-coverage

Conversation

@killagu
Copy link
Copy Markdown
Contributor

@killagu killagu commented Apr 1, 2026

Summary

  • Plugins that create FileLoader instances via app.loader.FileLoader (e.g., the schedule plugin) were bypassing the manifest system because the getter returned the raw class without injecting the manifest store
  • Now the FileLoader and ContextLoader getters return subclasses that auto-inject the manifest, ensuring all file discovery and resolution is captured for snapshot/manifest generation
  • Added comprehensive tests verifying manifest injection works for both FileLoader and ContextLoader created through the getter API

Problem

When plugins do:

const loader = new app.loader.FileLoader({ directory, target, inject: app });
await loader.load();

The FileLoader getter previously returned the raw FileLoader class, so the created instances had no manifest attached. This meant file discovery results from these loaders were lost and not included in the generated manifest.

Solution

The FileLoader and ContextLoader getters now return anonymous subclasses that auto-inject this.manifest into the constructor options:

get FileLoader(): typeof FileLoader {
  const manifest = this.manifest;
  return class ManifestFileLoader extends FileLoader {
    constructor(options: FileLoaderOptions) {
      super({ manifest, ...options });
    }
  } as typeof FileLoader;
}

This is transparent to callers — they use the same API, but the manifest is always injected.

Test plan

  • Test that generateManifest() produces non-empty fileDiscovery and resolveCache after loading
  • Test that FileLoader created via app.loader.FileLoader getter auto-injects manifest
  • Test that ContextLoader created via app.loader.ContextLoader getter auto-injects manifest
  • Verify all manifest keys are relative paths (not absolute)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests

    • Introduced comprehensive test suite validating manifest generation across loader phases, file discovery population, and path normalization constraints.
  • Bug Fixes

    • FileLoader now automatically initializes manifest configuration from the application instance, ensuring consistent file discovery behavior.

Copilot AI review requested due to automatic review settings April 1, 2026 15:39
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

A new test suite validates manifest auto-injection in FileLoader, and the FileLoader constructor is modified to automatically resolve manifest from the application context when not explicitly provided, enabling manifest-driven file discovery.

Changes

Cohort / File(s) Summary
Manifest Auto-Injection Implementation
packages/core/src/loader/file_loader.ts
Modified FileLoader constructor to auto-set options.manifest from options.inject.loader?.manifest when manifest is not explicitly provided, enabling downstream manifest-driven file discovery.
Manifest Coverage Test Suite
packages/core/test/loader/manifest_coverage.test.ts
New comprehensive test suite validating manifest auto-injection behavior, verifying generateManifest() populates resolveCache and fileDiscovery correctly, and enforcing relative path normalization constraints across all manifest entries.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • fengmk2
  • gxkl
  • jerryliang64

Poem

🐰 Manifest paths now flow so true,
Auto-injected, fresh and new,
Loaders bind with context near,
File discovery crystal clear!
Relative paths, no absolute cheer—
The loader's magic draws us here.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: auto-injecting manifest into FileLoader/ContextLoader getters to fix a problem where plugins creating loaders via getters weren't capturing file discovery in the manifest.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/manifest-complete-coverage

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 1, 2026

Deploying egg with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0fd269b
Status: ✅  Deploy successful!
Preview URL: https://3c8d59da.egg-cci.pages.dev
Branch Preview URL: https://feat-manifest-complete-cover.egg-cci.pages.dev

View logs

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.28%. Comparing base (36d126b) to head (0fd269b).

Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5857      +/-   ##
==========================================
- Coverage   85.28%   85.28%   -0.01%     
==========================================
  Files         666      666              
  Lines       13247    13249       +2     
  Branches     1538     1539       +1     
==========================================
+ Hits        11298    11299       +1     
- Misses       1818     1819       +1     
  Partials      131      131              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 1, 2026

Deploying egg-v3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0fd269b
Status: ✅  Deploy successful!
Preview URL: https://3b2bc86d.egg-v3.pages.dev
Branch Preview URL: https://feat-manifest-complete-cover.egg-v3.pages.dev

View logs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/core/test/loader/manifest_coverage.test.ts (1)

45-74: Use try/finally for per-test app cleanup.

await testApp.close() at the tail won’t run if an assertion throws. Wrapping each test body in try/finally makes cleanup deterministic and reduces test leakage risk.

As per coding guidelines, "Use Vitest as the standard test runner for all packages; test files follow pattern 'test/**/*.test.ts'".

Also applies to: 76-102

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/test/loader/manifest_coverage.test.ts` around lines 45 - 74,
The test creates a testApp and calls await testApp.close() only at the end,
which can be skipped if an assertion throws; update the test body that
constructs testApp (the it block using createApp('context-loader') and variable
testApp and loader/FileLoader usage) to wrap the test logic in a try/finally and
call await testApp.close() in the finally so cleanup always runs; apply the same
try/finally pattern to the other similar test block around lines 76-102 that
also creates testApp.
packages/core/src/loader/egg_loader.ts (1)

1687-1696: Consider caching getter-generated subclasses for stable class identity.

Both getters create a new class per access. Caching once per EggLoader instance avoids identity drift and is friendlier for plugin code that stores/reuses constructor references.

Also applies to: 1702-1709

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/loader/egg_loader.ts` around lines 1687 - 1696, The
generated subclass in the FileLoader getter (ManifestFileLoader) is recreated on
every access causing unstable class identity; fix by caching the generated class
on the EggLoader instance (e.g. add a private field like _ManifestFileLoader or
_FileLoaderClass) and change the FileLoader getter to return the cached class if
present, otherwise create, store, and return it; apply the same caching pattern
to the other similar getter (the one around lines 1702-1709) so both getters
return a stable subclass reference across accesses while still injecting the
manifest in the constructor.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/loader/egg_loader.ts`:
- Around line 1692-1693: The constructor currently calls super({ manifest,
...options }) which allows a caller-supplied options.manifest (including
undefined) to override the intended injected manifest; change the call so the
injected manifest wins (e.g., pass the spread options first and then manifest so
manifest is last: super({ ...options, manifest }) or equivalently ensure
manifest is assigned as options.manifest ?? manifest) and apply the same change
to the other occurrence around lines 1705-1706; update the constructor in
egg_loader.ts (constructor and the second call site) to ensure manifest cannot
be disabled by caller options.

---

Nitpick comments:
In `@packages/core/src/loader/egg_loader.ts`:
- Around line 1687-1696: The generated subclass in the FileLoader getter
(ManifestFileLoader) is recreated on every access causing unstable class
identity; fix by caching the generated class on the EggLoader instance (e.g. add
a private field like _ManifestFileLoader or _FileLoaderClass) and change the
FileLoader getter to return the cached class if present, otherwise create,
store, and return it; apply the same caching pattern to the other similar getter
(the one around lines 1702-1709) so both getters return a stable subclass
reference across accesses while still injecting the manifest in the constructor.

In `@packages/core/test/loader/manifest_coverage.test.ts`:
- Around line 45-74: The test creates a testApp and calls await testApp.close()
only at the end, which can be skipped if an assertion throws; update the test
body that constructs testApp (the it block using createApp('context-loader') and
variable testApp and loader/FileLoader usage) to wrap the test logic in a
try/finally and call await testApp.close() in the finally so cleanup always
runs; apply the same try/finally pattern to the other similar test block around
lines 76-102 that also creates testApp.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 95daa734-e150-4d08-9c44-ffc8a7dc3b10

📥 Commits

Reviewing files that changed from the base of the PR and between 36d126b and 0036281.

📒 Files selected for processing (2)
  • packages/core/src/loader/egg_loader.ts
  • packages/core/test/loader/manifest_coverage.test.ts

Comment on lines +1692 to +1693
constructor(options: FileLoaderOptions) {
super({ manifest, ...options });
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

Manifest injection can be overridden by caller options.

Line 1693 and Line 1706 spread ...options after manifest, so options.manifest (including undefined) can disable the intended auto-injection.

✅ Proposed fix
 return class ManifestFileLoader extends FileLoader {
   constructor(options: FileLoaderOptions) {
-    super({ manifest, ...options });
+    super({ ...options, manifest });
   }
 } as typeof FileLoader;
 return class ManifestContextLoader extends ContextLoader {
   constructor(options: ContextLoaderOptions) {
-    super({ manifest, ...options });
+    super({ ...options, manifest });
   }
 } as typeof ContextLoader;

Also applies to: 1705-1706

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/loader/egg_loader.ts` around lines 1692 - 1693, The
constructor currently calls super({ manifest, ...options }) which allows a
caller-supplied options.manifest (including undefined) to override the intended
injected manifest; change the call so the injected manifest wins (e.g., pass the
spread options first and then manifest so manifest is last: super({ ...options,
manifest }) or equivalently ensure manifest is assigned as options.manifest ??
manifest) and apply the same change to the other occurrence around lines
1705-1706; update the constructor in egg_loader.ts (constructor and the second
call site) to ensure manifest cannot be disabled by caller options.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a manifest-collection gap in @eggjs/core where plugins creating loaders via app.loader.FileLoader / app.loader.ContextLoader were bypassing the manifest system, preventing directory scans and module resolutions from being captured for startup manifest generation.

Changes:

  • Updated EggLoader’s FileLoader and ContextLoader getters to return subclasses that inject this.manifest into loader options.
  • Added a new test suite to validate that manifest fileDiscovery/resolveCache are populated and that keys are stored as relative paths.
  • Added targeted tests ensuring loaders instantiated via the getter APIs participate in manifest collection.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
packages/core/src/loader/egg_loader.ts Wrapes FileLoader / ContextLoader getters with manifest-injecting subclasses to ensure manifest collection applies to plugin-created loaders.
packages/core/test/loader/manifest_coverage.test.ts Adds coverage tests verifying manifest injection and that collected manifest paths are relative.

Comment on lines +1688 to +1694
const manifest = this.manifest;
// Auto-inject manifest so plugins extending FileLoader
// automatically participate in manifest collection/caching
return class ManifestFileLoader extends FileLoader {
constructor(options: FileLoaderOptions) {
super({ manifest, ...options });
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The injected manifest can be overridden by callers because the spread order is { manifest, ...options }. This allows plugins to accidentally (or intentionally) pass manifest: undefined and bypass manifest collection again. To ensure the getter always injects the loader’s manifest, spread options first and then set manifest last.

Copilot uses AI. Check for mistakes.
Comment on lines +1687 to +1695
get FileLoader(): typeof FileLoader {
return FileLoader;
const manifest = this.manifest;
// Auto-inject manifest so plugins extending FileLoader
// automatically participate in manifest collection/caching
return class ManifestFileLoader extends FileLoader {
constructor(options: FileLoaderOptions) {
super({ manifest, ...options });
}
} as typeof FileLoader;
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

get FileLoader() / get ContextLoader() currently creates a new subclass on every property access. That changes the API semantics vs returning a stable class reference, and can break consumer checks like instanceof app.loader.FileLoader (because subsequent getter calls return a different constructor). Consider caching the generated subclass on the loader instance so the getter returns the same class each time.

Copilot uses AI. Check for mistakes.
Comment on lines +1703 to +1707
const manifest = this.manifest;
return class ManifestContextLoader extends ContextLoader {
constructor(options: ContextLoaderOptions) {
super({ manifest, ...options });
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

Same issue as FileLoader: { manifest, ...options } lets the caller override/clear the injected manifest, which undermines the guarantee that ContextLoader created via the getter participates in manifest collection. Prefer setting manifest after spreading options so it can’t be overridden.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request updates EggLoader to automatically inject the manifest into FileLoader and ContextLoader by returning subclasses from their getters, and adds corresponding test coverage. Feedback highlights that returning a new class definition on every access breaks class identity and instanceof checks, recommending memoization of the subclasses and safer merging of constructor options to prevent accidental manifest overrides.

Comment on lines 1687 to 1696
get FileLoader(): typeof FileLoader {
return FileLoader;
const manifest = this.manifest;
// Auto-inject manifest so plugins extending FileLoader
// automatically participate in manifest collection/caching
return class ManifestFileLoader extends FileLoader {
constructor(options: FileLoaderOptions) {
super({ manifest, ...options });
}
} as typeof FileLoader;
}
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.

high

Returning a new class definition on every access to the FileLoader getter breaks class identity (app.loader.FileLoader !== app.loader.FileLoader) and instanceof checks. This can lead to unexpected behavior in plugins or internal code that rely on class comparisons or type checking.

Additionally, the spread order { manifest, ...options } allows an explicit undefined in options.manifest to override the auto-injected manifest. It is safer to ensure the manifest is provided if missing.

Consider memoizing the subclass on the instance to maintain consistency.

  get FileLoader(): typeof FileLoader {
    if (!(this as any)._FileLoader) {
      const manifest = this.manifest;
      // Auto-inject manifest so plugins extending FileLoader
      // automatically participate in manifest collection/caching
      (this as any)._FileLoader = class ManifestFileLoader extends FileLoader {
        constructor(options: FileLoaderOptions) {
          super({ ...options, manifest: options.manifest ?? manifest });
        }
      } as typeof FileLoader;
    }
    return (this as any)._FileLoader;
  }

Comment on lines 1702 to 1709
get ContextLoader(): typeof ContextLoader {
return ContextLoader;
const manifest = this.manifest;
return class ManifestContextLoader extends ContextLoader {
constructor(options: ContextLoaderOptions) {
super({ manifest, ...options });
}
} as typeof ContextLoader;
}
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.

high

Similar to the FileLoader getter, returning a new class definition here breaks class identity and instanceof checks. The subclass should be memoized on the instance to ensure consistency across multiple accesses.

Also, the spread order should be adjusted to prevent options.manifest from accidentally overriding the injected manifest with null or undefined.

  get ContextLoader(): typeof ContextLoader {
    if (!(this as any)._ContextLoader) {
      const manifest = this.manifest;
      (this as any)._ContextLoader = class ManifestContextLoader extends ContextLoader {
        constructor(options: ContextLoaderOptions) {
          super({ ...options, manifest: options.manifest ?? manifest });
        }
      } as typeof ContextLoader;
    }
    return (this as any)._ContextLoader;
  }

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 <[email protected]>
@killagu killagu force-pushed the feat/manifest-complete-coverage branch from 0036281 to 0fd269b Compare April 1, 2026 15:57
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/loader/file_loader.ts`:
- Around line 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.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 994cda2a-9697-4585-b2af-c612a64e5eb8

📥 Commits

Reviewing files that changed from the base of the PR and between 0036281 and 0fd269b.

📒 Files selected for processing (2)
  • packages/core/src/loader/file_loader.ts
  • packages/core/test/loader/manifest_coverage.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/test/loader/manifest_coverage.test.ts

Comment on lines +95 to +98
// Auto-resolve manifest from inject (the app) when not explicitly provided
if (!options.manifest && options.inject) {
options.manifest = options.inject.loader?.manifest;
}
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.

@jerryliang64
Copy link
Copy Markdown
Contributor

PR title/body 描述的方案是修改 EggLoaderget FileLoader() / get ContextLoader() getter 返回匿名子类,但实际提交的代码没有修改任何 getter(egg_loader.ts 未变),而是在 FileLoader 构造函数中通过 options.inject.loader?.manifest 自动解析。

实际方案比描述的更好(更简单、不需要改 getter、ContextLoader 自动继承生效),只是 title 和 body 需要同步更新以匹配实际实现,避免 reviewer 对着描述找不到对应代码。

Copy link
Copy Markdown
Contributor

@jerryliang64 jerryliang64 left a comment

Choose a reason for hiding this comment

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants