Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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. 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.
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, dot-relative targets resolve from --base',
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 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',
},
},
},
});
});
});
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