Skip to content

Commit a4430eb

Browse files
committed
feat(core): add ManifestStore.fromBundle for bundled artifacts (#5876)
Adds static `ManifestStore.fromBundle(data, baseDir)` that creates a store from pre-validated bundled manifest data, skipping lockfile/config fingerprint checks. Part of #5863 split. Tracking: #5871. Stacked on: #5867 (A1) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent e046356 commit a4430eb

2 files changed

Lines changed: 108 additions & 0 deletions

File tree

packages/core/src/loader/manifest.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,24 @@ export class ManifestStore {
8484
return new ManifestStore(data, baseDir);
8585
}
8686

87+
/**
88+
* Create a ManifestStore from pre-validated bundled data.
89+
* Skips invalidation checks — the caller (bundler) is responsible for
90+
* guaranteeing the data matches the shipped artifact.
91+
*/
92+
static fromBundle(data: StartupManifest, baseDir: string): ManifestStore {
93+
if (!data || data.version !== MANIFEST_VERSION) {
94+
throw new Error(
95+
`[@eggjs/core] bundled manifest version mismatch: expected ${MANIFEST_VERSION}, got ${data?.version}`,
96+
);
97+
}
98+
if (!data.invalidation) {
99+
throw new Error('[@eggjs/core] bundled manifest missing invalidation data');
100+
}
101+
debug('manifest loaded from bundle');
102+
return new ManifestStore(data, baseDir);
103+
}
104+
87105
/**
88106
* Create a collector-only ManifestStore (no cached data).
89107
* Used during normal startup to collect data for future manifest generation.

packages/core/test/loader/manifest.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import mm from 'mm';
77
import { describe, it, beforeEach, afterEach } from 'vitest';
88

99
import { ManifestStore } from '../../src/loader/manifest.ts';
10+
import type { StartupManifest } from '../../src/loader/manifest.ts';
1011
import { createTmpDir, setupBaseDir, generateAndWrite } from './manifest_helper.ts';
1112

1213
let tmpDir: string;
@@ -377,6 +378,95 @@ describe('ManifestStore', () => {
377378
});
378379
});
379380

381+
describe('fromBundle()', () => {
382+
it('should create store from bundled manifest data', () => {
383+
const baseDir = setupBaseDir();
384+
try {
385+
const manifest = ManifestStore.createCollector(baseDir).generateManifest({
386+
serverEnv: 'prod',
387+
serverScope: '',
388+
typescriptEnabled: true,
389+
});
390+
manifest.resolveCache['config/plugin'] = 'config/plugin.ts';
391+
392+
const store = ManifestStore.fromBundle(manifest, baseDir);
393+
const result = store.resolveModule(path.join(baseDir, 'config/plugin'), () => {
394+
throw new Error('should not be called');
395+
});
396+
397+
assert.equal(store.baseDir, baseDir);
398+
assert.equal(store.data, manifest);
399+
assert.equal(result, path.join(baseDir, 'config/plugin.ts'));
400+
} finally {
401+
fs.rmSync(baseDir, { recursive: true, force: true });
402+
}
403+
});
404+
405+
it('should throw when bundled manifest version mismatches', () => {
406+
const baseDir = setupBaseDir();
407+
try {
408+
const manifest = ManifestStore.createCollector(baseDir).generateManifest({
409+
serverEnv: 'prod',
410+
serverScope: '',
411+
typescriptEnabled: true,
412+
});
413+
manifest.version = 999;
414+
415+
assert.throws(
416+
() => ManifestStore.fromBundle(manifest, baseDir),
417+
/bundled manifest version mismatch: expected 1, got 999/,
418+
);
419+
} finally {
420+
fs.rmSync(baseDir, { recursive: true, force: true });
421+
}
422+
});
423+
424+
it('should throw when bundled manifest is missing invalidation data', () => {
425+
const baseDir = setupBaseDir();
426+
try {
427+
const manifest = ManifestStore.createCollector(baseDir).generateManifest({
428+
serverEnv: 'prod',
429+
serverScope: '',
430+
typescriptEnabled: true,
431+
});
432+
delete (manifest as Partial<StartupManifest>).invalidation;
433+
434+
assert.throws(() => ManifestStore.fromBundle(manifest, baseDir), /bundled manifest missing invalidation data/);
435+
} finally {
436+
fs.rmSync(baseDir, { recursive: true, force: true });
437+
}
438+
});
439+
440+
it('should throw a clear error when bundled manifest data is null', () => {
441+
assert.throws(
442+
() => ManifestStore.fromBundle(null as unknown as StartupManifest, tmpDir),
443+
/bundled manifest version mismatch: expected 1, got undefined/,
444+
);
445+
});
446+
447+
it('should bypass normal invalidation checks for bundled manifest data', async () => {
448+
const baseDir = setupBaseDir({ lockfile: 'pnpm' });
449+
try {
450+
const manifest = ManifestStore.createCollector(baseDir).generateManifest({
451+
serverEnv: 'prod',
452+
serverScope: '',
453+
typescriptEnabled: true,
454+
});
455+
manifest.invalidation.serverEnv = 'stale-env';
456+
manifest.invalidation.serverScope = 'stale-scope';
457+
manifest.invalidation.typescriptEnabled = !manifest.invalidation.typescriptEnabled;
458+
manifest.invalidation.lockfileFingerprint = 'stale-lockfile';
459+
manifest.invalidation.configFingerprint = 'stale-config';
460+
await ManifestStore.write(baseDir, manifest);
461+
462+
assert.equal(ManifestStore.load(baseDir, 'prod', ''), null);
463+
assert.doesNotThrow(() => ManifestStore.fromBundle(manifest, baseDir));
464+
} finally {
465+
fs.rmSync(baseDir, { recursive: true, force: true });
466+
}
467+
});
468+
});
469+
380470
describe('resolveModule()', () => {
381471
it('should return cached result from loaded manifest', async () => {
382472
const baseDir = setupBaseDir();

0 commit comments

Comments
 (0)