Skip to content

Commit 49f07a8

Browse files
alan-agius4clydin
authored andcommitted
feat(@angular-devkit/build-angular): standardize application builder output structure
This commit updates the application builder to output files in a standardized manner. The builder will output a `browser` directory for all the files that can be accessible by the browser, and a `server` directory that contains the SSR application. Both of these directories are created as children in the configured `outputPath`. Stats and license files will be outputted directly in the configured `outputPath`. Example of output: ``` 3rdpartylicenses.txt ├── browser │ ├── chunk-2XJVAMHT.js │ ├── favicon.ico │ ├── index.html │ ├── main-6JLMM7WW.js │ ├── polyfills-4UVFGIFL.js │ └── styles-5INURTSO.css └── server ├── chunk-4ZCEIHD4.mjs ├── chunk-PMR7BAU4.mjs ├── chunk-TSP6W7K5.mjs ├── index.server.html ├── main.server.mjs └── server.mjs ```
1 parent f29b744 commit 49f07a8

File tree

16 files changed

+259
-87
lines changed

16 files changed

+259
-87
lines changed

packages/angular_devkit/build_angular/src/builders/application/build-action.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { BuilderOutput } from '@angular-devkit/architect';
1010
import type { logging } from '@angular-devkit/core';
1111
import fs from 'node:fs/promises';
1212
import path from 'node:path';
13+
import { BuildOutputFile } from '../../tools/esbuild/bundler-context';
1314
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
1415
import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language';
1516
import { withNoProgress, withSpinner, writeResultFiles } from '../../tools/esbuild/utils';
@@ -25,6 +26,7 @@ export async function* runEsBuildBuildAction(
2526
logger: logging.LoggerApi;
2627
cacheOptions: NormalizedCachedOptions;
2728
writeToFileSystem?: boolean;
29+
writeToFileSystemFilter?: (file: BuildOutputFile) => boolean;
2830
watch?: boolean;
2931
verbose?: boolean;
3032
progress?: boolean;
@@ -34,6 +36,7 @@ export async function* runEsBuildBuildAction(
3436
},
3537
): AsyncIterable<(ExecutionResult['outputWithFiles'] | ExecutionResult['output']) & BuilderOutput> {
3638
const {
39+
writeToFileSystemFilter,
3740
writeToFileSystem = true,
3841
watch,
3942
poll,
@@ -177,7 +180,10 @@ export async function* runEsBuildBuildAction(
177180

178181
if (writeToFileSystem) {
179182
// Write output files
180-
await writeResultFiles(result.outputFiles, result.assetFiles, outputPath);
183+
const filesToWrite = writeToFileSystemFilter
184+
? result.outputFiles.filter(writeToFileSystemFilter)
185+
: result.outputFiles;
186+
await writeResultFiles(filesToWrite, result.assetFiles, outputPath);
181187

182188
yield result.output;
183189
} else {

packages/angular_devkit/build_angular/src/builders/application/execute-build.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
createBrowserCodeBundleOptions,
1414
createServerCodeBundleOptions,
1515
} from '../../tools/esbuild/application-code-bundle';
16-
import { BundlerContext } from '../../tools/esbuild/bundler-context';
16+
import { BuildOutputFileType, BundlerContext } from '../../tools/esbuild/bundler-context';
1717
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
1818
import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker';
1919
import { createGlobalScriptsBundleOptions } from '../../tools/esbuild/global-scripts';
@@ -174,10 +174,14 @@ export async function executeBuild(
174174
indexContentOutputNoCssInlining = contentWithoutCriticalCssInlined;
175175
printWarningsAndErrorsToConsole(context, warnings, errors);
176176

177-
executionResult.addOutputFile(indexHtmlOptions.output, content);
177+
executionResult.addOutputFile(indexHtmlOptions.output, content, BuildOutputFileType.Browser);
178178

179179
if (ssrOptions) {
180-
executionResult.addOutputFile('index.server.html', contentWithoutCriticalCssInlined);
180+
executionResult.addOutputFile(
181+
'index.server.html',
182+
contentWithoutCriticalCssInlined,
183+
BuildOutputFileType.Server,
184+
);
181185
}
182186
}
183187

@@ -203,22 +207,23 @@ export async function executeBuild(
203207
printWarningsAndErrorsToConsole(context, warnings, errors);
204208

205209
for (const [path, content] of Object.entries(output)) {
206-
executionResult.addOutputFile(path, content);
210+
executionResult.addOutputFile(path, content, BuildOutputFileType.Browser);
207211
}
208212
}
209213

210214
// Copy assets
211215
if (assets) {
212216
// The webpack copy assets helper is used with no base paths defined. This prevents the helper
213217
// from directly writing to disk. This should eventually be replaced with a more optimized helper.
214-
executionResult.assetFiles.push(...(await copyAssets(assets, [], workspaceRoot)));
218+
executionResult.addAssets(await copyAssets(assets, [], workspaceRoot));
215219
}
216220

217221
// Extract and write licenses for used packages
218222
if (options.extractLicenses) {
219223
executionResult.addOutputFile(
220224
'3rdpartylicenses.txt',
221225
await extractLicenses(metafile, workspaceRoot),
226+
BuildOutputFileType.Root,
222227
);
223228
}
224229

@@ -233,8 +238,12 @@ export async function executeBuild(
233238
executionResult.outputFiles,
234239
executionResult.assetFiles,
235240
);
236-
executionResult.addOutputFile('ngsw.json', serviceWorkerResult.manifest);
237-
executionResult.assetFiles.push(...serviceWorkerResult.assetFiles);
241+
executionResult.addOutputFile(
242+
'ngsw.json',
243+
serviceWorkerResult.manifest,
244+
BuildOutputFileType.Browser,
245+
);
246+
executionResult.addAssets(serviceWorkerResult.assetFiles);
238247
} catch (error) {
239248
context.logger.error(error instanceof Error ? error.message : `${error}`);
240249

@@ -261,7 +270,11 @@ export async function executeBuild(
261270

262271
// Write metafile if stats option is enabled
263272
if (options.stats) {
264-
executionResult.addOutputFile('stats.json', JSON.stringify(metafile, null, 2));
273+
executionResult.addOutputFile(
274+
'stats.json',
275+
JSON.stringify(metafile, null, 2),
276+
BuildOutputFileType.Root,
277+
);
265278
}
266279

267280
return executionResult;

packages/angular_devkit/build_angular/src/builders/application/i18n.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { BuilderContext } from '@angular-devkit/architect';
1010
import { join } from 'node:path';
11-
import { InitialFileRecord } from '../../tools/esbuild/bundler-context';
11+
import { BuildOutputFileType, InitialFileRecord } from '../../tools/esbuild/bundler-context';
1212
import { ExecutionResult } from '../../tools/esbuild/bundler-execution-result';
1313
import { I18nInliner } from '../../tools/esbuild/i18n-inliner';
1414
import { generateIndexHtml } from '../../tools/esbuild/index-html-generator';
@@ -75,7 +75,13 @@ export async function inlineI18n(
7575
locale,
7676
);
7777

78-
localeOutputFiles.push(createOutputFileFromText(options.indexHtmlOptions.output, content));
78+
localeOutputFiles.push(
79+
createOutputFileFromText(
80+
options.indexHtmlOptions.output,
81+
content,
82+
BuildOutputFileType.Browser,
83+
),
84+
);
7985
inlineResult.errors.push(...errors);
8086
inlineResult.warnings.push(...warnings);
8187

@@ -96,7 +102,9 @@ export async function inlineI18n(
96102
inlineResult.warnings.push(...warnings);
97103

98104
for (const [path, content] of Object.entries(output)) {
99-
localeOutputFiles.push(createOutputFileFromText(path, content));
105+
localeOutputFiles.push(
106+
createOutputFileFromText(path, content, BuildOutputFileType.Browser),
107+
);
100108
}
101109
}
102110
}
@@ -111,7 +119,11 @@ export async function inlineI18n(
111119
executionResult.assetFiles,
112120
);
113121
localeOutputFiles.push(
114-
createOutputFileFromText('ngsw.json', serviceWorkerResult.manifest),
122+
createOutputFileFromText(
123+
'ngsw.json',
124+
serviceWorkerResult.manifest,
125+
BuildOutputFileType.Browser,
126+
),
115127
);
116128
executionResult.assetFiles.push(...serviceWorkerResult.assetFiles);
117129
} catch (error) {

packages/angular_devkit/build_angular/src/builders/application/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
10-
import type { OutputFile } from 'esbuild';
10+
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
1111
import { purgeStaleBuildCache } from '../../utils/purge-cache';
1212
import { assertCompatibleAngularVersion } from '../../utils/version';
1313
import { runEsBuildBuildAction } from './build-action';
@@ -24,7 +24,7 @@ export async function* buildApplicationInternal(
2424
},
2525
): AsyncIterable<
2626
BuilderOutput & {
27-
outputFiles?: OutputFile[];
27+
outputFiles?: BuildOutputFile[];
2828
assetFiles?: { source: string; destination: string }[];
2929
}
3030
> {
@@ -65,6 +65,12 @@ export async function* buildApplicationInternal(
6565
workspaceRoot: normalizedOptions.workspaceRoot,
6666
progress: normalizedOptions.progress,
6767
writeToFileSystem: infrastructureSettings?.write,
68+
// For app-shell and SSG server files are not required by users.
69+
// Omit these when SSR is not enabled.
70+
writeToFileSystemFilter:
71+
normalizedOptions.ssrOptions && normalizedOptions.serverEntryPoint
72+
? undefined
73+
: (file) => file.type !== BuildOutputFileType.Server,
6874
logger: context.logger,
6975
signal: context.signal,
7076
},
@@ -76,7 +82,7 @@ export function buildApplication(
7682
context: BuilderContext,
7783
): AsyncIterable<
7884
BuilderOutput & {
79-
outputFiles?: OutputFile[];
85+
outputFiles?: BuildOutputFile[];
8086
assetFiles?: { source: string; destination: string }[];
8187
}
8288
> {

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

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
*/
88

99
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
10-
import type { OutputFile } from 'esbuild';
10+
import { constants as fsConstants } from 'node:fs';
11+
import fs from 'node:fs/promises';
12+
import path from 'node:path';
13+
import { BuildOutputFile } from '../../tools/esbuild/bundler-context';
1114
import { buildApplicationInternal } from '../application';
1215
import { Schema as ApplicationBuilderOptions } from '../application/schema';
1316
import { logBuilderStatusWarnings } from './builder-status-warnings';
@@ -20,24 +23,33 @@ import { Schema as BrowserBuilderOptions } from './schema';
2023
* @param context The Architect builder context object
2124
* @returns An async iterable with the builder result output
2225
*/
23-
export function buildEsbuildBrowser(
26+
export async function* buildEsbuildBrowser(
2427
userOptions: BrowserBuilderOptions,
2528
context: BuilderContext,
2629
infrastructureSettings?: {
2730
write?: boolean;
2831
},
2932
): AsyncIterable<
3033
BuilderOutput & {
31-
outputFiles?: OutputFile[];
34+
outputFiles?: BuildOutputFile[];
3235
assetFiles?: { source: string; destination: string }[];
3336
}
3437
> {
3538
// Inform user of status of builder and options
3639
logBuilderStatusWarnings(userOptions, context);
37-
3840
const normalizedOptions = normalizeOptions(userOptions);
41+
const fullOutputPath = path.join(context.workspaceRoot, normalizedOptions.outputPath);
42+
43+
for await (const result of buildApplicationInternal(normalizedOptions, context, {
44+
write: false,
45+
})) {
46+
if (infrastructureSettings?.write !== false && result.outputFiles) {
47+
// Write output files
48+
await writeResultFiles(result.outputFiles, result.assetFiles, fullOutputPath);
49+
}
3950

40-
return buildApplicationInternal(normalizedOptions, context, infrastructureSettings);
51+
yield result;
52+
}
4153
}
4254

4355
function normalizeOptions(options: BrowserBuilderOptions): ApplicationBuilderOptions {
@@ -51,4 +63,41 @@ function normalizeOptions(options: BrowserBuilderOptions): ApplicationBuilderOpt
5163
};
5264
}
5365

66+
// We write the file directly from this builder to maintain webpack output compatibility
67+
// and not output browser files into '/browser'.
68+
async function writeResultFiles(
69+
outputFiles: BuildOutputFile[],
70+
assetFiles: { source: string; destination: string }[] | undefined,
71+
outputPath: string,
72+
) {
73+
const directoryExists = new Set<string>();
74+
await Promise.all(
75+
outputFiles.map(async (file) => {
76+
// Ensure output subdirectories exist
77+
const basePath = path.dirname(file.path);
78+
if (basePath && !directoryExists.has(basePath)) {
79+
await fs.mkdir(path.join(outputPath, basePath), { recursive: true });
80+
directoryExists.add(basePath);
81+
}
82+
// Write file contents
83+
await fs.writeFile(path.join(outputPath, file.path), file.contents);
84+
}),
85+
);
86+
87+
if (assetFiles?.length) {
88+
await Promise.all(
89+
assetFiles.map(async ({ source, destination }) => {
90+
// Ensure output subdirectories exist
91+
const basePath = path.dirname(destination);
92+
if (basePath && !directoryExists.has(basePath)) {
93+
await fs.mkdir(path.join(outputPath, basePath), { recursive: true });
94+
directoryExists.add(basePath);
95+
}
96+
// Copy file contents
97+
await fs.copyFile(source, path.join(outputPath), fsConstants.COPYFILE_FICLONE);
98+
}),
99+
);
100+
}
101+
}
102+
54103
export default createBuilder(buildEsbuildBrowser);

packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import type { BuilderContext } from '@angular-devkit/architect';
1010
import type { json, logging } from '@angular-devkit/core';
11-
import type { OutputFile } from 'esbuild';
1211
import { lookup as lookupMimeType } from 'mrmime';
1312
import assert from 'node:assert';
1413
import { BinaryLike, createHash } from 'node:crypto';
@@ -17,6 +16,7 @@ import { ServerResponse } from 'node:http';
1716
import type { AddressInfo } from 'node:net';
1817
import path, { posix } from 'node:path';
1918
import type { Connect, InlineConfig, ViteDevServer } from 'vite';
19+
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
2020
import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer';
2121
import { createAngularLocaleDataPlugin } from '../../tools/vite/i18n-locale-plugin';
2222
import { RenderOptions, renderPage } from '../../utils/server-rendering/render-page';
@@ -32,6 +32,7 @@ interface OutputFileRecord {
3232
size: number;
3333
hash?: Buffer;
3434
updated: boolean;
35+
servable: boolean;
3536
}
3637

3738
const SSG_MARKER_REGEXP = /ng-server-context=["']\w*\|?ssg\|?\w*["']/;
@@ -222,7 +223,7 @@ function handleUpdate(
222223
function analyzeResultFiles(
223224
normalizePath: (id: string) => string,
224225
htmlIndexPath: string,
225-
resultFiles: OutputFile[],
226+
resultFiles: BuildOutputFile[],
226227
generatedFiles: Map<string, OutputFileRecord>,
227228
) {
228229
const seen = new Set<string>(['/index.html']);
@@ -241,6 +242,8 @@ function analyzeResultFiles(
241242
if (filePath.endsWith('.map')) {
242243
generatedFiles.set(filePath, {
243244
contents: file.contents,
245+
servable:
246+
file.type === BuildOutputFileType.Browser || file.type === BuildOutputFileType.Media,
244247
size: file.contents.byteLength,
245248
updated: false,
246249
});
@@ -270,6 +273,8 @@ function analyzeResultFiles(
270273
size: file.contents.byteLength,
271274
hash: fileHash,
272275
updated: true,
276+
servable:
277+
file.type === BuildOutputFileType.Browser || file.type === BuildOutputFileType.Media,
273278
});
274279
}
275280

@@ -397,7 +402,7 @@ export async function setupServer(
397402
// dev server sourcemap issues with stylesheets.
398403
if (extension !== '.js' && extension !== '.html') {
399404
const outputFile = outputFiles.get(pathname);
400-
if (outputFile) {
405+
if (outputFile?.servable) {
401406
const mimeType = lookupMimeType(extension);
402407
if (mimeType) {
403408
res.setHeader('Content-Type', mimeType);

packages/angular_devkit/build_angular/src/builders/jest/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export default createBuilder(
8484
'--experimental-vm-modules',
8585
jest,
8686

87-
`--rootDir="${testOut}"`,
87+
`--rootDir="${path.join(testOut, 'browser')}"`,
8888
'--testEnvironment=jsdom',
8989

9090
// TODO(dgp1130): Enable cache once we have a mechanism for properly clearing / disabling it.

0 commit comments

Comments
 (0)