@@ -24,7 +24,10 @@ import {
2424 virtualFs ,
2525} from '@angular-devkit/core' ;
2626import { NodeJsSyncHost } from '@angular-devkit/core/node' ;
27+ import { createHash } from 'crypto' ;
28+ import * as findCacheDirectory from 'find-cache-dir' ;
2729import * as fs from 'fs' ;
30+ import * as os from 'os' ;
2831import * as path from 'path' ;
2932import { from , of } from 'rxjs' ;
3033import { bufferCount , catchError , concatMap , map , mergeScan , switchMap } from 'rxjs/operators' ;
@@ -62,6 +65,7 @@ import {
6265 normalizeOptimization ,
6366 normalizeSourceMaps ,
6467} from '../utils' ;
68+ import { CacheKey , ProcessBundleOptions } from '../utils/process-bundle' ;
6569import { assertCompatibleAngularVersion } from '../utils/version' ;
6670import {
6771 generateBrowserWebpackConfigFromContext ,
@@ -70,6 +74,10 @@ import {
7074} from '../utils/webpack-browser-config' ;
7175import { Schema as BrowserBuilderSchema } from './schema' ;
7276
77+ const cacache = require ( 'cacache' ) ;
78+ const cacheDownlevelPath = findCacheDirectory ( { name : 'angular-build-dl' } ) ;
79+ const packageVersion = require ( '../../package.json' ) . version ;
80+
7381export type BrowserBuilderOutput = json . JsonObject &
7482 BuilderOutput & {
7583 outputPath : string ;
@@ -240,6 +248,7 @@ export function buildWebpackBrowser(
240248 1 ,
241249 ) ,
242250 bufferCount ( configs . length ) ,
251+ // tslint:disable-next-line: no-big-function
243252 switchMap ( async buildEvents => {
244253 configs . length = 0 ;
245254 const success = buildEvents . every ( r => r . success ) ;
@@ -274,9 +283,10 @@ export function buildWebpackBrowser(
274283 optimize : normalizeOptimization ( options . optimization ) . scripts ,
275284 sourceMaps : sourceMapOptions . scripts ,
276285 hiddenSourceMaps : sourceMapOptions . hidden ,
286+ vendorSourceMaps : sourceMapOptions . vendor ,
277287 } ;
278288
279- const actions : { } [ ] = [ ] ;
289+ const actions : ProcessBundleOptions [ ] = [ ] ;
280290 const seen = new Set < string > ( ) ;
281291 for ( const file of emittedFiles ) {
282292 // Scripts and non-javascript files are not processed
@@ -348,6 +358,7 @@ export function buildWebpackBrowser(
348358 code,
349359 map,
350360 runtime : file . file . startsWith ( 'runtime' ) ,
361+ ignoreOriginal : es5Polyfills ,
351362 } ) ;
352363
353364 // Add the newly created ES5 bundles to the index as nomodule scripts
@@ -359,30 +370,133 @@ export function buildWebpackBrowser(
359370
360371 // Execute the bundle processing actions
361372 context . logger . info ( 'Generating ES5 bundles for differential loading...' ) ;
362- await new Promise < void > ( ( resolve , reject ) => {
363- const workerFile = require . resolve ( '../utils/process-bundle' ) ;
364- const workers = workerFarm (
365- {
366- maxRetries : 1 ,
367- } ,
368- path . extname ( workerFile ) !== '.ts'
369- ? workerFile
370- : require . resolve ( '../utils/process-bundle-bootstrap' ) ,
371- [ 'process' ] ,
372- ) ;
373- let completed = 0 ;
374- const workCallback = ( error : Error | null ) => {
375- if ( error ) {
376- workerFarm . end ( workers ) ;
377- reject ( error ) ;
378- } else if ( ++ completed === actions . length ) {
379- workerFarm . end ( workers ) ;
380- resolve ( ) ;
373+
374+ const processActions : typeof actions = [ ] ;
375+ const cacheActions : { src : string ; dest : string } [ ] = [ ] ;
376+ for ( const action of actions ) {
377+ // Create base cache key with elements:
378+ // * package version - different build-angular versions cause different final outputs
379+ // * code length/hash - ensure cached version matches the same input code
380+ const codeHash = createHash ( 'sha1' )
381+ . update ( action . code )
382+ . digest ( 'hex' ) ;
383+ const baseCacheKey = `${ packageVersion } |${ action . code . length } |${ codeHash } ` ;
384+
385+ // Postfix added to sourcemap cache keys when vendor sourcemaps are present
386+ // Allows non-destructive caching of both variants
387+ const SourceMapVendorPostfix =
388+ ! ! action . sourceMaps && action . vendorSourceMaps ? '|vendor' : '' ;
389+
390+ // Determine cache entries required based on build settings
391+ const cacheKeys = [ ] ;
392+
393+ // If optimizing and the original is not ignored, add original as required
394+ if ( ( action . optimize || action . optimizeOnly ) && ! action . ignoreOriginal ) {
395+ cacheKeys [ CacheKey . OriginalCode ] = baseCacheKey + '|orig' ;
396+
397+ // If sourcemaps are enabled, add original sourcemap as required
398+ if ( action . sourceMaps ) {
399+ cacheKeys [ CacheKey . OriginalMap ] =
400+ baseCacheKey + SourceMapVendorPostfix + '|orig-map' ;
401+ }
402+ }
403+ // If not only optimizing, add downlevel as required
404+ if ( ! action . optimizeOnly ) {
405+ cacheKeys [ CacheKey . DownlevelCode ] = baseCacheKey + '|dl' ;
406+
407+ // If sourcemaps are enabled, add downlevel sourcemap as required
408+ if ( action . sourceMaps ) {
409+ cacheKeys [ CacheKey . DownlevelMap ] =
410+ baseCacheKey + SourceMapVendorPostfix + '|dl-map' ;
411+ }
412+ }
413+
414+ // Attempt to get required cache entries
415+ const cacheEntries = [ ] ;
416+ for ( const key of cacheKeys ) {
417+ if ( key ) {
418+ cacheEntries . push ( await cacache . get . info ( cacheDownlevelPath , key ) ) ;
419+ } else {
420+ cacheEntries . push ( null ) ;
421+ }
422+ }
423+
424+ // Check if required cache entries are present
425+ let cached = cacheKeys . length > 0 ;
426+ for ( let i = 0 ; i < cacheKeys . length ; ++ i ) {
427+ if ( cacheKeys [ i ] && ! cacheEntries [ i ] ) {
428+ cached = false ;
429+ break ;
430+ }
431+ }
432+
433+ // If all required cached entries are present, use the cached entries
434+ // Otherwise process the files
435+ if ( cached ) {
436+ if ( cacheEntries [ CacheKey . OriginalCode ] ) {
437+ cacheActions . push ( {
438+ src : cacheEntries [ CacheKey . OriginalCode ] . path ,
439+ dest : action . filename ,
440+ } ) ;
381441 }
382- } ;
442+ if ( cacheEntries [ CacheKey . OriginalMap ] ) {
443+ cacheActions . push ( {
444+ src : cacheEntries [ CacheKey . OriginalMap ] . path ,
445+ dest : action . filename + '.map' ,
446+ } ) ;
447+ }
448+ if ( cacheEntries [ CacheKey . DownlevelCode ] ) {
449+ cacheActions . push ( {
450+ src : cacheEntries [ CacheKey . DownlevelCode ] . path ,
451+ dest : action . filename . replace ( 'es2015' , 'es5' ) ,
452+ } ) ;
453+ }
454+ if ( cacheEntries [ CacheKey . DownlevelMap ] ) {
455+ cacheActions . push ( {
456+ src : cacheEntries [ CacheKey . DownlevelMap ] . path ,
457+ dest : action . filename . replace ( 'es2015' , 'es5' ) + '.map' ,
458+ } ) ;
459+ }
460+ } else {
461+ processActions . push ( {
462+ ...action ,
463+ cacheKeys,
464+ cachePath : cacheDownlevelPath || undefined ,
465+ } ) ;
466+ }
467+ }
468+
469+ for ( const action of cacheActions ) {
470+ fs . copyFileSync ( action . src , action . dest , fs . constants . COPYFILE_FICLONE ) ;
471+ }
472+
473+ if ( processActions . length > 0 ) {
474+ await new Promise < void > ( ( resolve , reject ) => {
475+ const workerFile = require . resolve ( '../utils/process-bundle' ) ;
476+ const workers = workerFarm (
477+ {
478+ maxRetries : 1 ,
479+ } ,
480+ path . extname ( workerFile ) !== '.ts'
481+ ? workerFile
482+ : require . resolve ( '../utils/process-bundle-bootstrap' ) ,
483+ [ 'process' ] ,
484+ ) ;
485+ let completed = 0 ;
486+ const workCallback = ( error : Error | null ) => {
487+ if ( error ) {
488+ workerFarm . end ( workers ) ;
489+ reject ( error ) ;
490+ } else if ( ++ completed === processActions . length ) {
491+ workerFarm . end ( workers ) ;
492+ resolve ( ) ;
493+ }
494+ } ;
495+
496+ processActions . forEach ( action => workers [ 'process' ] ( action , workCallback ) ) ;
497+ } ) ;
498+ }
383499
384- actions . forEach ( action => workers [ 'process' ] ( action , workCallback ) ) ;
385- } ) ;
386500 context . logger . info ( 'ES5 bundle generation complete.' ) ;
387501 } else {
388502 const { emittedFiles = [ ] } = firstBuild ;
0 commit comments