From 832ae52acfdaffcc6f128ca0ba02540a3bdfa66c Mon Sep 17 00:00:00 2001 From: killagu Date: Tue, 14 Apr 2026 10:55:06 +0800 Subject: [PATCH 1/2] feat(core): add ManifestStore.fromBundle for bundled artifacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `ManifestStore.fromBundle(data, baseDir)` that creates a store from pre-validated bundled manifest data, skipping the lockfile / config fingerprint checks that would fail for a frozen artifact shipped across environments. Only the manifest `version` field is validated — the caller (bundler) is responsible for guaranteeing the data matches the artifact. Required by the upcoming @eggjs/egg-bundler to bootstrap a bundled Egg app without re-running filesystem discovery. --- packages/core/src/loader/manifest.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/core/src/loader/manifest.ts b/packages/core/src/loader/manifest.ts index 7674d7f9cd..e0a08d81e3 100644 --- a/packages/core/src/loader/manifest.ts +++ b/packages/core/src/loader/manifest.ts @@ -84,6 +84,20 @@ export class ManifestStore { return new ManifestStore(data, baseDir); } + /** + * Create a ManifestStore from pre-validated bundled data. + * Skips invalidation checks — the caller (bundler) is responsible for + * guaranteeing the data matches the shipped artifact. + */ + static fromBundle(data: StartupManifest, baseDir: string): ManifestStore { + if (data.version !== MANIFEST_VERSION) { + throw new Error( + `[@eggjs/core] bundled manifest version mismatch: expected ${MANIFEST_VERSION}, got ${data.version}`, + ); + } + return new ManifestStore(data, baseDir); + } + /** * Create a collector-only ManifestStore (no cached data). * Used during normal startup to collect data for future manifest generation. From 60804b346815adaa7f1b59acef30a8c1872d1454 Mon Sep 17 00:00:00 2001 From: killa Date: Sat, 25 Apr 2026 20:07:28 +0800 Subject: [PATCH 2/2] test(core): cover ManifestStore.fromBundle --- packages/core/src/loader/manifest.ts | 8 +- packages/core/test/loader/manifest.test.ts | 90 ++++++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/packages/core/src/loader/manifest.ts b/packages/core/src/loader/manifest.ts index e0a08d81e3..f36645bed2 100644 --- a/packages/core/src/loader/manifest.ts +++ b/packages/core/src/loader/manifest.ts @@ -90,11 +90,15 @@ export class ManifestStore { * guaranteeing the data matches the shipped artifact. */ static fromBundle(data: StartupManifest, baseDir: string): ManifestStore { - if (data.version !== MANIFEST_VERSION) { + if (!data || data.version !== MANIFEST_VERSION) { throw new Error( - `[@eggjs/core] bundled manifest version mismatch: expected ${MANIFEST_VERSION}, got ${data.version}`, + `[@eggjs/core] bundled manifest version mismatch: expected ${MANIFEST_VERSION}, got ${data?.version}`, ); } + if (!data.invalidation) { + throw new Error('[@eggjs/core] bundled manifest missing invalidation data'); + } + debug('manifest loaded from bundle'); return new ManifestStore(data, baseDir); } diff --git a/packages/core/test/loader/manifest.test.ts b/packages/core/test/loader/manifest.test.ts index fba88a14e7..09b821397b 100644 --- a/packages/core/test/loader/manifest.test.ts +++ b/packages/core/test/loader/manifest.test.ts @@ -7,6 +7,7 @@ import mm from 'mm'; import { describe, it, beforeEach, afterEach } from 'vitest'; import { ManifestStore } from '../../src/loader/manifest.ts'; +import type { StartupManifest } from '../../src/loader/manifest.ts'; import { createTmpDir, setupBaseDir, generateAndWrite } from './manifest_helper.ts'; let tmpDir: string; @@ -377,6 +378,95 @@ describe('ManifestStore', () => { }); }); + describe('fromBundle()', () => { + it('should create store from bundled manifest data', () => { + const baseDir = setupBaseDir(); + try { + const manifest = ManifestStore.createCollector(baseDir).generateManifest({ + serverEnv: 'prod', + serverScope: '', + typescriptEnabled: true, + }); + manifest.resolveCache['config/plugin'] = 'config/plugin.ts'; + + const store = ManifestStore.fromBundle(manifest, baseDir); + const result = store.resolveModule(path.join(baseDir, 'config/plugin'), () => { + throw new Error('should not be called'); + }); + + assert.equal(store.baseDir, baseDir); + assert.equal(store.data, manifest); + assert.equal(result, path.join(baseDir, 'config/plugin.ts')); + } finally { + fs.rmSync(baseDir, { recursive: true, force: true }); + } + }); + + it('should throw when bundled manifest version mismatches', () => { + const baseDir = setupBaseDir(); + try { + const manifest = ManifestStore.createCollector(baseDir).generateManifest({ + serverEnv: 'prod', + serverScope: '', + typescriptEnabled: true, + }); + manifest.version = 999; + + assert.throws( + () => ManifestStore.fromBundle(manifest, baseDir), + /bundled manifest version mismatch: expected 1, got 999/, + ); + } finally { + fs.rmSync(baseDir, { recursive: true, force: true }); + } + }); + + it('should throw when bundled manifest is missing invalidation data', () => { + const baseDir = setupBaseDir(); + try { + const manifest = ManifestStore.createCollector(baseDir).generateManifest({ + serverEnv: 'prod', + serverScope: '', + typescriptEnabled: true, + }); + delete (manifest as Partial).invalidation; + + assert.throws(() => ManifestStore.fromBundle(manifest, baseDir), /bundled manifest missing invalidation data/); + } finally { + fs.rmSync(baseDir, { recursive: true, force: true }); + } + }); + + it('should throw a clear error when bundled manifest data is null', () => { + assert.throws( + () => ManifestStore.fromBundle(null as unknown as StartupManifest, tmpDir), + /bundled manifest version mismatch: expected 1, got undefined/, + ); + }); + + it('should bypass normal invalidation checks for bundled manifest data', async () => { + const baseDir = setupBaseDir({ lockfile: 'pnpm' }); + try { + const manifest = ManifestStore.createCollector(baseDir).generateManifest({ + serverEnv: 'prod', + serverScope: '', + typescriptEnabled: true, + }); + manifest.invalidation.serverEnv = 'stale-env'; + manifest.invalidation.serverScope = 'stale-scope'; + manifest.invalidation.typescriptEnabled = !manifest.invalidation.typescriptEnabled; + manifest.invalidation.lockfileFingerprint = 'stale-lockfile'; + manifest.invalidation.configFingerprint = 'stale-config'; + await ManifestStore.write(baseDir, manifest); + + assert.equal(ManifestStore.load(baseDir, 'prod', ''), null); + assert.doesNotThrow(() => ManifestStore.fromBundle(manifest, baseDir)); + } finally { + fs.rmSync(baseDir, { recursive: true, force: true }); + } + }); + }); + describe('resolveModule()', () => { it('should return cached result from loaded manifest', async () => { const baseDir = setupBaseDir();