Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions tools/egg-bin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 `<specifier>=<target>` form,
supports multiple. Relative targets are resolved from the application base
directory

See [`@eggjs/egg-bundler`](../egg-bundler/README.md) for the programmatic API
and output structure.
Expand Down
26 changes: 26 additions & 0 deletions tools/egg-bin/src/commands/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,24 @@ function getBundleMode(mode: string): BundleMode {
throw new Error(`Unsupported bundle mode: ${mode}`);
}

function parsePackAliases(values: readonly string[], baseDir: string): Record<string, string> | undefined {
if (values.length === 0) return undefined;

const alias: Record<string, string> = {};
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 <specifier>=<target>.`);
}

const specifier = value.slice(0, separator);
const target = value.slice(separator + 1);
alias[specifier] = target.startsWith('.') ? path.resolve(baseDir, target) : target;
}
Comment thread
killagu marked this conversation as resolved.

return alias;
}

export default class Bundle extends BaseCommand<typeof Bundle> {
static override description = 'Bundle an egg app into a deployable artifact using @eggjs/egg-bundler';

Expand All @@ -25,6 +43,7 @@ export default class Bundle extends BaseCommand<typeof Bundle> {
'<%= 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 = {
Expand Down Expand Up @@ -59,6 +78,11 @@ export default class Bundle extends BaseCommand<typeof Bundle> {
multiple: true,
default: [],
}),
'pack-alias': Flags.string({
description: '@utoo/pack resolve alias in <specifier>=<target> form, supports multiple',
multiple: true,
default: [],
}),
};

public async run(): Promise<void> {
Expand All @@ -81,6 +105,7 @@ export default class Bundle extends BaseCommand<typeof Bundle> {
);

const { bundle } = await import('@eggjs/egg-bundler');
const packAlias = parsePackAliases(flags['pack-alias'], baseDir);
const result = await bundle({
baseDir,
outputDir,
Expand All @@ -92,6 +117,7 @@ export default class Bundle extends BaseCommand<typeof Bundle> {
force: flags['force-external'],
inline: flags['inline-external'],
},
...(packAlias ? { pack: { resolve: { alias: packAlias } } } : {}),
});

this.log(`bundled to ${result.outputDir} (${result.files.length} files)`);
Expand Down
33 changes: 33 additions & 0 deletions tools/egg-bin/test/commands/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,37 @@ describe('test/commands/bundle.test.ts', () => {
},
});
});

it('should pass pack aliases to egg-bundler with 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',
},
},
},
});
});
});
32 changes: 20 additions & 12 deletions tools/egg-bundler/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
},
});
```

Expand All @@ -23,18 +30,19 @@ path is `<baseDir>/.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 `<baseDir>/.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 `<baseDir>/.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

Expand Down
5 changes: 4 additions & 1 deletion tools/egg-bundler/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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 {
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 @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions tools/egg-bundler/src/lib/PackRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export interface PackEntry {

export type BuildFunc = (config: { config: unknown }, projectPath: string, rootPath: string) => Promise<void>;

export interface PackRunnerResolveConfig {
readonly alias?: Readonly<Record<string, string>>;
}

export interface PackRunnerOptions {
readonly entries: readonly PackEntry[];
readonly outputDir: string;
Expand All @@ -17,6 +21,7 @@ export interface PackRunnerOptions {
readonly rootPath?: string;
readonly mode?: 'production' | 'development';
readonly buildFunc?: BuildFunc;
readonly resolve?: PackRunnerResolveConfig;
}

export interface PackRunnerResult {
Expand Down Expand Up @@ -65,6 +70,7 @@ export class PackRunner {
rootPath = projectPath,
mode = 'production',
buildFunc = DEFAULT_BUILD_FUNC,
resolve,
} = this.#options;

await fs.mkdir(outputDir, { recursive: true });
Expand All @@ -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',
Expand All @@ -90,6 +98,7 @@ export class PackRunner {
type: 'standalone',
},
externals: umdExternals,
...(resolveConfig ? { resolve: resolveConfig } : {}),
optimization: {
treeShaking: false,
minify: false,
Expand All @@ -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<readonly string[]> {
const files: string[] = [];
await this.#collectFilesInDir(dir, dir, files);
Expand Down
21 changes: 21 additions & 0 deletions tools/egg-bundler/test/Bundler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
});
});
17 changes: 17 additions & 0 deletions tools/egg-bundler/test/PackRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ describe('PackRunner', () => {
rootPath?: string;
mode?: 'production' | 'development';
buildFunc?: BuildFunc;
resolve?: {
alias?: Record<string, string>;
};
} = {},
): PackRunner {
const outputDir = overrides.outputDir ?? path.join(tmpDir, 'out');
Expand All @@ -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 () => {}),
});
}
Expand Down Expand Up @@ -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<BuildFunc>(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<string, unknown> }).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<BuildFunc>(async () => {});
await makeRunner({ buildFunc }).run();
Expand Down
Loading