Skip to content

Commit

Permalink
feat(@angular-devkit/build-angular): add JSON build logs when using t…
Browse files Browse the repository at this point in the history
…he 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": [
    "/"
  ]
}
```
  • Loading branch information
alan-agius4 committed Jan 31, 2024
1 parent 476a68d commit f4f5356
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 81 deletions.
Expand Up @@ -13,12 +13,7 @@ import path from 'node:path';
import { BuildOutputFile } from '../../tools/esbuild/bundler-context';
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language';
import {
logMessages,
withNoProgress,
withSpinner,
writeResultFiles,
} from '../../tools/esbuild/utils';
import { withNoProgress, withSpinner, writeResultFiles } from '../../tools/esbuild/utils';
import { deleteOutputDir } from '../../utils/delete-output-dir';
import { shouldWatchRoot } from '../../utils/environment-options';
import { NormalizedCachedOptions } from '../../utils/normalize-cache';
Expand Down Expand Up @@ -73,9 +68,6 @@ export async function* runEsBuildBuildAction(
try {
// Perform the build action
result = await withProgress('Building...', () => action());

// Log all diagnostic (error/warning) messages from the build
await logMessages(logger, result);
} finally {
// Ensure Sass workers are shutdown if not watching
if (!watch) {
Expand Down Expand Up @@ -180,9 +172,6 @@ export async function* runEsBuildBuildAction(
action(result.createRebuildState(changes)),
);

// Log all diagnostic (error/warning) messages from the rebuild
await logMessages(logger, result);

// Update watched locations provided by the new build result.
// Keep watching all previous files if there are any errors; otherwise consider all
// files stale until confirmed present in the new result's watch files.
Expand Down
Expand Up @@ -15,7 +15,6 @@ import { checkCommonJSModules } from '../../tools/esbuild/commonjs-checker';
import { extractLicenses } from '../../tools/esbuild/license-extractor';
import { calculateEstimatedTransferSizes, logBuildStats } from '../../tools/esbuild/utils';
import { BudgetCalculatorResult, checkBudgets } from '../../utils/bundle-calculator';
import { colors } from '../../utils/color';
import { copyAssets } from '../../utils/copy-assets';
import { getSupportedBrowsers } from '../../utils/supported-browsers';
import { executePostBundleSteps } from './execute-post-bundle';
Expand All @@ -38,6 +37,8 @@ export async function executeBuild(
prerenderOptions,
ssrOptions,
verbose,
colors,
jsonLogs,
} = options;

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

// Perform i18n translation inlining if enabled
let prerenderedRoutes: string[];
if (i18nOptions.shouldInline) {
const result = await inlineI18n(options, executionResult, initialFiles);
executionResult.addErrors(result.errors);
executionResult.addWarnings(result.warnings);
prerenderedRoutes = result.prerenderedRoutes;
executionResult.addPrerenderedRoutes(result.prerenderedRoutes);
} else {
const result = await executePostBundleSteps(
options,
Expand All @@ -161,39 +161,20 @@ export async function executeBuild(

executionResult.addErrors(result.errors);
executionResult.addWarnings(result.warnings);
prerenderedRoutes = result.prerenderedRoutes;
executionResult.addPrerenderedRoutes(result.prerenderedRoutes);
executionResult.outputFiles.push(...result.additionalOutputFiles);
executionResult.assetFiles.push(...result.additionalAssets);
}

if (prerenderOptions) {
const prerenderedRoutes = executionResult.prerenderedRoutes;
executionResult.addOutputFile(
'prerendered-routes.json',
JSON.stringify({ routes: prerenderedRoutes.sort((a, b) => a.localeCompare(b)) }, null, 2),
JSON.stringify({ routes: prerenderedRoutes }, null, 2),
BuildOutputFileType.Root,
);

let prerenderMsg = `Prerendered ${prerenderedRoutes.length} static route`;
if (prerenderedRoutes.length > 1) {
prerenderMsg += 's.';
} else {
prerenderMsg += '.';
}

context.logger.info(colors.magenta(prerenderMsg) + '\n');
}

logBuildStats(
context.logger,
metafile,
initialFiles,
budgetFailures,
changedFiles,
estimatedTransferSizes,
!!ssrOptions,
verbose,
);

// Write metafile if stats option is enabled
if (options.stats) {
executionResult.addOutputFile(
Expand All @@ -203,5 +184,20 @@ export async function executeBuild(
);
}

if (!jsonLogs) {
context.logger.info(
logBuildStats(
metafile,
initialFiles,
budgetFailures,
colors,
changedFiles,
estimatedTransferSizes,
!!ssrOptions,
verbose,
),
);
}

return executionResult;
}
Expand Up @@ -9,6 +9,8 @@
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import type { Plugin } from 'esbuild';
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
import { logMessages } from '../../tools/esbuild/utils';
import { colors as ansiColors } from '../../utils/color';
import { purgeStaleBuildCache } from '../../utils/purge-cache';
import { assertCompatibleAngularVersion } from '../../utils/version';
import { runEsBuildBuildAction } from './build-action';
Expand Down Expand Up @@ -83,19 +85,33 @@ export async function* buildApplicationInternal(

yield* runEsBuildBuildAction(
async (rebuildState) => {
const { prerenderOptions, outputOptions, jsonLogs } = normalizedOptions;

const startTime = process.hrtime.bigint();
const result = await executeBuild(normalizedOptions, context, rebuildState);

const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
const hasError = result.errors.length > 0;
if (!jsonLogs) {
if (prerenderOptions) {
const prerenderedRoutesLength = result.prerenderedRoutes.length;
let prerenderMsg = `Prerendered ${prerenderedRoutesLength} static route`;
prerenderMsg += prerenderedRoutesLength !== 1 ? 's.' : '.';

logger.info(ansiColors.magenta(prerenderMsg));
}

const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
const hasError = result.errors.length > 0;
if (writeToFileSystem && !hasError) {
logger.info(`Output location: ${outputOptions.base}\n`);
}

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

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

return result;
},
Expand Down
Expand Up @@ -17,6 +17,8 @@ import {
normalizeGlobalStyles,
} from '../../tools/webpack/utils/helpers';
import { normalizeAssetPatterns, normalizeOptimization, normalizeSourceMaps } from '../../utils';
import { colors } from '../../utils/color';
import { useJSONBuildLogs } from '../../utils/environment-options';
import { I18nOptions, createI18nOptions } from '../../utils/i18n-options';
import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator';
import { normalizeCacheOptions } from '../../utils/normalize-cache';
Expand Down Expand Up @@ -344,6 +346,8 @@ export async function normalizeOptions(
publicPath: deployUrl ? deployUrl : undefined,
plugins: extensions?.codePlugins?.length ? extensions?.codePlugins : undefined,
loaderExtensions,
jsonLogs: useJSONBuildLogs,
colors: colors.enabled,
};
}

Expand Down
Expand Up @@ -329,6 +329,7 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu
preserveSymlinks,
jit,
loaderExtensions,
jsonLogs,
} = options;

// Ensure unique hashes for i18n translation changes when using post-process inlining.
Expand All @@ -355,7 +356,7 @@ function getEsBuildCommonOptions(options: NormalizedApplicationBuildOptions): Bu
resolveExtensions: ['.ts', '.tsx', '.mjs', '.js'],
metafile: true,
legalComments: options.extractLicenses ? 'none' : 'eof',
logLevel: options.verbose ? 'debug' : 'silent',
logLevel: options.verbose && !jsonLogs ? 'debug' : 'silent',
minifyIdentifiers: optimizationOptions.scripts && allowMangle,
minifySyntax: optimizationOptions.scripts,
minifyWhitespace: optimizationOptions.scripts,
Expand Down
Expand Up @@ -38,6 +38,7 @@ export class ExecutionResult {
outputFiles: BuildOutputFile[] = [];
assetFiles: BuildOutputAsset[] = [];
errors: (Message | PartialMessage)[] = [];
prerenderedRoutes: string[] = [];
warnings: (Message | PartialMessage)[] = [];
externalMetadata?: ExternalResultMetadata;

Expand Down Expand Up @@ -68,6 +69,12 @@ export class ExecutionResult {
}
}

addPrerenderedRoutes(routes: string[]): void {
this.prerenderedRoutes.push(...routes);
// Sort the prerendered routes.
this.prerenderedRoutes.sort((a, b) => a.localeCompare(b));
}

addWarning(error: PartialMessage | string): void {
if (typeof error === 'string') {
this.warnings.push({ text: error, location: null });
Expand Down
Expand Up @@ -34,6 +34,7 @@ export function createGlobalScriptsBundleOptions(
outputNames,
preserveSymlinks,
sourcemapOptions,
jsonLogs,
workspaceRoot,
} = options;

Expand Down Expand Up @@ -63,7 +64,7 @@ export function createGlobalScriptsBundleOptions(
mainFields: ['script', 'browser', 'main'],
conditions: ['script'],
resolveExtensions: ['.mjs', '.js'],
logLevel: options.verbose ? 'debug' : 'silent',
logLevel: options.verbose && !jsonLogs ? 'debug' : 'silent',
metafile: true,
minify: optimizationOptions.scripts,
outdir: workspaceRoot,
Expand All @@ -81,8 +82,9 @@ export function createGlobalScriptsBundleOptions(
transformPath: (path) => path.slice(namespace.length + 1) + '.js',
loadContent: (args, build) =>
createCachedLoad(loadCache, async (args) => {
const files = globalScripts.find(({ name }) => name === args.path.slice(0, -3))
?.files;
const files = globalScripts.find(
({ name }) => name === args.path.slice(0, -3),
)?.files;
assert(files, `Invalid operation: global scripts name not found [${args.path}]`);

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

0 comments on commit f4f5356

Please sign in to comment.