Skip to content

Commit 1b2f902

Browse files
committed
refactor: add entryPoints to browser-esbuild as an internal option
This makes the `main` parameter optional and allows multiple entry points instead. `main` is still technically required by the schema, since it should almost always be set when invoked by a user. However, it now supports `null` as a value so it can be explicitly omitted. Longer term, we may choose to remove `main` and fold it into `entryPoints`, but for now we want to keep compatibility with the existing `browser` builder. Since `entryPoints` is an internal-only options (cannot be set in `angular.json` and isn't exposed in the schema), I made a new `buildEsbuildBrowserInternal()` function which adds the extra private option. This way direct invocations of the builder can provide this extra information without compromising the public API surface defined in the schema. (cherry picked from commit 71e87fc)
1 parent d3ffcb9 commit 1b2f902

File tree

5 files changed

+173
-10
lines changed

5 files changed

+173
-10
lines changed

packages/angular_devkit/build_angular/src/builders/browser-esbuild/experimental-warnings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { BuilderContext } from '@angular-devkit/architect';
1010
import { Schema as BrowserBuilderOptions } from '../browser/schema';
11-
import { Schema as BrowserEsbuildOptions } from './schema';
11+
import { BrowserEsbuildOptions } from './options';
1212

1313
const UNSUPPORTED_OPTIONS: Array<keyof BrowserBuilderOptions> = [
1414
'budgets',

packages/angular_devkit/build_angular/src/builders/browser-esbuild/index.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { logExperimentalWarnings } from './experimental-warnings';
2828
import { createGlobalScriptsBundleOptions } from './global-scripts';
2929
import { extractLicenses } from './license-extractor';
3030
import { LoadResultCache } from './load-result-cache';
31-
import { NormalizedBrowserOptions, normalizeOptions } from './options';
31+
import { BrowserEsbuildOptions, NormalizedBrowserOptions, normalizeOptions } from './options';
3232
import { shutdownSassWorkerPool } from './sass-plugin';
3333
import { Schema as BrowserBuilderOptions } from './schema';
3434
import { createStylesheetBundleOptions } from './stylesheets';
@@ -584,7 +584,7 @@ async function withNoProgress<T>(test: string, action: () => T | Promise<T>): Pr
584584
* @param context The Architect builder context object
585585
* @returns An async iterable with the builder result output
586586
*/
587-
export async function* buildEsbuildBrowser(
587+
export function buildEsbuildBrowser(
588588
userOptions: BrowserBuilderOptions,
589589
context: BuilderContext,
590590
infrastructureSettings?: {
@@ -595,6 +595,28 @@ export async function* buildEsbuildBrowser(
595595
outputFiles?: OutputFile[];
596596
assetFiles?: { source: string; destination: string }[];
597597
}
598+
> {
599+
return buildEsbuildBrowserInternal(userOptions, context, infrastructureSettings);
600+
}
601+
602+
/**
603+
* Internal version of the main execution function for the esbuild-based application builder.
604+
* Exposes some additional "private" options in addition to those exposed by the schema.
605+
* @param userOptions The browser-esbuild builder options to use when setting up the application build
606+
* @param context The Architect builder context object
607+
* @returns An async iterable with the builder result output
608+
*/
609+
export async function* buildEsbuildBrowserInternal(
610+
userOptions: BrowserEsbuildOptions,
611+
context: BuilderContext,
612+
infrastructureSettings?: {
613+
write?: boolean;
614+
},
615+
): AsyncIterable<
616+
BuilderOutput & {
617+
outputFiles?: OutputFile[];
618+
assetFiles?: { source: string; destination: string }[];
619+
}
598620
> {
599621
// Inform user of experimental status of builder and options
600622
logExperimentalWarnings(userOptions, context);

packages/angular_devkit/build_angular/src/builders/browser-esbuild/options.ts

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ import { Schema as BrowserBuilderOptions, OutputHashing } from './schema';
2020

2121
export type NormalizedBrowserOptions = Awaited<ReturnType<typeof normalizeOptions>>;
2222

23+
/** Internal options hidden from builder schema but available when invoked programmatically. */
24+
interface InternalOptions {
25+
/**
26+
* Entry points to use for the compilation. Incompatible with `main`, which must not be provided. May be relative or absolute paths.
27+
* If given a relative path, it is resolved relative to the current workspace and will generate an output at the same relative location
28+
* in the output directory. If given an absolute path, the output will be generated in the root of the output directory with the same base
29+
* name.
30+
*/
31+
entryPoints?: Set<string>;
32+
}
33+
34+
/** Full set of options for `browser-esbuild` builder. */
35+
export type BrowserEsbuildOptions = Omit<BrowserBuilderOptions & InternalOptions, 'main'> & {
36+
// `main` can be `undefined` if `entryPoints` is used.
37+
main?: string;
38+
};
39+
2340
/**
2441
* Normalize the user provided options by creating full paths for all path based options
2542
* and converting multi-form options into a single form that can be directly used
@@ -33,7 +50,7 @@ export type NormalizedBrowserOptions = Awaited<ReturnType<typeof normalizeOption
3350
export async function normalizeOptions(
3451
context: BuilderContext,
3552
projectName: string,
36-
options: BrowserBuilderOptions,
53+
options: BrowserEsbuildOptions,
3754
) {
3855
const workspaceRoot = context.workspaceRoot;
3956
const projectMetadata = await context.getProjectMetadata(projectName);
@@ -46,7 +63,7 @@ export async function normalizeOptions(
4663

4764
const cacheOptions = normalizeCacheOptions(projectMetadata, workspaceRoot);
4865

49-
const mainEntryPoint = path.join(workspaceRoot, options.main);
66+
const entryPoints = normalizeEntryPoints(workspaceRoot, options.main, options.entryPoints);
5067
const tsconfig = path.join(workspaceRoot, options.tsConfig);
5168
const outputPath = normalizeDirectoryPath(path.join(workspaceRoot, options.outputPath));
5269
const optimizationOptions = normalizeOptimization(options.optimization);
@@ -125,11 +142,6 @@ export async function normalizeOptions(
125142
: path.join(projectRoot, 'ngsw-config.json');
126143
}
127144

128-
// Setup bundler entry points
129-
const entryPoints: Record<string, string> = {
130-
main: mainEntryPoint,
131-
};
132-
133145
let indexHtmlOptions;
134146
if (options.index) {
135147
indexHtmlOptions = {
@@ -204,6 +216,57 @@ export async function normalizeOptions(
204216
};
205217
}
206218

219+
/**
220+
* Normalize entry point options. To maintain compatibility with the legacy browser builder, we need a single `main` option which defines a
221+
* single entry point. However, we also want to support multiple entry points as an internal option. The two options are mutually exclusive
222+
* and if `main` is provided it will be used as the sole entry point. If `entryPoints` are provided, they will be used as the set of entry
223+
* points.
224+
*
225+
* @param workspaceRoot Path to the root of the Angular workspace.
226+
* @param main The `main` option pointing at the application entry point. While required per the schema file, it may be omitted by
227+
* programmatic usages of `browser-esbuild`.
228+
* @param entryPoints Set of entry points to use if provided.
229+
* @returns An object mapping entry point names to their file paths.
230+
*/
231+
function normalizeEntryPoints(
232+
workspaceRoot: string,
233+
main: string | undefined,
234+
entryPoints: Set<string> = new Set(),
235+
): Record<string, string> {
236+
if (main === '') {
237+
throw new Error('`main` option cannot be an empty string.');
238+
}
239+
240+
// `main` and `entryPoints` are mutually exclusive.
241+
if (main && entryPoints.size > 0) {
242+
throw new Error('Only one of `main` or `entryPoints` may be provided.');
243+
}
244+
if (!main && entryPoints.size === 0) {
245+
// Schema should normally reject this case, but programmatic usages of the builder might make this mistake.
246+
throw new Error('Either `main` or at least one `entryPoints` value must be provided.');
247+
}
248+
249+
// Schema types force `main` to always be provided, but it may be omitted when the builder is invoked programmatically.
250+
if (main) {
251+
// Use `main` alone.
252+
return { 'main': path.join(workspaceRoot, main) };
253+
} else {
254+
// Use `entryPoints` alone.
255+
return Object.fromEntries(
256+
Array.from(entryPoints).map((entryPoint) => {
257+
const parsedEntryPoint = path.parse(entryPoint);
258+
259+
return [
260+
// File path without extension.
261+
path.join(parsedEntryPoint.dir, parsedEntryPoint.name),
262+
// Full file path.
263+
path.join(workspaceRoot, entryPoint),
264+
];
265+
}),
266+
);
267+
}
268+
}
269+
207270
/**
208271
* Normalize a directory path string.
209272
* Currently only removes a trailing slash if present.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import { buildEsbuildBrowserInternal } from '../../index';
10+
import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
11+
12+
describeBuilder(buildEsbuildBrowserInternal, BROWSER_BUILDER_INFO, (harness) => {
13+
describe('Option: "entryPoints"', () => {
14+
it('provides multiple entry points', async () => {
15+
await harness.writeFiles({
16+
'src/entry1.ts': `console.log('entry1');`,
17+
'src/entry2.ts': `console.log('entry2');`,
18+
'tsconfig.app.json': `
19+
{
20+
"extends": "./tsconfig.json",
21+
"files": ["src/entry1.ts", "src/entry2.ts"]
22+
}
23+
`,
24+
});
25+
26+
harness.useTarget('build', {
27+
...BASE_OPTIONS,
28+
main: undefined,
29+
tsConfig: 'tsconfig.app.json',
30+
entryPoints: new Set(['src/entry1.ts', 'src/entry2.ts']),
31+
});
32+
33+
const { result } = await harness.executeOnce();
34+
expect(result?.success).toBeTrue();
35+
36+
harness.expectFile('dist/entry1.js').toExist();
37+
harness.expectFile('dist/entry2.js').toExist();
38+
});
39+
40+
it('throws when `main` is omitted and an empty `entryPoints` Set is provided', async () => {
41+
harness.useTarget('build', {
42+
...BASE_OPTIONS,
43+
main: undefined,
44+
entryPoints: new Set(),
45+
});
46+
47+
const { result, error } = await harness.executeOnce();
48+
expect(result).toBeUndefined();
49+
50+
expect(error?.message).toContain('Either `main` or at least one `entryPoints`');
51+
});
52+
53+
it('throws when provided with a `main` option', async () => {
54+
harness.useTarget('build', {
55+
...BASE_OPTIONS,
56+
main: 'src/main.ts',
57+
entryPoints: new Set(['src/entry.ts']),
58+
});
59+
60+
const { result, error } = await harness.executeOnce();
61+
expect(result).toBeUndefined();
62+
63+
expect(error?.message).toContain('Only one of `main` or `entryPoints` may be provided.');
64+
});
65+
});
66+
});

packages/angular_devkit/build_angular/src/builders/browser-esbuild/tests/options/main_spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,5 +59,17 @@ describeBuilder(buildEsbuildBrowser, BROWSER_BUILDER_INFO, (harness) => {
5959
harness.expectFile('dist/main.js').toNotExist();
6060
harness.expectFile('dist/index.html').toNotExist();
6161
});
62+
63+
it('throws an error when given an empty string', async () => {
64+
harness.useTarget('build', {
65+
...BASE_OPTIONS,
66+
main: '',
67+
});
68+
69+
const { result, error } = await harness.executeOnce();
70+
expect(result).toBeUndefined();
71+
72+
expect(error?.message).toContain('cannot be an empty string');
73+
});
6274
});
6375
});

0 commit comments

Comments
 (0)