Skip to content

Commit 71e87fc

Browse files
dgp1130angular-robot[bot]
authored andcommitted
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.
1 parent 2d0943d commit 71e87fc

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)