Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
92 changes: 41 additions & 51 deletions tools/egg-bundler/src/lib/EntryGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,15 @@ export class EntryGenerator {
}

const manifestJson = JSON.stringify(manifest, null, 2);
const frameworkSpec = JSON.stringify(this.#toFrameworkImportSpecifier());
const frameworkJson = JSON.stringify(this.#framework);

const externalBlock =
externalSpecs.length > 0
? `
// External-package files: loaded at runtime via require(), not bundled.
// Uses createRequire + dynamic specifiers so @utoo/pack cannot trace them.
import { createRequire as __createRequire } from 'node:module';
const __rtReq = __createRequire(path.join(__baseDir, 'package.json'));
const __rtReq = __createRequire(path.join(__appBaseDir, 'package.json'));
const __EXTERNAL_SPECS: Array<[string, string]> = ${JSON.stringify(externalSpecs)};
for (const [key, spec] of __EXTERNAL_SPECS) {
__BUNDLE_MAP_REL[key] = __rtReq(spec);
Expand All @@ -212,17 +212,30 @@ for (const [key, spec] of __EXTERNAL_SPECS) {

return `// ⚠️ auto-generated by @eggjs/egg-bundler — do not edit
/* eslint-disable */
import fs from 'node:fs';
import path from 'node:path';

import { ManifestStore } from '@eggjs/core';
import { startEgg } from ${frameworkSpec};
import { startEgg } from "egg";

${importLines.join('\n')}

// Derive the runtime output directory from the entry file being executed.
// Cannot use __dirname because turbopack replaces it with the compile-time
// path of the INPUT file, not the OUTPUT directory.
const __baseDir = path.dirname(path.resolve(process.argv[1] || '.'));
const __outputDir = process.argv[1] ? path.dirname(path.resolve(process.argv[1])) : process.cwd();
type __BundleManifest = { baseDir?: string; framework?: string };
function __readBundleManifest(): __BundleManifest {
try {
return JSON.parse(fs.readFileSync(path.join(__outputDir, 'bundle-manifest.json'), 'utf8')) as __BundleManifest;
} catch {
return {};
}
}

const __bundleManifest = __readBundleManifest();
const __appBaseDir = __bundleManifest.baseDir ? path.resolve(__outputDir, __bundleManifest.baseDir) : __outputDir;
Comment thread
killagu marked this conversation as resolved.
Outdated
const __framework = __bundleManifest.framework || ${frameworkJson};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const MANIFEST_DATA = ${manifestJson} as const;

Expand All @@ -231,23 +244,40 @@ ${mapLines.join('\n')}
};
${externalBlock}
const __BUNDLE_MAP: Record<string, unknown> = {};
function __toMapKey(filepath: string): string {
return filepath.split(path.win32.sep).join(path.posix.sep);
}

function __setBundleMapAlias(key: string | undefined, mod: unknown): void {
if (!key) return;
__BUNDLE_MAP[__toMapKey(key)] = mod;
}

for (const [rel, mod] of Object.entries(__BUNDLE_MAP_REL)) {
const abs = path.resolve(__baseDir, rel).split(path.sep).join('/');
__BUNDLE_MAP[abs] = mod;
// Also key by posix join so callers that already hand us posix paths hit.
__BUNDLE_MAP[rel] = mod;
__setBundleMapAlias(rel, mod);
__setBundleMapAlias(path.resolve(__appBaseDir, rel), mod);
__setBundleMapAlias(path.resolve(__outputDir, rel), mod);
}

for (const [requestRel, resolvedRel] of Object.entries(MANIFEST_DATA.resolveCache ?? {})) {
if (!resolvedRel) continue;
const mod = __BUNDLE_MAP[__toMapKey(resolvedRel)];
if (mod === undefined) continue;
__setBundleMapAlias(requestRel, mod);
__setBundleMapAlias(path.resolve(__appBaseDir, requestRel), mod);
__setBundleMapAlias(path.resolve(__outputDir, requestRel), mod);
}

const __bundleGlobalThis = globalThis as typeof globalThis & {
__EGG_BUNDLE_MODULE_LOADER__?: (filepath: string) => unknown;
};
ManifestStore.setBundleStore(ManifestStore.fromBundle(MANIFEST_DATA as any, __baseDir));
ManifestStore.setBundleStore(ManifestStore.fromBundle(MANIFEST_DATA as any, __appBaseDir));
__bundleGlobalThis.__EGG_BUNDLE_MODULE_LOADER__ = (filepath) => {
const key = filepath.split(path.sep).join('/');
const key = __toMapKey(filepath);
return __BUNDLE_MAP[key];
};

startEgg({ baseDir: __baseDir, mode: 'single' }).then((app) => {
startEgg({ baseDir: __appBaseDir, framework: __framework, mode: 'single' }).then((app) => {
Comment thread
killagu marked this conversation as resolved.
const port = process.env.PORT || app.config.cluster?.listen?.port || 7001;
app.listen(port, () => {
// eslint-disable-next-line no-console
Expand All @@ -261,46 +291,6 @@ startEgg({ baseDir: __baseDir, mode: 'single' }).then((app) => {
`;
}

#toFrameworkImportSpecifier(): string {
if (!path.isAbsolute(this.#framework)) return this.#framework;
const packageName = this.#packageNameFromDir(this.#framework);
if (packageName && this.#canUseFrameworkPackageName(packageName, this.#framework)) {
return packageName;
}
return this.#toImportSpecifier(this.#framework);
}

#packageNameFromDir(dir: string): string | undefined {
try {
const req = createRequire(path.join(dir, 'package.json'));
const pkg = req(path.join(dir, 'package.json')) as { name?: unknown };
return typeof pkg.name === 'string' && pkg.name ? pkg.name : undefined;
} catch {
return undefined;
}
}

#canUseFrameworkPackageName(packageName: string, dir: string): boolean {
if (this.#isInsideDir(path.join(this.#baseDir, 'node_modules'), dir)) return true;

try {
const req = createRequire(path.join(this.#baseDir, 'package.json'));
const resolvedPackageJson = req.resolve(`${packageName}/package.json`);
return this.#samePath(path.dirname(resolvedPackageJson), dir);
} catch {
return false;
}
}

#isInsideDir(parent: string, dir: string): boolean {
const rel = path.relative(path.resolve(parent), path.resolve(dir));
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
}

#samePath(left: string, right: string): boolean {
return path.resolve(left) === path.resolve(right);
}

#toImportSpecifier(absPath: string): string {
// Prefer a relative specifier from the entry output dir to keep the
// bundled paths portable across machines (absolute paths would leak
Expand Down
54 changes: 24 additions & 30 deletions tools/egg-bundler/test/EntryGenerator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,21 @@ describe('EntryGenerator', () => {

expect(worker).toContain("import { ManifestStore } from '@eggjs/core'");
expect(worker).toContain('import { startEgg } from "egg"');
expect(worker).toContain(
'const __outputDir = process.argv[1] ? path.dirname(path.resolve(process.argv[1])) : process.cwd()',
);
expect(worker).toContain(
'const __appBaseDir = __bundleManifest.baseDir ? path.resolve(__outputDir, __bundleManifest.baseDir) : __outputDir',
);
expect(worker).toContain('ManifestStore.setBundleStore(ManifestStore.fromBundle(MANIFEST_DATA');
expect(worker).toContain('__EGG_BUNDLE_MODULE_LOADER__');
expect(worker).toContain("startEgg({ baseDir: __baseDir, mode: 'single' })");
expect(worker).toContain("startEgg({ baseDir: __appBaseDir, framework: __framework, mode: 'single' })");
});

it('builds a BUNDLE_MAP keyed by both the relKey form and the resolved absolute form', async () => {
it('builds a BUNDLE_MAP keyed by relKey, app absolute, output absolute, and resolveCache request aliases', async () => {
const manifest = makeManifest({
fileDiscovery: { app: ['controller.ts'] },
resolveCache: { 'app/controller': 'app/controller.ts' },
});

const gen = new EntryGenerator({ baseDir: tmpDir, manifestLoader: createFakeLoader(manifest) });
Expand All @@ -188,8 +195,13 @@ describe('EntryGenerator', () => {

expect(worker).toContain('__BUNDLE_MAP_REL');
expect(worker).toContain('["app/controller.ts"]: __m0');
expect(worker).toContain('__BUNDLE_MAP[abs] = mod');
expect(worker).toContain('__BUNDLE_MAP[rel] = mod');
expect(worker).toContain('__setBundleMapAlias(rel, mod)');
expect(worker).toContain('__setBundleMapAlias(path.resolve(__appBaseDir, rel), mod)');
expect(worker).toContain('__setBundleMapAlias(path.resolve(__outputDir, rel), mod)');
expect(worker).toContain(
'for (const [requestRel, resolvedRel] of Object.entries(MANIFEST_DATA.resolveCache ?? {}))',
);
expect(worker).toContain('__setBundleMapAlias(path.resolve(__appBaseDir, requestRel), mod)');
});

it('loads externalized package files via createRequire instead of static imports', async () => {
Expand Down Expand Up @@ -242,7 +254,7 @@ describe('EntryGenerator', () => {
const worker = await fs.readFile(result.workerEntry, 'utf8');

expect(extractImports(worker).length).toBe(0);
expect(worker).toContain("startEgg({ baseDir: __baseDir, mode: 'single' })");
expect(worker).toContain("startEgg({ baseDir: __appBaseDir, framework: __framework, mode: 'single' })");
expect(worker).toContain('__EGG_BUNDLE_MODULE_LOADER__');
expect(worker).toContain('ManifestStore.setBundleStore');
});
Expand Down Expand Up @@ -270,7 +282,7 @@ describe('EntryGenerator', () => {
expect(path.dirname(result.workerEntry)).toBe(customOut);
});

it('honors a custom framework specifier', async () => {
it('passes a custom framework specifier to startEgg', async () => {
const gen = new EntryGenerator({
baseDir: tmpDir,
framework: '@my-org/framework',
Expand All @@ -279,28 +291,12 @@ describe('EntryGenerator', () => {
const result = await gen.generate();
const worker = await fs.readFile(result.workerEntry, 'utf8');

expect(worker).toContain('import { startEgg } from "@my-org/framework"');
expect(worker).not.toContain('import { startEgg } from "egg"');
});

it('uses the package name for an absolute framework directory with package metadata', async () => {
const frameworkDir = path.join(tmpDir, 'node_modules/custom-egg');
await fs.mkdir(frameworkDir, { recursive: true });
await fs.writeFile(path.join(frameworkDir, 'package.json'), JSON.stringify({ name: 'custom-egg' }));

const gen = new EntryGenerator({
baseDir: tmpDir,
framework: frameworkDir,
manifestLoader: createFakeLoader(makeManifest()),
});
const result = await gen.generate();
const worker = await fs.readFile(result.workerEntry, 'utf8');

expect(worker).toContain('import { startEgg } from "custom-egg"');
expect(worker).not.toContain(frameworkDir);
expect(worker).toContain('import { startEgg } from "egg"');
expect(worker).toContain('const __framework = __bundleManifest.framework || "@my-org/framework"');
expect(worker).toContain("startEgg({ baseDir: __appBaseDir, framework: __framework, mode: 'single' })");
});

it('keeps an absolute framework checkout relative when the app cannot resolve its package name', async () => {
it('passes an absolute framework checkout through startEgg', async () => {
const frameworkDir = await fs.mkdtemp(path.join(os.tmpdir(), 'egg-bundler-framework-'));
createdDirs.push(frameworkDir);
await fs.writeFile(path.join(frameworkDir, 'package.json'), JSON.stringify({ name: 'custom-egg' }));
Expand All @@ -312,11 +308,9 @@ describe('EntryGenerator', () => {
});
const result = await gen.generate();
const worker = await fs.readFile(result.workerEntry, 'utf8');
const relFramework = path.relative(result.entryDir, frameworkDir).replaceAll(path.sep, '/');

expect(worker).toContain(`import { startEgg } from "${relFramework}"`);
expect(worker).not.toContain('import { startEgg } from "custom-egg"');
expect(worker).not.toContain(frameworkDir);
expect(worker).toContain('import { startEgg } from "egg"');
expect(worker).toContain(`const __framework = __bundleManifest.framework || ${JSON.stringify(frameworkDir)}`);
});

it('produces byte-identical worker output across independent baseDir runs (T17 determinism baseline)', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// ⚠️ auto-generated by @eggjs/egg-bundler — do not edit
/* eslint-disable */
import fs from 'node:fs';
import path from 'node:path';

import { ManifestStore } from '@eggjs/core';
Expand All @@ -13,7 +14,19 @@ import * as __m3 from "../../node_modules/@eggjs/fake-module/app/service/UserSer
// Derive the runtime output directory from the entry file being executed.
// Cannot use __dirname because turbopack replaces it with the compile-time
// path of the INPUT file, not the OUTPUT directory.
const __baseDir = path.dirname(path.resolve(process.argv[1] || '.'));
const __outputDir = process.argv[1] ? path.dirname(path.resolve(process.argv[1])) : process.cwd();
type __BundleManifest = { baseDir?: string; framework?: string };
function __readBundleManifest(): __BundleManifest {
try {
return JSON.parse(fs.readFileSync(path.join(__outputDir, 'bundle-manifest.json'), 'utf8')) as __BundleManifest;
} catch {
return {};
}
}

const __bundleManifest = __readBundleManifest();
const __appBaseDir = __bundleManifest.baseDir ? path.resolve(__outputDir, __bundleManifest.baseDir) : __outputDir;
const __framework = __bundleManifest.framework || "egg";

const MANIFEST_DATA = {
"version": 1,
Expand Down Expand Up @@ -59,23 +72,40 @@ const __BUNDLE_MAP_REL: Record<string, unknown> = {
};

const __BUNDLE_MAP: Record<string, unknown> = {};
function __toMapKey(filepath: string): string {
return filepath.split(path.win32.sep).join(path.posix.sep);
}

function __setBundleMapAlias(key: string | undefined, mod: unknown): void {
if (!key) return;
__BUNDLE_MAP[__toMapKey(key)] = mod;
}

for (const [rel, mod] of Object.entries(__BUNDLE_MAP_REL)) {
const abs = path.resolve(__baseDir, rel).split(path.sep).join('/');
__BUNDLE_MAP[abs] = mod;
// Also key by posix join so callers that already hand us posix paths hit.
__BUNDLE_MAP[rel] = mod;
__setBundleMapAlias(rel, mod);
__setBundleMapAlias(path.resolve(__appBaseDir, rel), mod);
__setBundleMapAlias(path.resolve(__outputDir, rel), mod);
}

for (const [requestRel, resolvedRel] of Object.entries(MANIFEST_DATA.resolveCache ?? {})) {
if (!resolvedRel) continue;
const mod = __BUNDLE_MAP[__toMapKey(resolvedRel)];
if (mod === undefined) continue;
__setBundleMapAlias(requestRel, mod);
__setBundleMapAlias(path.resolve(__appBaseDir, requestRel), mod);
__setBundleMapAlias(path.resolve(__outputDir, requestRel), mod);
}

const __bundleGlobalThis = globalThis as typeof globalThis & {
__EGG_BUNDLE_MODULE_LOADER__?: (filepath: string) => unknown;
};
ManifestStore.setBundleStore(ManifestStore.fromBundle(MANIFEST_DATA as any, __baseDir));
ManifestStore.setBundleStore(ManifestStore.fromBundle(MANIFEST_DATA as any, __appBaseDir));
__bundleGlobalThis.__EGG_BUNDLE_MODULE_LOADER__ = (filepath) => {
const key = filepath.split(path.sep).join('/');
const key = __toMapKey(filepath);
return __BUNDLE_MAP[key];
};

startEgg({ baseDir: __baseDir, mode: 'single' }).then((app) => {
startEgg({ baseDir: __appBaseDir, framework: __framework, mode: 'single' }).then((app) => {
const port = process.env.PORT || app.config.cluster?.listen?.port || 7001;
app.listen(port, () => {
// eslint-disable-next-line no-console
Expand Down
Loading