Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 7 additions & 0 deletions packages/core/src/global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { ManifestStore } from './loader/manifest.ts';

declare global {
var __EGG_BUNDLE_STORE__: ManifestStore | undefined;
}

export {};
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '@eggjs/typings/global';
import type {} from './global.ts';
import utils from './utils/index.ts';

export { utils };
Expand Down
25 changes: 15 additions & 10 deletions packages/core/src/loader/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const debug = debuglog('egg/core/loader/manifest');
const MANIFEST_VERSION = 1;

const LOCKFILE_NAMES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock'] as const;
const BUNDLE_STORE_KEY = '__EGG_BUNDLE_STORE__' as const;

export interface ManifestInvalidation {
lockfileFingerprint: string;
Expand Down Expand Up @@ -51,32 +52,32 @@ export class ManifestStore {

/**
* Register a pre-built manifest store for bundled egg apps. When set,
* `ManifestStore.load()` returns this store unconditionally, bypassing
* disk reads and invalidation checks. The bundler-generated entry calls
* this at startup before creating the Application.
* `ManifestStore.load()` returns this store for matching baseDir requests,
* bypassing disk reads and invalidation checks. The bundler-generated entry
* calls this at startup before creating the Application.
*
* Uses globalThis so that bundled and external copies of @eggjs/core
* share the same store instance.
*/
static setBundleStore(store: ManifestStore | undefined): void {
(globalThis as any).__EGG_BUNDLE_STORE__ = store;
globalThis[BUNDLE_STORE_KEY] = store;
}

/**
* Return the registered bundle store, if any.
*/
static getBundleStore(): ManifestStore | undefined {
return (globalThis as any).__EGG_BUNDLE_STORE__;
return globalThis[BUNDLE_STORE_KEY];
}

/**
* Load and validate manifest from `.egg/manifest.json`.
* Returns null if manifest doesn't exist or is invalid.
*/
static load(baseDir: string, serverEnv: string, serverScope: string): ManifestStore | null {
const bundleStore: ManifestStore | undefined = (globalThis as any).__EGG_BUNDLE_STORE__;
if (bundleStore) {
debug('load: returning registered bundle store');
const bundleStore = ManifestStore.getBundleStore();
if (bundleStore && bundleStore.baseDir === baseDir) {
debug('load: returning registered bundle store for %s', baseDir);
return bundleStore;
}
if (serverEnv === 'local' && process.env.EGG_MANIFEST !== 'true') {
Expand Down Expand Up @@ -115,11 +116,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);
}

Expand Down
163 changes: 163 additions & 0 deletions packages/core/test/loader/manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,6 +19,7 @@ describe('ManifestStore', () => {

afterEach(() => {
mm.restore();
ManifestStore.setBundleStore(undefined);
fs.rmSync(tmpDir, { recursive: true, force: true });
});

Expand Down Expand Up @@ -257,6 +259,15 @@ describe('ManifestStore', () => {
});

describe('load()', () => {
function createBundleStore(baseDir: string, serverEnv = 'prod') {
const manifest = ManifestStore.createCollector(baseDir).generateManifest({
serverEnv,
serverScope: '',
typescriptEnabled: true,
});
return ManifestStore.fromBundle(manifest, baseDir);
}

it('should load a valid manifest', async () => {
const baseDir = setupBaseDir();
try {
Expand All @@ -270,6 +281,69 @@ describe('ManifestStore', () => {
}
});

it('should return registered bundle store when manifest file does not exist', () => {
const baseDir = setupBaseDir();
try {
const bundleStore = createBundleStore(baseDir);
ManifestStore.setBundleStore(bundleStore);

const store = ManifestStore.load(baseDir, 'prod', '');
assert.equal(store, bundleStore);
} finally {
fs.rmSync(baseDir, { recursive: true, force: true });
}
});

it('should return registered bundle store before reading invalid manifest JSON', () => {
const baseDir = setupBaseDir();
try {
const eggDir = path.join(baseDir, '.egg');
fs.mkdirSync(eggDir, { recursive: true });
fs.writeFileSync(path.join(eggDir, 'manifest.json'), 'not json{{{');

const bundleStore = createBundleStore(baseDir);
ManifestStore.setBundleStore(bundleStore);

const store = ManifestStore.load(baseDir, 'prod', '');
assert.equal(store, bundleStore);
} finally {
fs.rmSync(baseDir, { recursive: true, force: true });
}
});

it('should return registered bundle store in local env when EGG_MANIFEST is unset', () => {
const baseDir = setupBaseDir();
const savedEggManifest = process.env.EGG_MANIFEST;
try {
delete process.env.EGG_MANIFEST;
const bundleStore = createBundleStore(baseDir, 'local');
ManifestStore.setBundleStore(bundleStore);

const store = ManifestStore.load(baseDir, 'local', '');
assert.equal(store, bundleStore);
} finally {
if (savedEggManifest !== undefined) {
process.env.EGG_MANIFEST = savedEggManifest;
} else {
delete process.env.EGG_MANIFEST;
}
fs.rmSync(baseDir, { recursive: true, force: true });
}
});

it('should ignore registered bundle store for a different baseDir', () => {
const bundleBaseDir = setupBaseDir();
try {
const bundleStore = createBundleStore(bundleBaseDir);
ManifestStore.setBundleStore(bundleStore);

const store = ManifestStore.load(tmpDir, 'prod', '');
assert.equal(store, null);
} finally {
fs.rmSync(bundleBaseDir, { recursive: true, force: true });
}
});

it('should return null when manifest file does not exist', () => {
const store = ManifestStore.load(tmpDir, 'prod', '');
assert.equal(store, null);
Expand Down Expand Up @@ -377,6 +451,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<StartupManifest>).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();
Expand Down
62 changes: 50 additions & 12 deletions tools/egg-bundler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,59 @@
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./src/index.ts",
"./lib/Bundler": "./src/lib/Bundler.ts",
"./lib/EntryGenerator": "./src/lib/EntryGenerator.ts",
"./lib/ExternalsResolver": "./src/lib/ExternalsResolver.ts",
"./lib/ManifestLoader": "./src/lib/ManifestLoader.ts",
"./lib/PackRunner": "./src/lib/PackRunner.ts",
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
},
"./lib/Bundler": {
"types": "./src/lib/Bundler.ts",
"import": "./src/lib/Bundler.ts"
},
"./lib/EntryGenerator": {
"types": "./src/lib/EntryGenerator.ts",
"import": "./src/lib/EntryGenerator.ts"
},
"./lib/ExternalsResolver": {
"types": "./src/lib/ExternalsResolver.ts",
"import": "./src/lib/ExternalsResolver.ts"
},
"./lib/ManifestLoader": {
"types": "./src/lib/ManifestLoader.ts",
"import": "./src/lib/ManifestLoader.ts"
},
"./lib/PackRunner": {
"types": "./src/lib/PackRunner.ts",
"import": "./src/lib/PackRunner.ts"
},
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public",
"exports": {
".": "./dist/index.js",
"./lib/Bundler": "./dist/lib/Bundler.js",
"./lib/EntryGenerator": "./dist/lib/EntryGenerator.js",
"./lib/ExternalsResolver": "./dist/lib/ExternalsResolver.js",
"./lib/ManifestLoader": "./dist/lib/ManifestLoader.js",
"./lib/PackRunner": "./dist/lib/PackRunner.js",
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./lib/Bundler": {
"types": "./dist/lib/Bundler.d.ts",
"import": "./dist/lib/Bundler.js"
},
"./lib/EntryGenerator": {
"types": "./dist/lib/EntryGenerator.d.ts",
"import": "./dist/lib/EntryGenerator.js"
},
"./lib/ExternalsResolver": {
"types": "./dist/lib/ExternalsResolver.d.ts",
"import": "./dist/lib/ExternalsResolver.js"
},
"./lib/ManifestLoader": {
"types": "./dist/lib/ManifestLoader.d.ts",
"import": "./dist/lib/ManifestLoader.js"
},
"./lib/PackRunner": {
"types": "./dist/lib/PackRunner.d.ts",
"import": "./dist/lib/PackRunner.js"
},
"./package.json": "./package.json"
}
},
Expand All @@ -53,6 +89,7 @@
"@eggjs/core": "workspace:*",
"@eggjs/utils": "workspace:*",
"@utoo/pack": "catalog:",
"execa": "catalog:",
"tsx": "catalog:"
},
"devDependencies": {
Expand All @@ -63,6 +100,7 @@
"@eggjs/tegg-config": "workspace:*",
"@eggjs/tegg-plugin": "workspace:*",
"@types/node": "catalog:",
"@typescript/native-preview": "catalog:",
"rimraf": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
Expand Down
Loading
Loading