diff --git a/packages/angular_devkit/build_angular/src/app-shell/index.ts b/packages/angular_devkit/build_angular/src/app-shell/index.ts index 1f8806f36051..10c03cdb97eb 100644 --- a/packages/angular_devkit/build_angular/src/app-shell/index.ts +++ b/packages/angular_devkit/build_angular/src/app-shell/index.ts @@ -75,9 +75,8 @@ async function _renderUniversal( const { AppServerModule, renderModule } = await import(serverBundlePath); - const renderModuleFn: - | ((module: unknown, options: {}) => Promise) - | undefined = renderModule; + const renderModuleFn: ((module: unknown, options: {}) => Promise) | undefined = + renderModule; if (!(renderModuleFn && AppServerModule)) { throw new Error( @@ -172,7 +171,7 @@ async function _appShellBuilder( const browserTargetRun = await context.scheduleTarget(browserTarget, { watch: false, serviceWorker: false, - optimization: (optimization as unknown) as JsonObject, + optimization: optimization as unknown as JsonObject, }); const serverTargetRun = await context.scheduleTarget(serverTarget, { watch: false, @@ -181,9 +180,10 @@ async function _appShellBuilder( let spinner: Spinner | undefined; try { + // Using `.result` instead of `.output` causes Webpack FS cache not to be created. const [browserResult, serverResult] = await Promise.all([ - (browserTargetRun.result as unknown) as BrowserBuilderOutput, - (serverTargetRun.result as unknown) as ServerBuilderOutput, + browserTargetRun.output.toPromise() as Promise, + serverTargetRun.output.toPromise() as Promise, ]); if (browserResult.success === false || browserResult.baseOutputPath === undefined) { @@ -203,8 +203,8 @@ async function _appShellBuilder( return { success: false, error: err.message }; } finally { - // Just be good citizens and stop those jobs. - await Promise.all([browserTargetRun.stop(), serverTargetRun.stop()]); + // workaround for [tsetse] All Promises in async functions must either be awaited or used in an expression. + const _ = Promise.all([browserTargetRun.stop(), serverTargetRun.stop()]); } } diff --git a/packages/angular_devkit/build_angular/src/utils/environment-options.ts b/packages/angular_devkit/build_angular/src/utils/environment-options.ts index 48700186053e..1b019c4cc96d 100644 --- a/packages/angular_devkit/build_angular/src/utils/environment-options.ts +++ b/packages/angular_devkit/build_angular/src/utils/environment-options.ts @@ -80,6 +80,13 @@ export const cachingBasePath = (() => { return cacheVariable; })(); +// Persistent build cache +const persistentBuildCacheVariable = process.env['NG_PERSISTENT_BUILD_CACHE']; +export const persistentBuildCacheEnabled = + !cachingDisabled && + isPresent(persistentBuildCacheVariable) && + isEnabled(persistentBuildCacheVariable); + // Build profiling const profilingVariable = process.env['NG_BUILD_PROFILING']; export const profilingEnabled = isPresent(profilingVariable) && isEnabled(profilingVariable); diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts index 4c9f83fad922..96eb15ad9953 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts @@ -10,7 +10,13 @@ import { BuildOptimizerWebpackPlugin, buildOptimizerLoaderPath, } from '@angular-devkit/build-optimizer'; +import { + GLOBAL_DEFS_FOR_TERSER, + GLOBAL_DEFS_FOR_TERSER_WITH_AOT, + VERSION as NG_VERSION, +} from '@angular/compiler-cli'; import * as CopyWebpackPlugin from 'copy-webpack-plugin'; +import { createHash } from 'crypto'; import { createWriteStream, existsSync, promises as fsPromises } from 'fs'; import * as path from 'path'; import { ScriptTarget } from 'typescript'; @@ -20,6 +26,7 @@ import { ContextReplacementPlugin, ProgressPlugin, RuleSetRule, + WebpackOptionsNormalized, debug, } from 'webpack'; import { AssetPatternClass } from '../../browser/schema'; @@ -31,6 +38,7 @@ import { allowMinify, cachingDisabled, maxWorkers, + persistentBuildCacheEnabled, profilingEnabled, shouldBeautify, } from '../../utils/environment-options'; @@ -310,11 +318,6 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration { if (scriptsOptimization) { const TerserPlugin = require('terser-webpack-plugin'); - const { - GLOBAL_DEFS_FOR_TERSER, - GLOBAL_DEFS_FOR_TERSER_WITH_AOT, - } = require('@angular/compiler-cli'); - const angularGlobalDefinitions = buildOptions.aot ? GLOBAL_DEFS_FOR_TERSER_WITH_AOT : GLOBAL_DEFS_FOR_TERSER; @@ -473,11 +476,7 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration { syncWebAssembly: true, asyncWebAssembly: true, }, - cache: !!buildOptions.watch && - !cachingDisabled && { - type: 'memory', - maxGenerations: 1, - }, + cache: getCacheSettings(wco, buildBrowserFeatures.supportedBrowsers), optimization: { minimizer: extraMinimizers, moduleIds: 'deterministic', @@ -497,3 +496,38 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration { ], }; } + +function getCacheSettings( + wco: WebpackConfigOptions, + supportedBrowsers: string[], +): WebpackOptionsNormalized['cache'] { + if (persistentBuildCacheEnabled) { + const packageVersion = require('../../../package.json').version; + + return { + type: 'filesystem', + cacheDirectory: findCachePath('angular-webpack'), + maxMemoryGenerations: 1, + // We use the versions and build options as the cache name. The Webpack configurations are too + // dynamic and shared among different build types: test, build and serve. + // None of which are "named". + name: createHash('sha1') + .update(NG_VERSION.full) + .update(packageVersion) + .update(wco.projectRoot) + .update(JSON.stringify(wco.tsConfig)) + .update(JSON.stringify(wco.buildOptions)) + .update(supportedBrowsers.join('')) + .digest('base64'), + }; + } + + if (wco.buildOptions.watch && !cachingDisabled) { + return { + type: 'memory', + maxGenerations: 1, + }; + } + + return false; +} diff --git a/packages/angular_devkit/build_angular/src/webpack/utils/stats.ts b/packages/angular_devkit/build_angular/src/webpack/utils/stats.ts index a6f59294e521..87376ae8f7a1 100644 --- a/packages/angular_devkit/build_angular/src/webpack/utils/stats.ts +++ b/packages/angular_devkit/build_angular/src/webpack/utils/stats.ts @@ -160,6 +160,12 @@ function generateBuildStats(hash: string, time: number, colors: boolean): string return `Build at: ${w(new Date().toISOString())} - Hash: ${w(hash)} - Time: ${w('' + time)}ms`; } +// We use this cache because we can have multiple builders running in the same process, +// where each builder has different output path. + +// Ideally, we should create the logging callback as a factory, but that would need a refactoring. +const runsCache = new Set(); + function statsToString( json: StatsCompilation, // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -176,8 +182,12 @@ function statsToString( const changedChunksStats: BundleStats[] = bundleState ?? []; let unchangedChunkNumber = 0; if (!bundleState?.length) { + const isFirstRun = !runsCache.has(json.outputPath || ''); + for (const chunk of json.chunks) { - if (!chunk.rendered) { + // During first build we want to display unchanged chunks + // but unchanged cached chunks are always marked as not rendered. + if (!isFirstRun && !chunk.rendered) { continue; } @@ -188,6 +198,8 @@ function statsToString( changedChunksStats.push(generateBundleStats({ ...chunk, size: summedSize })); } unchangedChunkNumber = json.chunks.length - changedChunksStats.length; + + runsCache.add(json.outputPath || ''); } // Sort chunks by size in descending order