import { BuilderContext, createBuilder } from '@angular-devkit/architect'; import { join as devkitJoin, JsonObject, normalize, } from '@angular-devkit/core'; import { BuildResult, runWebpack } from '@angular-devkit/build-webpack'; import { from, of } from 'rxjs'; import { normalizeWebBuildOptions } from '../../utils/normalize'; import { getWebConfig } from '../../utils/web.config'; import { BuildBuilderOptions } from '../../utils/types'; import { bufferCount, map, mergeScan, switchMap } from 'rxjs/operators'; import { getSourceRoot } from '../../utils/source-root'; import { writeIndexHtml } from '../../utils/third-party/cli-files/utilities/index-file/write-index-html'; import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { execSync } from 'child_process'; import { Range, satisfies } from 'semver'; import { basename } from 'path'; import { createProjectGraph } from '@nrwl/workspace/src/core/project-graph'; import { calculateProjectDependencies, createTmpTsConfig, } from '@nrwl/workspace/src/utils/buildable-libs-utils'; import { CrossOriginValue } from '../../utils/third-party/cli-files/utilities/index-file/augment-index-html'; export interface WebBuildBuilderOptions extends BuildBuilderOptions { index: string; budgets: any[]; baseHref: string; deployUrl: string; extractCss?: boolean; crossOrigin?: CrossOriginValue; polyfills?: string; es2015Polyfills?: string; scripts: string[]; styles: string[]; vendorChunk?: boolean; commonChunk?: boolean; stylePreprocessingOptions?: any; subresourceIntegrity?: boolean; verbose?: boolean; buildLibsFromSource?: boolean; } export default createBuilder(run); export function run(options: WebBuildBuilderOptions, context: BuilderContext) { const host = new NodeJsSyncHost(); const isScriptOptimizeOn = typeof options.optimization === 'boolean' ? options.optimization : options.optimization && options.optimization.scripts ? options.optimization.scripts : false; // Node versions 12.2-12.8 has a bug where prod builds will hang for 2-3 minutes // after the program exits. const nodeVersion = execSync(`node --version`).toString('utf-8').trim(); const supportedRange = new Range('10 || >=12.9'); if (!satisfies(nodeVersion, supportedRange)) { throw new Error( `Node version ${nodeVersion} is not supported. Supported range is "${supportedRange.raw}".` ); } if (!options.buildLibsFromSource) { const projGraph = createProjectGraph(); const { target, dependencies } = calculateProjectDependencies( projGraph, context ); options.tsConfig = createTmpTsConfig( options.tsConfig, context.workspaceRoot, target.data.root, dependencies ); } return from(getSourceRoot(context, host)) .pipe( map((sourceRoot) => { options = normalizeWebBuildOptions( options, context.workspaceRoot, sourceRoot ); return [ // ESM build for modern browsers. getWebConfig( context.workspaceRoot, sourceRoot, options, context.logger, true, isScriptOptimizeOn ), // ES5 build for legacy browsers. isScriptOptimizeOn ? getWebConfig( context.workspaceRoot, sourceRoot, options, context.logger, false, isScriptOptimizeOn ) : undefined, ] .filter(Boolean) .map((config) => options.webpackConfig ? require(options.webpackConfig)(config, { options, configuration: context.target.configuration, }) : config ); }) ) .pipe( switchMap((configs) => from(configs).pipe( // Run build sequentially and bail when first one fails. mergeScan( (acc, config) => { if (acc.success) { return runWebpack(config, context, { logging: (stats) => { context.logger.info(stats.toString(config.stats)); }, webpackFactory: require('webpack'), }); } else { return of(); } }, { success: true } as BuildResult, 1 ), // Collect build results as an array. bufferCount(configs.length) ) ), switchMap(([result1, result2 = { success: true, emittedFiles: [] }]) => { const success = [result1, result2].every((result) => result.success); return (options.optimization ? writeIndexHtml({ crossOrigin: options.crossOrigin, host, outputPath: devkitJoin( normalize(options.outputPath), basename(options.index) ), indexPath: devkitJoin( normalize(context.workspaceRoot), options.index ), files: result1.emittedFiles.filter((x) => x.extension === '.css'), noModuleFiles: result2.emittedFiles, moduleFiles: result1.emittedFiles, baseHref: options.baseHref, deployUrl: options.deployUrl, scripts: options.scripts, styles: options.styles, }) : of(null) ).pipe( map( () => ({ success, emittedFiles: [ ...result1.emittedFiles, ...result2.emittedFiles, ], } as BuildResult) ) ); }) ); }