-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(bundler): enable @eggjs/* packages to be bundled by turbopack #5863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from all commits
eac1949
f6f0579
0775c7e
1fcc323
ec05987
2bf4985
eb06786
1c36565
9762836
55c2f40
828e4b9
27ac2a4
6119c44
ce53d94
9351cd0
c15f71f
b0d75fd
d6c25b1
d3515c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -376,8 +376,6 @@ export function importResolve(filepath: string, options?: ImportResolveOptions): | |
| */ | ||
| export type SnapshotModuleLoader = (resolvedPath: string) => any; | ||
|
|
||
| let _snapshotModuleLoader: SnapshotModuleLoader | undefined; | ||
|
|
||
| /** | ||
| * Register a snapshot module loader that intercepts `importModule()` calls. | ||
| * | ||
|
|
@@ -388,15 +386,46 @@ let _snapshotModuleLoader: SnapshotModuleLoader | undefined; | |
| * | ||
| * Also sets `isESM = false` because the snapshot bundle is CJS and | ||
| * esbuild's `import.meta` polyfill causes incorrect ESM detection. | ||
| * | ||
| * Uses globalThis so that bundled and external copies share the same loader. | ||
| */ | ||
| export function setSnapshotModuleLoader(loader: SnapshotModuleLoader): void { | ||
| _snapshotModuleLoader = loader; | ||
| (globalThis as any).__EGG_SNAPSHOT_MODULE_LOADER__ = loader; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 可以给 globalThis 扩展一个类型定义,这样就不需要靠 any 了 |
||
| isESM = false; | ||
| } | ||
|
|
||
| /** | ||
| * Module loader for bundled egg apps. Called with the raw `importModule()` | ||
| * filepath (posix-normalized) before `importResolve`, so bundled apps can | ||
| * serve modules that no longer exist on disk. Return `undefined` to fall | ||
| * through to the standard import path. | ||
| */ | ||
| export type BundleModuleLoader = (filepath: string) => unknown; | ||
|
|
||
| /** | ||
| * Register a bundle module loader. Uses globalThis so that bundled and | ||
| * external copies of @eggjs/utils share the same loader. | ||
| */ | ||
| export function setBundleModuleLoader(loader: BundleModuleLoader | undefined): void { | ||
| (globalThis as any).__EGG_BUNDLE_MODULE_LOADER__ = loader; | ||
| if (loader) isESM = false; | ||
|
Comment on lines
+406
to
+411
|
||
| } | ||
|
|
||
| export async function importModule(filepath: string, options?: ImportModuleOptions): Promise<any> { | ||
| const _bundleModuleLoader: BundleModuleLoader | undefined = (globalThis as any).__EGG_BUNDLE_MODULE_LOADER__; | ||
| if (_bundleModuleLoader) { | ||
| const hit = _bundleModuleLoader(filepath.replaceAll('\\', '/')); | ||
| if (hit !== undefined) { | ||
| let obj = hit as any; | ||
| if (obj?.default?.__esModule === true && 'default' in obj.default) obj = obj.default; | ||
| if (options?.importDefaultOnly && obj && typeof obj === 'object' && 'default' in obj) obj = obj.default; | ||
| return obj; | ||
| } | ||
| } | ||
|
|
||
| const moduleFilePath = importResolve(filepath, options); | ||
|
|
||
| const _snapshotModuleLoader: SnapshotModuleLoader | undefined = (globalThis as any).__EGG_SNAPSHOT_MODULE_LOADER__; | ||
| if (_snapshotModuleLoader) { | ||
| let obj = _snapshotModuleLoader(moduleFilePath); | ||
| if (obj && typeof obj === 'object' && obj.default?.__esModule === true && obj.default && 'default' in obj.default) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { strict as assert } from 'node:assert'; | ||
|
|
||
| import { afterEach, describe, it } from 'vitest'; | ||
|
|
||
| import { importModule, setBundleModuleLoader } from '../src/import.ts'; | ||
| import { getFilepath } from './helper.ts'; | ||
|
|
||
| describe('test/bundle-import.test.ts', () => { | ||
| afterEach(() => { | ||
| setBundleModuleLoader(undefined); | ||
| }); | ||
|
|
||
| it('returns the real module when no bundle loader is registered', async () => { | ||
| const result = await importModule(getFilepath('esm')); | ||
| assert.ok(result); | ||
| assert.equal(typeof result, 'object'); | ||
| }); | ||
|
|
||
| it('intercepts importModule with the registered loader', async () => { | ||
| const seen: string[] = []; | ||
| const fakeModule = { default: { hello: 'bundle' }, other: 'stuff' }; | ||
| setBundleModuleLoader((p) => { | ||
| seen.push(p); | ||
| if (p.endsWith('/fixtures/esm')) return fakeModule; | ||
| }); | ||
|
|
||
| const result = await importModule(getFilepath('esm')); | ||
| assert.deepEqual(result, fakeModule); | ||
| assert.ok(seen.some((p) => p.endsWith('/fixtures/esm'))); | ||
| }); | ||
|
|
||
| it('honors importDefaultOnly when the bundle hit has a default key', async () => { | ||
| setBundleModuleLoader(() => ({ default: { greet: 'hi' }, other: 'x' })); | ||
|
|
||
| const result = await importModule(getFilepath('esm'), { importDefaultOnly: true }); | ||
| assert.deepEqual(result, { greet: 'hi' }); | ||
| }); | ||
|
|
||
| it('unwraps __esModule double-default shape', async () => { | ||
| setBundleModuleLoader(() => ({ | ||
| default: { __esModule: true, default: { fn: 'bundled' } }, | ||
| })); | ||
|
|
||
| const result = await importModule(getFilepath('esm')); | ||
| assert.equal(result.__esModule, true); | ||
| assert.deepEqual(result.default, { fn: 'bundled' }); | ||
| }); | ||
|
|
||
| it('falls through to normal import when loader returns undefined', async () => { | ||
| setBundleModuleLoader(() => undefined); | ||
|
|
||
| const result = await importModule(getFilepath('esm')); | ||
| assert.ok(result); | ||
| }); | ||
|
|
||
| it('short-circuits importResolve so bundled paths need not exist on disk', async () => { | ||
| const fakeModule = { virtual: true }; | ||
| setBundleModuleLoader((p) => (p === 'virtual/not-on-disk' ? fakeModule : undefined)); | ||
|
|
||
| const result = await importModule('virtual/not-on-disk'); | ||
| assert.deepEqual(result, fakeModule); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -5,16 +5,15 @@ import type { Application, MiddlewareFunc } from 'egg'; | |||||
| import { readJSON } from 'utility'; | ||||||
|
|
||||||
| import { isTimingFile } from '../../utils.ts'; | ||||||
| import { LOADER_TRACE_TEMPLATE } from './loader_trace_template.ts'; | ||||||
|
|
||||||
| export default function createEggLoaderTraceMiddleware(_options: unknown, app: Application): MiddlewareFunc { | ||||||
| return async (ctx, next) => { | ||||||
| if (ctx.path !== '/__loader_trace__') { | ||||||
| return await next(); | ||||||
| } | ||||||
| const templatePath = path.join(import.meta.dirname, 'loader_trace.html'); | ||||||
| const template = await fs.readFile(templatePath, 'utf8'); | ||||||
| const data = await loadTimingData(app); | ||||||
| ctx.body = template.replace('{{placeholder}}', JSON.stringify(data)); | ||||||
| ctx.body = LOADER_TRACE_TEMPLATE.replace('{{placeholder}}', JSON.stringify(data)); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, locate and read the target file
cat -n plugins/development/src/app/middleware/egg_loader_trace.tsRepository: eggjs/egg Length of output: 1624 🏁 Script executed: # Read the template file
cat -n plugins/development/src/app/middleware/loader_trace_template.tsRepository: eggjs/egg Length of output: 1585 🏁 Script executed: # Search for ctx.helper or escape-related functions in the codebase
rg "helper\.escape|ctx\.helper" --type ts -A 2 -B 2 | head -50Repository: eggjs/egg Length of output: 4625 🏁 Script executed: # Search for escape function in security or core context
rg "escape\s*\(" --type ts plugins/security/ -B 3 -A 5 | head -100Repository: eggjs/egg Length of output: 681 🏁 Script executed: # Find the escape helper test
rg "escape" plugins/security/test/app/extends/helper.test.ts -B 5 -A 10Repository: eggjs/egg Length of output: 1540 🏁 Script executed: # Search for escape helper implementation in security plugin
rg "escape.*=" plugins/security/src --type ts -B 2 -A 5 | head -80Repository: eggjs/egg Length of output: 1151 🏁 Script executed: # Find escape function definition
fd ".*helper.*" plugins/security/src -type f | xargs grep -l "escape"Repository: eggjs/egg Length of output: 225 🏁 Script executed: # Find escape function definition
find plugins/security/src -name "*.ts" -type f | xargs grep -l "escape"Repository: eggjs/egg Length of output: 355 🏁 Script executed: # Read the escape helper
cat -n plugins/security/src/lib/helper/escape.tsRepository: eggjs/egg Length of output: 139 🏁 Script executed: # Check sjson helper which might be for safe JSON in scripts
cat -n plugins/security/src/lib/helper/sjson.tsRepository: eggjs/egg Length of output: 1082 🏁 Script executed: # Check sjs helper which is used by sjson
cat -n plugins/security/src/lib/helper/sjs.tsRepository: eggjs/egg Length of output: 1729 🏁 Script executed: # Check the helper index to see what's exported
cat -n plugins/security/src/lib/helper/index.tsRepository: eggjs/egg Length of output: 1011 🏁 Script executed: # Verify that sjson is available by checking core context extensions
rg "sjson" plugins/security/src -A 3 -B 1 | head -30Repository: eggjs/egg Length of output: 924 Use
The framework provides - ctx.body = LOADER_TRACE_TEMPLATE.replace('{{placeholder}}', JSON.stringify(data));
+ ctx.body = LOADER_TRACE_TEMPLATE.replace('{{placeholder}}', ctx.helper.sjson(data));📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| }; | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| /** Loader trace visualization template - inlined from loader_trace.html */ | ||
| export const LOADER_TRACE_TEMPLATE = `<!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <title></title> | ||
| </head> | ||
| <body> | ||
| <div id="mountNode"></div> | ||
| <script src="https://gw.alipayobjects.com/os/antv/assets/g2/3.0.9/g2.min.js"></script> | ||
| <script> | ||
| var data = {{placeholder}}; | ||
|
|
||
| const chart = new G2.Chart({ | ||
| container: 'mountNode', // 指定图表容器 ID | ||
| height: data.length * 22, // 指定图表高度 | ||
| forceFit: true, | ||
| padding: 'auto', | ||
| }); | ||
|
|
||
| // Step 2: 载入数据源 | ||
| chart.source(data, { | ||
| range: { | ||
| type: 'time', | ||
| mask: 'HH:mm:ss', | ||
| nice: true, | ||
| }, | ||
| }); | ||
| chart | ||
| .coord() | ||
| .transpose() | ||
| .scale(1, -1); | ||
|
|
||
| chart.legend({ position: 'top' }); | ||
|
|
||
| chart.tooltip({ | ||
| showTitle: false, | ||
| itemTpl: '<li>{duration}ms: {name} </li>' | ||
| }); | ||
|
|
||
| chart | ||
| .interval() | ||
| .position('title*range') | ||
| .color('type') | ||
| .tooltip('name*duration', (name, duration) => ({ name, duration })) | ||
| .size(5); | ||
|
|
||
| chart.render(); | ||
| </script> | ||
| </body> | ||
| </html> | ||
| `; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scope-check the registered bundle store before returning it.
Line 77 currently short-circuits
load()for any non-empty global store. In multi-app or reused-process scenarios, this can return a manifest from a differentbaseDir/env/scope and bypass the normal validation path.Proposed fix
static load(baseDir: string, serverEnv: string, serverScope: string): ManifestStore | null { - const bundleStore: ManifestStore | undefined = (globalThis as any).__EGG_BUNDLE_STORE__; - if (bundleStore) { + const bundleStore = ManifestStore.getBundleStore(); + if ( + bundleStore && + bundleStore.baseDir === baseDir && + bundleStore.data.invalidation.serverEnv === serverEnv && + bundleStore.data.invalidation.serverScope === serverScope + ) { debug('load: returning registered bundle store'); return bundleStore; } + if (bundleStore) { + debug('load: ignore registered bundle store due context mismatch'); + }🤖 Prompt for AI Agents