Skip to content

Commit f4f5356

Browse files
committed
feat(@angular-devkit/build-angular): add JSON build logs when using the application builder
This change implements the capability to display JSON build logs in the terminal instead of a format readable by humans. This is particularly useful for hosting providers, as it allows them to effortlessly access the necessary information without having to parse the JSON configuration. To enable this output, set the `NG_BUILD_LOGS_JSON=1` environment variable. Additionally, warnings, errors, and logs are automatically colorized when the standard output is a WritableStream. You can disable the colors by using the `FORCE_COLOR=0` environment variable. ``` FORCE_COLOR=0 NG_BUILD_LOGS_JSON=1 ng b { "errors": [], "warnings": [], "outputPaths": { "root": "file:///usr/local/test/home//test-project/dist/test-project", "browser": "file:///usr/local/test/home//test-project/dist/test-project/browser", "server": "file:///usr/local/test/home//test-project/dist/test-project/server" }, "prerenderedRoutes": [ "/" ] } ``` ``` NG_BUILD_LOGS_JSON=1 ng b { "errors": [], "warnings": [], "outputPaths": { "root": "file:///usr/local/test/home//test-project/dist/test-project", "browser": "file:///usr/local/test/home//test-project/dist/test-project/browser", "server": "file:///usr/local/test/home//test-project/dist/test-project/server" }, "prerenderedRoutes": [ "/" ] } ```
1 parent 476a68d commit f4f5356

File tree

9 files changed

+145
-81
lines changed

9 files changed

+145
-81
lines changed

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

+1-12
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,7 @@ import path from 'node:path';
1313
import { BuildOutputFile } from '../../tools/esbuild/bundler-context';
1414
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
1515
import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language';
16-
import {
17-
logMessages,
18-
withNoProgress,
19-
withSpinner,
20-
writeResultFiles,
21-
} from '../../tools/esbuild/utils';
16+
import { withNoProgress, withSpinner, writeResultFiles } from '../../tools/esbuild/utils';
2217
import { deleteOutputDir } from '../../utils/delete-output-dir';
2318
import { shouldWatchRoot } from '../../utils/environment-options';
2419
import { NormalizedCachedOptions } from '../../utils/normalize-cache';
@@ -73,9 +68,6 @@ export async function* runEsBuildBuildAction(
7368
try {
7469
// Perform the build action
7570
result = await withProgress('Building...', () => action());
76-
77-
// Log all diagnostic (error/warning) messages from the build
78-
await logMessages(logger, result);
7971
} finally {
8072
// Ensure Sass workers are shutdown if not watching
8173
if (!watch) {
@@ -180,9 +172,6 @@ export async function* runEsBuildBuildAction(
180172
action(result.createRebuildState(changes)),
181173
);
182174

183-
// Log all diagnostic (error/warning) messages from the rebuild
184-
await logMessages(logger, result);
185-
186175
// Update watched locations provided by the new build result.
187176
// Keep watching all previous files if there are any errors; otherwise consider all
188177
// files stale until confirmed present in the new result's watch files.

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

+21-25
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker';
1515
import { extractLicenses } from '../../tools/esbuild/license-extractor';
1616
import { calculateEstimatedTransferSizes, logBuildStats } from '../../tools/esbuild/utils';
1717
import { BudgetCalculatorResult, checkBudgets } from '../../utils/bundle-calculator';
18-
import { colors } from '../../utils/color';
1918
import { copyAssets } from '../../utils/copy-assets';
2019
import { getSupportedBrowsers } from '../../utils/supported-browsers';
2120
import { executePostBundleSteps } from './execute-post-bundle';
@@ -38,6 +37,8 @@ export async function executeBuild(
3837
prerenderOptions,
3938
ssrOptions,
4039
verbose,
40+
colors,
41+
jsonLogs,
4142
} = options;
4243

4344
// TODO: Consider integrating into watch mode. Would require full rebuild on target changes.
@@ -143,12 +144,11 @@ export async function executeBuild(
143144
}
144145

145146
// Perform i18n translation inlining if enabled
146-
let prerenderedRoutes: string[];
147147
if (i18nOptions.shouldInline) {
148148
const result = await inlineI18n(options, executionResult, initialFiles);
149149
executionResult.addErrors(result.errors);
150150
executionResult.addWarnings(result.warnings);
151-
prerenderedRoutes = result.prerenderedRoutes;
151+
executionResult.addPrerenderedRoutes(result.prerenderedRoutes);
152152
} else {
153153
const result = await executePostBundleSteps(
154154
options,
@@ -161,39 +161,20 @@ export async function executeBuild(
161161

162162
executionResult.addErrors(result.errors);
163163
executionResult.addWarnings(result.warnings);
164-
prerenderedRoutes = result.prerenderedRoutes;
164+
executionResult.addPrerenderedRoutes(result.prerenderedRoutes);
165165
executionResult.outputFiles.push(...result.additionalOutputFiles);
166166
executionResult.assetFiles.push(...result.additionalAssets);
167167
}
168168

169169
if (prerenderOptions) {
170+
const prerenderedRoutes = executionResult.prerenderedRoutes;
170171
executionResult.addOutputFile(
171172
'prerendered-routes.json',
172-
JSON.stringify({ routes: prerenderedRoutes.sort((a, b) => a.localeCompare(b)) }, null, 2),
173+
JSON.stringify({ routes: prerenderedRoutes }, null, 2),
173174
BuildOutputFileType.Root,
174175
);
175-
176-
let prerenderMsg = `Prerendered ${prerenderedRoutes.length} static route`;
177-
if (prerenderedRoutes.length > 1) {
178-
prerenderMsg += 's.';
179-
} else {
180-
prerenderMsg += '.';
181-
}
182-
183-
context.logger.info(colors.magenta(prerenderMsg) + '\n');
184176
}
185177

186-
logBuildStats(
187-
context.logger,
188-
metafile,
189-
initialFiles,
190-
budgetFailures,
191-
changedFiles,
192-
estimatedTransferSizes,
193-
!!ssrOptions,
194-
verbose,
195-
);
196-
197178
// Write metafile if stats option is enabled
198179
if (options.stats) {
199180
executionResult.addOutputFile(
@@ -203,5 +184,20 @@ export async function executeBuild(
203184
);
204185
}
205186

187+
if (!jsonLogs) {
188+
context.logger.info(
189+
logBuildStats(
190+
metafile,
191+
initialFiles,
192+
budgetFailures,
193+
colors,
194+
changedFiles,
195+
estimatedTransferSizes,
196+
!!ssrOptions,
197+
verbose,
198+
),
199+
);
200+
}
201+
206202
return executionResult;
207203
}

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

+23-7
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
1010
import type { Plugin } from 'esbuild';
1111
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
12+
import { logMessages } from '../../tools/esbuild/utils';
13+
import { colors as ansiColors } from '../../utils/color';
1214
import { purgeStaleBuildCache } from '../../utils/purge-cache';
1315
import { assertCompatibleAngularVersion } from '../../utils/version';
1416
import { runEsBuildBuildAction } from './build-action';
@@ -83,19 +85,33 @@ export async function* buildApplicationInternal(
8385

8486
yield* runEsBuildBuildAction(
8587
async (rebuildState) => {
88+
const { prerenderOptions, outputOptions, jsonLogs } = normalizedOptions;
89+
8690
const startTime = process.hrtime.bigint();
8791
const result = await executeBuild(normalizedOptions, context, rebuildState);
8892

89-
const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
90-
const hasError = result.errors.length > 0;
93+
if (!jsonLogs) {
94+
if (prerenderOptions) {
95+
const prerenderedRoutesLength = result.prerenderedRoutes.length;
96+
let prerenderMsg = `Prerendered ${prerenderedRoutesLength} static route`;
97+
prerenderMsg += prerenderedRoutesLength !== 1 ? 's.' : '.';
98+
99+
logger.info(ansiColors.magenta(prerenderMsg));
100+
}
101+
102+
const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
103+
const hasError = result.errors.length > 0;
104+
if (writeToFileSystem && !hasError) {
105+
logger.info(`Output location: ${outputOptions.base}\n`);
106+
}
91107

92-
if (writeToFileSystem && !hasError) {
93-
logger.info(`Output location: ${normalizedOptions.outputOptions.base}\n`);
108+
logger.info(
109+
`Application bundle generation ${hasError ? 'failed' : 'complete'}. [${buildTime.toFixed(3)} seconds]`,
110+
);
94111
}
95112

96-
logger.info(
97-
`Application bundle generation ${hasError ? 'failed' : 'complete'}. [${buildTime.toFixed(3)} seconds]`,
98-
);
113+
// Log all diagnostic (error/warning) messages
114+
await logMessages(logger, result, normalizedOptions);
99115

100116
return result;
101117
},

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

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
normalizeGlobalStyles,
1818
} from '../../tools/webpack/utils/helpers';
1919
import { normalizeAssetPatterns, normalizeOptimization, normalizeSourceMaps } from '../../utils';
20+
import { colors } from '../../utils/color';
21+
import { useJSONBuildLogs } from '../../utils/environment-options';
2022
import { I18nOptions, createI18nOptions } from '../../utils/i18n-options';
2123
import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator';
2224
import { normalizeCacheOptions } from '../../utils/normalize-cache';
@@ -344,6 +346,8 @@ export async function normalizeOptions(
344346
publicPath: deployUrl ? deployUrl : undefined,
345347
plugins: extensions?.codePlugins?.length ? extensions?.codePlugins : undefined,
346348
loaderExtensions,
349+
jsonLogs: useJSONBuildLogs,
350+
colors: colors.enabled,
347351
};
348352
}
349353

packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,7 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu
329329
preserveSymlinks,
330330
jit,
331331
loaderExtensions,
332+
jsonLogs,
332333
} = options;
333334

334335
// Ensure unique hashes for i18n translation changes when using post-process inlining.
@@ -355,7 +356,7 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu
355356
resolveExtensions: ['.ts', '.tsx', '.mjs', '.js'],
356357
metafile: true,
357358
legalComments: options.extractLicenses ? 'none' : 'eof',
358-
logLevel: options.verbose ? 'debug' : 'silent',
359+
logLevel: options.verbose && !jsonLogs ? 'debug' : 'silent',
359360
minifyIdentifiers: optimizationOptions.scripts && allowMangle,
360361
minifySyntax: optimizationOptions.scripts,
361362
minifyWhitespace: optimizationOptions.scripts,

packages/angular_devkit/build_angular/src/tools/esbuild/bundler-execution-result.ts

+7
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class ExecutionResult {
3838
outputFiles: BuildOutputFile[] = [];
3939
assetFiles: BuildOutputAsset[] = [];
4040
errors: (Message | PartialMessage)[] = [];
41+
prerenderedRoutes: string[] = [];
4142
warnings: (Message | PartialMessage)[] = [];
4243
externalMetadata?: ExternalResultMetadata;
4344

@@ -68,6 +69,12 @@ export class ExecutionResult {
6869
}
6970
}
7071

72+
addPrerenderedRoutes(routes: string[]): void {
73+
this.prerenderedRoutes.push(...routes);
74+
// Sort the prerendered routes.
75+
this.prerenderedRoutes.sort((a, b) => a.localeCompare(b));
76+
}
77+
7178
addWarning(error: PartialMessage | string): void {
7279
if (typeof error === 'string') {
7380
this.warnings.push({ text: error, location: null });

packages/angular_devkit/build_angular/src/tools/esbuild/global-scripts.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function createGlobalScriptsBundleOptions(
3434
outputNames,
3535
preserveSymlinks,
3636
sourcemapOptions,
37+
jsonLogs,
3738
workspaceRoot,
3839
} = options;
3940

@@ -63,7 +64,7 @@ export function createGlobalScriptsBundleOptions(
6364
mainFields: ['script', 'browser', 'main'],
6465
conditions: ['script'],
6566
resolveExtensions: ['.mjs', '.js'],
66-
logLevel: options.verbose ? 'debug' : 'silent',
67+
logLevel: options.verbose && !jsonLogs ? 'debug' : 'silent',
6768
metafile: true,
6869
minify: optimizationOptions.scripts,
6970
outdir: workspaceRoot,
@@ -81,8 +82,9 @@ export function createGlobalScriptsBundleOptions(
8182
transformPath: (path) => path.slice(namespace.length + 1) + '.js',
8283
loadContent: (args, build) =>
8384
createCachedLoad(loadCache, async (args) => {
84-
const files = globalScripts.find(({ name }) => name === args.path.slice(0, -3))
85-
?.files;
85+
const files = globalScripts.find(
86+
({ name }) => name === args.path.slice(0, -3),
87+
)?.files;
8688
assert(files, `Invalid operation: global scripts name not found [${args.path}]`);
8789

8890
// Global scripts are concatenated using magic-string instead of bundled via esbuild.

0 commit comments

Comments
 (0)