nx/packages/web/src/executors/webpack/webpack.impl.ts

214 lines
6.2 KiB
TypeScript

import type { ExecutorContext } from '@nrwl/devkit';
import type { Configuration, Stats } from 'webpack';
import { from, of } from 'rxjs';
import { bufferCount, mergeScan, switchMap, tap } from 'rxjs/operators';
import { eachValueFrom } from 'rxjs-for-await';
import { execSync } from 'child_process';
import { Range, satisfies } from 'semver';
import { basename, join } from 'path';
import { readCachedProjectGraph } from '@nrwl/workspace/src/core/project-graph';
import {
calculateProjectDependencies,
checkDependentProjectsHaveBeenBuilt,
createTmpTsConfig,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import { normalizeWebBuildOptions } from '../../utils/normalize';
import { getWebConfig } from '../../utils/web.config';
import type { BuildBuilderOptions } from '../../utils/shared-models';
import { ExtraEntryPoint } from '../../utils/shared-models';
import { getEmittedFiles, runWebpack } from '../../utils/run-webpack';
import { BuildBrowserFeatures } from '../../utils/webpack/build-browser-features';
import { deleteOutputDir } from '../../utils/fs';
import {
CrossOriginValue,
writeIndexHtml,
} from '../../utils/webpack/write-index-html';
export interface WebWebpackExecutorOptions extends BuildBuilderOptions {
index: string;
budgets?: any[];
baseHref?: string;
deployUrl?: string;
crossOrigin?: CrossOriginValue;
polyfills?: string;
es2015Polyfills?: string;
scripts: ExtraEntryPoint[];
styles: ExtraEntryPoint[];
vendorChunk?: boolean;
commonChunk?: boolean;
namedChunks?: boolean;
stylePreprocessorOptions?: any;
subresourceIntegrity?: boolean;
verbose?: boolean;
buildLibsFromSource?: boolean;
deleteOutputPath?: boolean;
generateIndexHtml?: boolean;
}
function getWebpackConfigs(
options: WebWebpackExecutorOptions,
context: ExecutorContext
): Configuration[] {
const metadata = context.workspace.projects[context.projectName];
const sourceRoot = metadata.sourceRoot;
const projectRoot = metadata.root;
options = normalizeWebBuildOptions(options, context.root, sourceRoot);
const isScriptOptimizeOn =
typeof options.optimization === 'boolean'
? options.optimization
: options.optimization && options.optimization.scripts
? options.optimization.scripts
: false;
const tsConfig = readTsConfig(options.tsConfig);
const scriptTarget = tsConfig.options.target;
const buildBrowserFeatures = new BuildBrowserFeatures(
projectRoot,
scriptTarget
);
return [
// ESM build for modern browsers.
getWebConfig(
context.root,
projectRoot,
sourceRoot,
options,
true,
isScriptOptimizeOn,
context.configurationName
),
// ES5 build for legacy browsers.
isScriptOptimizeOn && buildBrowserFeatures.isDifferentialLoadingNeeded()
? getWebConfig(
context.root,
projectRoot,
sourceRoot,
options,
false,
isScriptOptimizeOn,
context.configurationName
)
: undefined,
]
.filter(Boolean)
.map((config) =>
options.webpackConfig
? require(options.webpackConfig)(config, {
options,
configuration: context.configurationName,
})
: config
);
}
export async function* run(
options: WebWebpackExecutorOptions,
context: ExecutorContext
) {
// 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}".`
);
}
process.env.NODE_ENV ||= 'production';
const metadata = context.workspace.projects[context.projectName];
if (!options.buildLibsFromSource && context.targetName) {
const { dependencies } = calculateProjectDependencies(
readCachedProjectGraph(),
context.root,
context.projectName,
context.targetName,
context.configurationName
);
options.tsConfig = createTmpTsConfig(
join(context.root, options.tsConfig),
context.root,
metadata.root,
dependencies
);
if (
!checkDependentProjectsHaveBeenBuilt(
context.root,
context.projectName,
context.targetName,
dependencies
)
) {
throw new Error();
}
}
// Delete output path before bundling
if (options.deleteOutputPath) {
deleteOutputDir(context.root, options.outputPath);
}
const configs = getWebpackConfigs(options, context);
return yield* eachValueFrom(
from(configs).pipe(
// Run build sequentially and bail when first one fails.
mergeScan(
(acc, config) => {
if (!acc.hasErrors()) {
return runWebpack(config).pipe(
tap((stats) => {
console.info(stats.toString(config.stats));
})
);
} else {
return of();
}
},
{ hasErrors: () => false } as Stats,
1
),
// Collect build results as an array.
bufferCount(configs.length),
switchMap(async ([result1, result2]) => {
const success =
result1 && !result1.hasErrors() && (!result2 || !result2.hasErrors());
const emittedFiles1 = getEmittedFiles(result1);
const emittedFiles2 = result2 ? getEmittedFiles(result2) : [];
if (options.generateIndexHtml) {
await writeIndexHtml({
crossOrigin: options.crossOrigin,
outputPath: join(options.outputPath, basename(options.index)),
indexPath: join(context.root, options.index),
files: emittedFiles1.filter((x) => x.extension === '.css'),
noModuleFiles: emittedFiles2,
moduleFiles: emittedFiles1,
baseHref: options.baseHref,
deployUrl: options.deployUrl,
scripts: options.scripts,
styles: options.styles,
});
}
return { success, emittedFiles: [...emittedFiles1, ...emittedFiles2] };
})
)
);
}
export default run;