diff --git a/tools/egg-bin/package.json b/tools/egg-bin/package.json index 043002072e..32af4bea14 100644 --- a/tools/egg-bin/package.json +++ b/tools/egg-bin/package.json @@ -28,6 +28,7 @@ "exports": { ".": "./src/index.ts", "./baseCommand": "./src/baseCommand.ts", + "./commands/bundle": "./src/commands/bundle.ts", "./commands/cov": "./src/commands/cov.ts", "./commands/dev": "./src/commands/dev.ts", "./commands/manifest": "./src/commands/manifest.ts", @@ -41,6 +42,7 @@ "exports": { ".": "./dist/index.js", "./baseCommand": "./dist/baseCommand.js", + "./commands/bundle": "./dist/commands/bundle.js", "./commands/cov": "./dist/commands/cov.js", "./commands/dev": "./dist/commands/dev.js", "./commands/manifest": "./dist/commands/manifest.js", @@ -59,6 +61,7 @@ }, "dependencies": { "@eggjs/core": "workspace:*", + "@eggjs/egg-bundler": "workspace:*", "@eggjs/tegg-vitest": "workspace:*", "@eggjs/utils": "workspace:*", "@oclif/core": "catalog:", diff --git a/tools/egg-bin/src/commands/bundle.ts b/tools/egg-bin/src/commands/bundle.ts new file mode 100644 index 0000000000..b9df4c1e61 --- /dev/null +++ b/tools/egg-bin/src/commands/bundle.ts @@ -0,0 +1,100 @@ +import path from 'node:path'; +import { debuglog } from 'node:util'; + +import { getFrameworkPath } from '@eggjs/utils'; +import { Flags } from '@oclif/core'; + +import { BaseCommand } from '../baseCommand.ts'; + +const debug = debuglog('egg/bin/commands/bundle'); +const bundleModes = ['production', 'development'] as const; +type BundleMode = (typeof bundleModes)[number]; + +function getBundleMode(mode: string): BundleMode { + if (mode === 'production' || mode === 'development') { + return mode; + } + throw new Error(`Unsupported bundle mode: ${mode}`); +} + +export default class Bundle extends BaseCommand { + static override description = 'Bundle an egg app into a deployable artifact using @eggjs/egg-bundler'; + + static override examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --output ./dist-bundle', + '<%= config.bin %> <%= command.id %> --mode development', + '<%= config.bin %> <%= command.id %> --framework egg --output ./out', + ]; + + static override flags = { + output: Flags.string({ + char: 'o', + description: 'output directory for the bundled artifact', + default: './dist-bundle', + }), + manifest: Flags.string({ + description: 'path to manifest.json (defaults to /.egg/manifest.json)', + }), + framework: Flags.string({ + char: 'f', + description: 'framework name or absolute path', + }), + mode: Flags.string({ + description: 'build mode', + options: [...bundleModes], + default: 'production', + }), + 'no-tegg': Flags.boolean({ + description: 'disable tegg decoratedFile collection', + default: false, + }), + 'force-external': Flags.string({ + description: 'package name to always mark as external (repeatable)', + multiple: true, + default: [], + }), + 'inline-external': Flags.string({ + description: 'package name to force-inline even if auto-detected as external (repeatable)', + multiple: true, + default: [], + }), + }; + + public async run(): Promise { + const { flags } = this; + const baseDir = flags.base; + const outputDir = path.isAbsolute(flags.output) ? flags.output : path.join(baseDir, flags.output); + const manifestPath = flags.manifest + ? path.isAbsolute(flags.manifest) + ? flags.manifest + : path.join(baseDir, flags.manifest) + : undefined; + + debug( + 'bundle: baseDir=%s, outputDir=%s, framework=%s, mode=%s, tegg=%s', + baseDir, + outputDir, + flags.framework, + flags.mode, + !flags['no-tegg'], + ); + + const { bundle } = await import('@eggjs/egg-bundler'); + const result = await bundle({ + baseDir, + outputDir, + manifestPath, + framework: getFrameworkPath({ framework: flags.framework, baseDir }), + mode: getBundleMode(flags.mode), + tegg: !flags['no-tegg'], + externals: { + force: flags['force-external'], + inline: flags['inline-external'], + }, + }); + + this.log(`bundled to ${result.outputDir} (${result.files.length} files)`); + this.log(`manifest: ${result.manifestPath}`); + } +} diff --git a/tools/egg-bin/src/index.ts b/tools/egg-bin/src/index.ts index c53db0f5b1..7ac2040641 100644 --- a/tools/egg-bin/src/index.ts +++ b/tools/egg-bin/src/index.ts @@ -1,9 +1,10 @@ +import Bundle from './commands/bundle.ts'; import Cov from './commands/cov.ts'; import Dev from './commands/dev.ts'; import Manifest from './commands/manifest.ts'; import Test from './commands/test.ts'; -export { Test, Cov, Dev, Manifest }; +export { Test, Cov, Dev, Manifest, Bundle }; export * from './baseCommand.ts'; export * from './types.ts'; diff --git a/tools/egg-bin/test/commands/bundle.test.ts b/tools/egg-bin/test/commands/bundle.test.ts new file mode 100644 index 0000000000..558ee2f39a --- /dev/null +++ b/tools/egg-bin/test/commands/bundle.test.ts @@ -0,0 +1,78 @@ +import path from 'node:path'; + +import { getFrameworkPath } from '@eggjs/utils'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +import Bundle from '../../src/commands/bundle.ts'; +import { getFixtures } from '../helper.ts'; + +const bundleMock = vi.hoisted(() => vi.fn()); + +vi.mock('@eggjs/egg-bundler', () => ({ + bundle: bundleMock, +})); + +describe('test/commands/bundle.test.ts', () => { + const baseDir = getFixtures('demo-app'); + + beforeEach(() => { + bundleMock.mockReset(); + bundleMock.mockResolvedValue({ + outputDir: path.join(baseDir, 'dist-bundle'), + manifestPath: path.join(baseDir, '.egg/manifest.json'), + files: ['server.js'], + }); + }); + + it('should pass default options to egg-bundler', async () => { + await Bundle.run(['--base', baseDir]); + + 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: [], + }, + }); + }); + + it('should pass resolved flag options to egg-bundler', async () => { + await Bundle.run([ + '--base', + baseDir, + '--output', + 'bundle-output', + '--manifest', + '.egg/custom-manifest.json', + '--mode', + 'development', + '--no-tegg', + '--force-external', + '@scope/foo', + '--force-external', + 'bar', + '--inline-external', + 'baz', + ]); + + expect(bundleMock).toHaveBeenCalledTimes(1); + expect(bundleMock).toHaveBeenCalledWith({ + baseDir, + outputDir: path.join(baseDir, 'bundle-output'), + manifestPath: path.join(baseDir, '.egg/custom-manifest.json'), + framework: getFrameworkPath({ baseDir }), + mode: 'development', + tegg: false, + externals: { + force: ['@scope/foo', 'bar'], + inline: ['baz'], + }, + }); + }); +}); diff --git a/tools/egg-bundler/package.json b/tools/egg-bundler/package.json index 730c9ea357..cce510449e 100644 --- a/tools/egg-bundler/package.json +++ b/tools/egg-bundler/package.json @@ -1,7 +1,6 @@ { "name": "@eggjs/egg-bundler", "version": "0.0.0", - "private": true, "description": "Bundle an Egg.js application into a deployable artifact using @utoo/pack", "homepage": "https://github.com/eggjs/egg/tree/next/tools/egg-bundler", "bugs": { @@ -52,6 +51,7 @@ }, "dependencies": { "@eggjs/core": "workspace:*", + "@eggjs/utils": "workspace:*", "@utoo/pack": "catalog:", "tsx": "catalog:" },