Skip to content

Commit fc1164b

Browse files
committed
refactor(@angular-devkit/build-angular): move more esbuild option setup into normalize option helper
The initial global stylesheet, file replacement, index HTML, and service worker option analysis and cleanup has now been moved into the `normalizeOptions` helper for the esbuild-based browser application builder. This better organizes the option related setup steps as well as reduces the amount of code in the main builder source file.
1 parent 648e6d6 commit fc1164b

File tree

3 files changed

+119
-47
lines changed

3 files changed

+119
-47
lines changed

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

Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,16 @@
99
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
1010
import * as assert from 'assert';
1111
import type { Message, OutputFile } from 'esbuild';
12-
import { promises as fs } from 'fs';
12+
import * as fs from 'fs/promises';
1313
import * as path from 'path';
1414
import { NormalizedOptimizationOptions, deleteOutputDir } from '../../utils';
1515
import { copyAssets } from '../../utils/copy-assets';
1616
import { assertIsError } from '../../utils/error';
1717
import { transformSupportedBrowsersToTargets } from '../../utils/esbuild-targets';
1818
import { FileInfo } from '../../utils/index-file/augment-index-html';
1919
import { IndexHtmlGenerator } from '../../utils/index-file/index-html-generator';
20-
import { generateEntryPoints } from '../../utils/package-chunk-sort';
21-
import { augmentAppWithServiceWorker } from '../../utils/service-worker';
20+
import { augmentAppWithServiceWorkerEsbuild } from '../../utils/service-worker';
2221
import { getSupportedBrowsers } from '../../utils/supported-browsers';
23-
import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config';
24-
import { normalizeGlobalStyles } from '../../webpack/utils/helpers';
2522
import { createCompilerPlugin } from './compiler-plugin';
2623
import { bundle, logMessages } from './esbuild';
2724
import { logExperimentalWarnings } from './experimental-warnings';
@@ -41,13 +38,16 @@ async function execute(
4138
projectRoot,
4239
workspaceRoot,
4340
entryPoints,
44-
entryPointNameLookup,
4541
optimizationOptions,
4642
outputPath,
4743
sourcemapOptions,
4844
tsconfig,
4945
assets,
5046
outputNames,
47+
fileReplacements,
48+
globalStyles,
49+
serviceWorkerOptions,
50+
indexHtmlOptions,
5151
} = normalizedOptions;
5252

5353
const target = transformSupportedBrowsersToTargets(
@@ -64,12 +64,14 @@ async function execute(
6464
optimizationOptions,
6565
sourcemapOptions,
6666
tsconfig,
67+
fileReplacements,
6768
target,
6869
),
6970
// Execute esbuild to bundle the global stylesheets
7071
bundleGlobalStylesheets(
7172
workspaceRoot,
7273
outputNames,
74+
globalStyles,
7375
options,
7476
optimizationOptions,
7577
sourcemapOptions,
@@ -102,7 +104,8 @@ async function execute(
102104
// An entryPoint value indicates an initial file
103105
initialFiles.push({
104106
file: outputFile.path,
105-
name: entryPointNameLookup.get(entryPoint) ?? '',
107+
// The first part of the filename is the name of file (e.g., "polyfills" for "polyfills.7S5G3MDY.js")
108+
name: path.basename(outputFile.path).split('.')[0],
106109
extension: path.extname(outputFile.path),
107110
});
108111
}
@@ -119,16 +122,11 @@ async function execute(
119122
}
120123

121124
// Generate index HTML file
122-
if (options.index) {
123-
const entrypoints = generateEntryPoints({
124-
scripts: options.scripts ?? [],
125-
styles: options.styles ?? [],
126-
});
127-
125+
if (indexHtmlOptions) {
128126
// Create an index HTML generator that reads from the in-memory output files
129127
const indexHtmlGenerator = new IndexHtmlGenerator({
130-
indexPath: path.join(context.workspaceRoot, getIndexInputFile(options.index)),
131-
entrypoints,
128+
indexPath: indexHtmlOptions.input,
129+
entrypoints: indexHtmlOptions.insertionOrder,
132130
sri: options.subresourceIntegrity,
133131
optimization: optimizationOptions,
134132
crossOrigin: options.crossOrigin,
@@ -161,7 +159,7 @@ async function execute(
161159
context.logger.warn(warning);
162160
}
163161

164-
outputFiles.push(createOutputFileFromText(getIndexOutputFile(options.index), content));
162+
outputFiles.push(createOutputFileFromText(indexHtmlOptions.output, content));
165163
}
166164

167165
// Copy assets
@@ -176,14 +174,13 @@ async function execute(
176174

177175
// Augment the application with service worker support
178176
// TODO: This should eventually operate on the in-memory files prior to writing the output files
179-
if (options.serviceWorker) {
177+
if (serviceWorkerOptions) {
180178
try {
181-
await augmentAppWithServiceWorker(
182-
projectRoot,
179+
await augmentAppWithServiceWorkerEsbuild(
183180
workspaceRoot,
181+
serviceWorkerOptions,
184182
outputPath,
185183
options.baseHref || '/',
186-
options.ngswConfigPath,
187184
);
188185
} catch (error) {
189186
context.logger.error(error instanceof Error ? error.message : `${error}`);
@@ -215,19 +212,9 @@ async function bundleCode(
215212
optimizationOptions: NormalizedOptimizationOptions,
216213
sourcemapOptions: SourceMapClass,
217214
tsconfig: string,
215+
fileReplacements: Record<string, string> | undefined,
218216
target: string[],
219217
) {
220-
let fileReplacements: Record<string, string> | undefined;
221-
if (options.fileReplacements) {
222-
for (const replacement of options.fileReplacements) {
223-
fileReplacements ??= {};
224-
fileReplacements[path.join(workspaceRoot, replacement.replace)] = path.join(
225-
workspaceRoot,
226-
replacement.with,
227-
);
228-
}
229-
}
230-
231218
return bundle({
232219
absWorkingDir: workspaceRoot,
233220
bundle: true,
@@ -295,6 +282,7 @@ async function bundleCode(
295282
async function bundleGlobalStylesheets(
296283
workspaceRoot: string,
297284
outputNames: { bundles: string; media: string },
285+
globalStyles: { name: string; files: string[]; initial: boolean }[],
298286
options: BrowserBuilderOptions,
299287
optimizationOptions: NormalizedOptimizationOptions,
300288
sourcemapOptions: SourceMapClass,
@@ -305,12 +293,7 @@ async function bundleGlobalStylesheets(
305293
const errors: Message[] = [];
306294
const warnings: Message[] = [];
307295

308-
// resolveGlobalStyles is temporarily reused from the Webpack builder code
309-
const { entryPoints: stylesheetEntrypoints, noInjectNames } = normalizeGlobalStyles(
310-
options.styles || [],
311-
);
312-
313-
for (const [name, files] of Object.entries(stylesheetEntrypoints)) {
296+
for (const { name, files, initial } of globalStyles) {
314297
const virtualEntryData = files
315298
.map((file) => `@import '${file.replace(/\\/g, '/')}';`)
316299
.join('\n');
@@ -321,7 +304,7 @@ async function bundleGlobalStylesheets(
321304
workspaceRoot,
322305
optimization: !!optimizationOptions.styles.minify,
323306
sourcemap: !!sourcemapOptions.styles && (sourcemapOptions.hidden ? 'external' : true),
324-
outputNames: noInjectNames.includes(name) ? { media: outputNames.media } : outputNames,
307+
outputNames: initial ? outputNames : { media: outputNames.media },
325308
includePaths: options.stylePreprocessorOptions?.includePaths,
326309
preserveSymlinks: options.preserveSymlinks,
327310
externalDependencies: options.externalDependencies,
@@ -356,7 +339,7 @@ async function bundleGlobalStylesheets(
356339
}
357340
outputFiles.push(createOutputFileFromText(sheetPath, sheetContents));
358341

359-
if (!noInjectNames.includes(name)) {
342+
if (initial) {
360343
initialFiles.push({
361344
file: sheetPath,
362345
name,

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

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import { BuilderContext } from '@angular-devkit/architect';
1010
import * as path from 'path';
1111
import { normalizeAssetPatterns, normalizeOptimization, normalizeSourceMaps } from '../../utils';
1212
import { normalizePolyfills } from '../../utils/normalize-polyfills';
13-
import { Schema as BrowserBuilderOptions, OutputHashing } from '../browser/schema';
13+
import { generateEntryPoints } from '../../utils/package-chunk-sort';
14+
import { getIndexInputFile, getIndexOutputFile } from '../../utils/webpack-browser-config';
15+
import { normalizeGlobalStyles } from '../../webpack/utils/helpers';
16+
import { Schema as BrowserBuilderOptions, OutputHashing } from './schema';
1417

1518
/**
1619
* Normalize the user provided options by creating full paths for all path based options
@@ -71,30 +74,70 @@ export async function normalizeOptions(
7174
outputNames.media = path.join(options.resourcesOutputPath, outputNames.media);
7275
}
7376

77+
let fileReplacements: Record<string, string> | undefined;
78+
if (options.fileReplacements) {
79+
for (const replacement of options.fileReplacements) {
80+
fileReplacements ??= {};
81+
fileReplacements[path.join(workspaceRoot, replacement.replace)] = path.join(
82+
workspaceRoot,
83+
replacement.with,
84+
);
85+
}
86+
}
87+
88+
const globalStyles: { name: string; files: string[]; initial: boolean }[] = [];
89+
if (options.styles?.length) {
90+
const { entryPoints: stylesheetEntrypoints, noInjectNames } = normalizeGlobalStyles(
91+
options.styles || [],
92+
);
93+
for (const [name, files] of Object.entries(stylesheetEntrypoints)) {
94+
globalStyles.push({ name, files, initial: !noInjectNames.includes(name) });
95+
}
96+
}
97+
98+
let serviceWorkerOptions;
99+
if (options.serviceWorker) {
100+
// If ngswConfigPath is not specified, the default is 'ngsw-config.json' within the project root
101+
serviceWorkerOptions = options.ngswConfigPath
102+
? path.join(workspaceRoot, options.ngswConfigPath)
103+
: path.join(projectRoot, 'ngsw-config.json');
104+
}
105+
74106
// Setup bundler entry points
75107
const entryPoints: Record<string, string> = {
76108
main: mainEntryPoint,
77109
};
78110
if (polyfillsEntryPoint) {
79111
entryPoints['polyfills'] = polyfillsEntryPoint;
80112
}
81-
// Create reverse lookup used during index HTML generation
82-
const entryPointNameLookup: ReadonlyMap<string, string> = new Map(
83-
Object.entries(entryPoints).map(
84-
([name, filePath]) => [path.relative(workspaceRoot, filePath), name] as const,
85-
),
86-
);
113+
114+
let indexHtmlOptions;
115+
if (options.index) {
116+
indexHtmlOptions = {
117+
input: path.join(workspaceRoot, getIndexInputFile(options.index)),
118+
// The output file will be created within the configured output path
119+
output: getIndexOutputFile(options.index),
120+
// TODO: Use existing information from above to create the insertion order
121+
insertionOrder: generateEntryPoints({
122+
scripts: options.scripts ?? [],
123+
styles: options.styles ?? [],
124+
}),
125+
};
126+
}
87127

88128
return {
89129
workspaceRoot,
90130
entryPoints,
91-
entryPointNameLookup,
92131
optimizationOptions,
93132
outputPath,
94133
sourcemapOptions,
95134
tsconfig,
96135
projectRoot,
97136
assets,
98137
outputNames,
138+
fileReplacements,
139+
globalStyles,
140+
serviceWorkerOptions,
141+
indexHtmlOptions,
99142
};
100143
}

packages/angular_devkit/build_angular/src/utils/service-worker.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,52 @@ export async function augmentAppWithServiceWorker(
9393
}
9494
}
9595

96+
return augmentAppWithServiceWorkerCore(
97+
config,
98+
outputPath,
99+
baseHref,
100+
inputputFileSystem,
101+
outputFileSystem,
102+
);
103+
}
104+
105+
// This is currently used by the esbuild-based builder
106+
export async function augmentAppWithServiceWorkerEsbuild(
107+
workspaceRoot: string,
108+
configPath: string,
109+
outputPath: string,
110+
baseHref: string,
111+
): Promise<void> {
112+
// Read the configuration file
113+
let config: Config | undefined;
114+
try {
115+
const configurationData = await fsPromises.readFile(configPath, 'utf-8');
116+
config = JSON.parse(configurationData) as Config;
117+
} catch (error) {
118+
assertIsError(error);
119+
if (error.code === 'ENOENT') {
120+
// TODO: Generate an error object that can be consumed by the esbuild-based builder
121+
const message = `Service worker configuration file "${path.relative(
122+
workspaceRoot,
123+
configPath,
124+
)}" could not be found.`;
125+
throw new Error(message);
126+
} else {
127+
throw error;
128+
}
129+
}
130+
131+
// TODO: Return the output files and any errors/warnings
132+
return augmentAppWithServiceWorkerCore(config, outputPath, baseHref);
133+
}
134+
135+
export async function augmentAppWithServiceWorkerCore(
136+
config: Config,
137+
outputPath: string,
138+
baseHref: string,
139+
inputputFileSystem = fsPromises,
140+
outputFileSystem = fsPromises,
141+
): Promise<void> {
96142
// Load ESM `@angular/service-worker/config` using the TypeScript dynamic import workaround.
97143
// Once TypeScript provides support for keeping the dynamic import this workaround can be
98144
// changed to a direct dynamic import.

0 commit comments

Comments
 (0)