Skip to content
Merged
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
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
1 change: 1 addition & 0 deletions tools/egg-bundler/src/lib/Bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class Bundler {
baseDir: absBaseDir,
manifestPath,
framework,
autoGenerate: true,
});
await wrapStep('manifest load', () => manifestLoader.load());

Expand Down
Loading
Loading