diff --git a/tools/egg-bin/README.md b/tools/egg-bin/README.md index 68ef7ea242..5d310ac649 100644 --- a/tools/egg-bin/README.md +++ b/tools/egg-bin/README.md @@ -193,6 +193,7 @@ egg-bin bundle egg-bin bundle --output ./dist-bundle egg-bin bundle --mode development egg-bin bundle --framework egg --output ./out +egg-bin bundle --pack-alias some-package=./node_modules/some-package/index.js ``` The command writes the bundle to `./dist-bundle` by default. The generated @@ -213,6 +214,9 @@ node worker.js implementation yet - `--force-external` package name to always keep external, supports multiple - `--inline-external` package name to force inline, supports multiple +- `--pack-alias` `@utoo/pack` resolve alias in `=` form, + supports multiple. Dot-relative targets such as `./target.js` and + `../target.js` are resolved from the application base directory See [`@eggjs/egg-bundler`](../egg-bundler/README.md) for the programmatic API and output structure. diff --git a/tools/egg-bin/src/commands/bundle.ts b/tools/egg-bin/src/commands/bundle.ts index b9df4c1e61..873e899876 100644 --- a/tools/egg-bin/src/commands/bundle.ts +++ b/tools/egg-bin/src/commands/bundle.ts @@ -17,6 +17,24 @@ function getBundleMode(mode: string): BundleMode { throw new Error(`Unsupported bundle mode: ${mode}`); } +function parsePackAliases(values: readonly string[], baseDir: string): Record | undefined { + if (values.length === 0) return undefined; + + const alias: Record = {}; + for (const value of values) { + const separator = value.indexOf('='); + if (separator <= 0 || separator === value.length - 1) { + throw new Error(`Invalid --pack-alias value: ${value}. Expected =.`); + } + + const specifier = value.slice(0, separator); + const target = value.slice(separator + 1); + alias[specifier] = target.startsWith('.') ? path.resolve(baseDir, target) : target; + } + + return alias; +} + export default class Bundle extends BaseCommand { static override description = 'Bundle an egg app into a deployable artifact using @eggjs/egg-bundler'; @@ -25,6 +43,7 @@ export default class Bundle extends BaseCommand { '<%= config.bin %> <%= command.id %> --output ./dist-bundle', '<%= config.bin %> <%= command.id %> --mode development', '<%= config.bin %> <%= command.id %> --framework egg --output ./out', + '<%= config.bin %> <%= command.id %> --pack-alias some-package=./node_modules/some-package/index.js', ]; static override flags = { @@ -59,6 +78,11 @@ export default class Bundle extends BaseCommand { multiple: true, default: [], }), + 'pack-alias': Flags.string({ + description: '@utoo/pack resolve alias in = form, dot-relative targets resolve from --base', + multiple: true, + default: [], + }), }; public async run(): Promise { @@ -81,6 +105,7 @@ export default class Bundle extends BaseCommand { ); const { bundle } = await import('@eggjs/egg-bundler'); + const packAlias = parsePackAliases(flags['pack-alias'], baseDir); const result = await bundle({ baseDir, outputDir, @@ -92,6 +117,7 @@ export default class Bundle extends BaseCommand { force: flags['force-external'], inline: flags['inline-external'], }, + ...(packAlias ? { pack: { resolve: { alias: packAlias } } } : {}), }); this.log(`bundled to ${result.outputDir} (${result.files.length} files)`); diff --git a/tools/egg-bin/test/commands/bundle.test.ts b/tools/egg-bin/test/commands/bundle.test.ts index 558ee2f39a..5b6f79a1ae 100644 --- a/tools/egg-bin/test/commands/bundle.test.ts +++ b/tools/egg-bin/test/commands/bundle.test.ts @@ -75,4 +75,37 @@ describe('test/commands/bundle.test.ts', () => { }, }); }); + + it('should pass pack aliases to egg-bundler with dot-relative targets resolved from baseDir', async () => { + await Bundle.run([ + '--base', + baseDir, + '--pack-alias', + 'some-package=./node_modules/some-package/index.js', + '--pack-alias', + 'virtual-module=/abs/virtual-module.js', + ]); + + expect(bundleMock).toHaveBeenCalledTimes(1); + expect(bundleMock).toHaveBeenCalledWith({ + baseDir, + outputDir: path.join(baseDir, 'dist-bundle'), + manifestPath: undefined, + framework: getFrameworkPath({ baseDir }), + mode: 'production', + tegg: true, + externals: { + force: [], + inline: [], + }, + pack: { + resolve: { + alias: { + 'some-package': path.join(baseDir, 'node_modules/some-package/index.js'), + 'virtual-module': '/abs/virtual-module.js', + }, + }, + }, + }); + }); }); diff --git a/tools/egg-bundler/README.md b/tools/egg-bundler/README.md index 4d1710aa1a..f9f714af8a 100644 --- a/tools/egg-bundler/README.md +++ b/tools/egg-bundler/README.md @@ -15,6 +15,13 @@ await bundle({ outputDir: './dist-bundle', framework: 'egg', mode: 'production', + pack: { + resolve: { + alias: { + 'some-package': '/path/to/app/node_modules/some-package/index.js', + }, + }, + }, }); ``` @@ -23,18 +30,19 @@ path is `/.egg/manifest.json`. ## Options -| Option | Description | -| ------------------ | ------------------------------------------------------------------------------- | -| `baseDir` | Application root directory. Required. | -| `outputDir` | Output directory for the bundled artifact. Required. | -| `manifestPath` | Path to `manifest.json`. Defaults to `/.egg/manifest.json`. | -| `framework` | Framework name or absolute path. Defaults to `egg`. | -| `mode` | Build mode, `production` or `development`. Defaults to `production`. | -| `tegg` | Accepted by `BundlerConfig`, but not applied by the current implementation yet. | -| `externals.force` | Package names to always keep external. | -| `externals.inline` | Package names to force inline even if auto-detected as external. | -| `pack.buildFunc` | Test hook for replacing the real `@utoo/pack` build entry. | -| `pack.rootPath` | Override the monorepo workspace root used by `@utoo/pack`. | +| Option | Description | +| -------------------- | ------------------------------------------------------------------------------- | +| `baseDir` | Application root directory. Required. | +| `outputDir` | Output directory for the bundled artifact. Required. | +| `manifestPath` | Path to `manifest.json`. Defaults to `/.egg/manifest.json`. | +| `framework` | Framework name or absolute path. Defaults to `egg`. | +| `mode` | Build mode, `production` or `development`. Defaults to `production`. | +| `tegg` | Accepted by `BundlerConfig`, but not applied by the current implementation yet. | +| `externals.force` | Package names to always keep external. | +| `externals.inline` | Package names to force inline even if auto-detected as external. | +| `pack.buildFunc` | Test hook for replacing the real `@utoo/pack` build entry. | +| `pack.rootPath` | Override the monorepo workspace root used by `@utoo/pack`. | +| `pack.resolve.alias` | Application-supplied `@utoo/pack` resolve aliases. | ## Result diff --git a/tools/egg-bundler/src/index.ts b/tools/egg-bundler/src/index.ts index a3cd43ab2a..4155a92bca 100644 --- a/tools/egg-bundler/src/index.ts +++ b/tools/egg-bundler/src/index.ts @@ -8,10 +8,11 @@ export { type PackEntry, type PackRunnerOptions, type PackRunnerResult, + type PackRunnerResolveConfig, } from './lib/PackRunner.ts'; import { Bundler } from './lib/Bundler.ts'; -import type { BuildFunc } from './lib/PackRunner.ts'; +import type { BuildFunc, PackRunnerResolveConfig } from './lib/PackRunner.ts'; export interface BundlerExternalsConfig { /** Package names to always mark as external, in addition to auto-detected ones. */ @@ -25,6 +26,8 @@ export interface BundlerPackConfig { readonly buildFunc?: BuildFunc; /** Override for the monorepo workspace root. Defaults to auto-detection. */ readonly rootPath?: string; + /** @utoo/pack resolve tuning supplied by the application. */ + readonly resolve?: PackRunnerResolveConfig; } export interface BundlerConfig { diff --git a/tools/egg-bundler/src/lib/Bundler.ts b/tools/egg-bundler/src/lib/Bundler.ts index a9bef4ae9f..28a13477a3 100644 --- a/tools/egg-bundler/src/lib/Bundler.ts +++ b/tools/egg-bundler/src/lib/Bundler.ts @@ -126,6 +126,7 @@ export class Bundler { rootPath: pack?.rootPath, mode, buildFunc: pack?.buildFunc, + resolve: pack?.resolve, }); const packResult = await wrapStep('pack build', () => packRunner.run()); debug('pack produced %d files', packResult.files.length); diff --git a/tools/egg-bundler/src/lib/PackRunner.ts b/tools/egg-bundler/src/lib/PackRunner.ts index bdc1abf31a..ffeb848147 100644 --- a/tools/egg-bundler/src/lib/PackRunner.ts +++ b/tools/egg-bundler/src/lib/PackRunner.ts @@ -9,6 +9,10 @@ export interface PackEntry { export type BuildFunc = (config: { config: unknown }, projectPath: string, rootPath: string) => Promise; +export interface PackRunnerResolveConfig { + readonly alias?: Readonly>; +} + export interface PackRunnerOptions { readonly entries: readonly PackEntry[]; readonly outputDir: string; @@ -17,6 +21,7 @@ export interface PackRunnerOptions { readonly rootPath?: string; readonly mode?: 'production' | 'development'; readonly buildFunc?: BuildFunc; + readonly resolve?: PackRunnerResolveConfig; } export interface PackRunnerResult { @@ -65,6 +70,7 @@ export class PackRunner { rootPath = projectPath, mode = 'production', buildFunc = DEFAULT_BUILD_FUNC, + resolve, } = this.#options; await fs.mkdir(outputDir, { recursive: true }); @@ -80,6 +86,8 @@ export class PackRunner { umdExternals[k] = { commonjs: v, root: v }; } + const resolveConfig = this.#buildResolveConfig(resolve); + const config = { entry: entries.map((e) => ({ name: e.name, import: e.filepath })), target: 'node 22', @@ -90,6 +98,7 @@ export class PackRunner { type: 'standalone', }, externals: umdExternals, + ...(resolveConfig ? { resolve: resolveConfig } : {}), optimization: { treeShaking: false, minify: false, @@ -108,6 +117,11 @@ export class PackRunner { return { outputDir, files }; } + #buildResolveConfig(resolve: PackRunnerResolveConfig | undefined): PackRunnerResolveConfig | undefined { + if (!resolve?.alias || Object.keys(resolve.alias).length === 0) return undefined; + return { alias: { ...resolve.alias } }; + } + async #collectFiles(dir: string): Promise { const files: string[] = []; await this.#collectFilesInDir(dir, dir, files); diff --git a/tools/egg-bundler/test/Bundler.test.ts b/tools/egg-bundler/test/Bundler.test.ts index 8869720565..40b82844ad 100644 --- a/tools/egg-bundler/test/Bundler.test.ts +++ b/tools/egg-bundler/test/Bundler.test.ts @@ -97,4 +97,25 @@ describe('Bundler', () => { autoGenerate: true, }); }); + + it('passes application supplied pack resolve aliases into the pack build config', async () => { + let packResolve: unknown; + const alias = { + 'some-package': path.join(tmpApp, 'node_modules/some-package/index.js'), + }; + + await bundle({ + baseDir: tmpApp, + outputDir: tmpOutput, + pack: { + resolve: { alias }, + buildFunc: async (wrapped) => { + packResolve = (wrapped.config as { resolve?: unknown }).resolve; + await fs.writeFile(path.join(tmpOutput, 'worker.js'), '// worker\n'); + }, + }, + }); + + expect(packResolve).toEqual({ alias }); + }); }); diff --git a/tools/egg-bundler/test/PackRunner.test.ts b/tools/egg-bundler/test/PackRunner.test.ts index 036eced18c..0e1944bda2 100644 --- a/tools/egg-bundler/test/PackRunner.test.ts +++ b/tools/egg-bundler/test/PackRunner.test.ts @@ -31,6 +31,9 @@ describe('PackRunner', () => { rootPath?: string; mode?: 'production' | 'development'; buildFunc?: BuildFunc; + resolve?: { + alias?: Record; + }; } = {}, ): PackRunner { const outputDir = overrides.outputDir ?? path.join(tmpDir, 'out'); @@ -42,6 +45,7 @@ describe('PackRunner', () => { projectPath, ...(overrides.rootPath !== undefined ? { rootPath: overrides.rootPath } : {}), ...(overrides.mode !== undefined ? { mode: overrides.mode } : {}), + ...(overrides.resolve !== undefined ? { resolve: overrides.resolve } : {}), buildFunc: overrides.buildFunc ?? (async () => {}), }); } @@ -104,10 +108,23 @@ describe('PackRunner', () => { expect(config.externals).toEqual({ '@eggjs/core': { commonjs: '@eggjs/core', root: '@eggjs/core' }, }); + expect(config.resolve).toBeUndefined(); expect(projectPath).toBe(tmpDir); expect(rootPath).toBe(tmpDir); }); + it('passes application supplied resolve aliases through to the pack config', async () => { + const buildFunc = vi.fn(async () => {}); + const alias = { + 'some-package': path.join(tmpDir, 'node_modules', 'some-package', 'index.js'), + }; + + await makeRunner({ buildFunc, resolve: { alias } }).run(); + + const config = (buildFunc.mock.calls[0]![0] as { config: Record }).config; + expect(config.resolve).toEqual({ alias }); + }); + it('disables treeShaking and minify in the pack config (tegg runtime requires the full graph)', async () => { const buildFunc = vi.fn(async () => {}); await makeRunner({ buildFunc }).run();