From f3a9f55fc7e25ecfadeace8bd59ae2e111917003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Thu, 16 Jun 2022 14:58:45 +0100 Subject: [PATCH] feat(angular): add support for incremental builds to the webpack-server executor (#10754) --- docs/generated/packages/angular.json | 4 + packages/angular/package.json | 1 + .../src/builders/webpack-server/schema.d.ts | 1 + .../src/builders/webpack-server/schema.json | 4 + .../webpack-server/webpack-server.impl.ts | 156 ++++++++++++------ 5 files changed, 116 insertions(+), 50 deletions(-) diff --git a/docs/generated/packages/angular.json b/docs/generated/packages/angular.json index 2bc0cb20b9b90..8b87f72678b40 100644 --- a/docs/generated/packages/angular.json +++ b/docs/generated/packages/angular.json @@ -2999,6 +2999,10 @@ "poll": { "type": "number", "description": "Enable and define the file watching poll time period in milliseconds." + }, + "buildLibsFromSource": { + "type": "boolean", + "description": "Read buildable libraries from source instead of building them separately. If not set, it will take the value specified in the `browserTarget` options, or it will default to `true` if it's also not set in the `browserTarget` options." } }, "additionalProperties": false, diff --git a/packages/angular/package.json b/packages/angular/package.json index 49fe79a0b672a..148020273a598 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -43,6 +43,7 @@ "@nrwl/jest": "file:../jest", "@nrwl/linter": "file:../linter", "@nrwl/storybook": "file:../storybook", + "@nrwl/web": "file:../web", "@nrwl/workspace": "file:../workspace", "@phenomnomnominal/tsquery": "4.1.1", "@schematics/angular": "~14.0.0", diff --git a/packages/angular/src/builders/webpack-server/schema.d.ts b/packages/angular/src/builders/webpack-server/schema.d.ts index 5ebdefd1fb25b..f534977441bf7 100644 --- a/packages/angular/src/builders/webpack-server/schema.d.ts +++ b/packages/angular/src/builders/webpack-server/schema.d.ts @@ -17,4 +17,5 @@ export interface Schema { hmr?: boolean; watch?: boolean; poll?: number; + buildLibsFromSource?: boolean; } diff --git a/packages/angular/src/builders/webpack-server/schema.json b/packages/angular/src/builders/webpack-server/schema.json index 2dd9ae9622a3d..232044420ac73 100644 --- a/packages/angular/src/builders/webpack-server/schema.json +++ b/packages/angular/src/builders/webpack-server/schema.json @@ -102,6 +102,10 @@ "poll": { "type": "number", "description": "Enable and define the file watching poll time period in milliseconds." + }, + "buildLibsFromSource": { + "type": "boolean", + "description": "Read buildable libraries from source instead of building them separately. If not set, it will take the value specified in the `browserTarget` options, or it will default to `true` if it's also not set in the `browserTarget` options." } }, "additionalProperties": false, diff --git a/packages/angular/src/builders/webpack-server/webpack-server.impl.ts b/packages/angular/src/builders/webpack-server/webpack-server.impl.ts index 737625cbc0ea6..18474f7dfcce6 100644 --- a/packages/angular/src/builders/webpack-server/webpack-server.impl.ts +++ b/packages/angular/src/builders/webpack-server/webpack-server.impl.ts @@ -1,31 +1,38 @@ -import { BuilderContext, createBuilder } from '@angular-devkit/architect'; +import { + BuilderContext, + BuilderOutput, + createBuilder, +} from '@angular-devkit/architect'; import { DevServerBuilderOptions, - serveWebpackBrowser, -} from '@angular-devkit/build-angular/src/builders/dev-server'; + executeDevServerBuilder, +} from '@angular-devkit/build-angular'; import { JsonObject } from '@angular-devkit/core'; import { joinPathFragments, parseTargetString, readAllWorkspaceConfiguration, - Workspaces, + readCachedProjectGraph, } from '@nrwl/devkit'; +import { WebpackNxBuildCoordinationPlugin } from '@nrwl/web/src/plugins/webpack-nx-build-coordination-plugin'; +import { + calculateProjectDependencies, + createTmpTsConfig, + DependentBuildableProjectNode, +} from '@nrwl/workspace/src/utilities/buildable-libs-utils'; import { existsSync } from 'fs'; +import { isNpmProject } from 'nx/src/project-graph/operators'; +import { Observable } from 'rxjs'; import { merge } from 'webpack-merge'; import { resolveCustomWebpackConfig } from '../utilities/webpack'; import { normalizeOptions } from './lib'; import type { Schema } from './schema'; export function executeWebpackServerBuilder( - schema: Schema, + rawOptions: Schema, context: BuilderContext -) { - process.env.NX_TSCONFIG_PATH = joinPathFragments( - context.workspaceRoot, - 'tsconfig.base.json' - ); - - const options = normalizeOptions(schema); +): Observable { + const options = normalizeOptions(rawOptions); const workspaceConfig = readAllWorkspaceConfiguration(); const parsedBrowserTarget = parseTargetString(options.browserTarget); @@ -34,62 +41,111 @@ export function executeWebpackServerBuilder( parsedBrowserTarget.target ]; - const selectedConfiguration = parsedBrowserTarget.configuration + const buildTargetConfiguration = parsedBrowserTarget.configuration ? buildTarget.configurations[parsedBrowserTarget.configuration] : buildTarget.defaultConfiguration ? buildTarget.configurations[buildTarget.defaultConfiguration] - : buildTarget.options; + : undefined; + + const buildLibsFromSource = + options.buildLibsFromSource ?? + buildTargetConfiguration?.buildLibsFromSource ?? + buildTarget.options.buildLibsFromSource ?? + true; const customWebpackConfig: { path: string } = - selectedConfiguration.customWebpackConfig ?? + buildTargetConfiguration?.customWebpackConfig ?? buildTarget.options.customWebpackConfig; + let pathToWebpackConfig: string; if (customWebpackConfig && customWebpackConfig.path) { - const pathToWebpackConfig = joinPathFragments( + pathToWebpackConfig = joinPathFragments( context.workspaceRoot, customWebpackConfig.path ); - if (existsSync(pathToWebpackConfig)) { - return serveWebpackBrowser( - options as DevServerBuilderOptions, - context as any, - { - webpackConfiguration: async (baseWebpackConfig) => { - const customWebpackConfiguration = resolveCustomWebpackConfig( - pathToWebpackConfig, - buildTarget.options.tsConfig - ); - // The extra Webpack configuration file can also export a Promise, for instance: - // `module.exports = new Promise(...)`. If it exports a single object, but not a Promise, - // then await will just resolve that object. - const config = await customWebpackConfiguration; - - // The extra Webpack configuration file can export a synchronous or asynchronous function, - // for instance: `module.exports = async config => { ... }`. - if (typeof config === 'function') { - return config( - baseWebpackConfig, - selectedConfiguration, - context.target - ); - } else { - return merge(baseWebpackConfig, config); - } - }, - } - ); - } else { + if (!existsSync(pathToWebpackConfig)) { throw new Error( `Custom Webpack Config File Not Found!\nTo use a custom webpack config, please ensure the path to the custom webpack file is correct: \n${pathToWebpackConfig}` ); } } - return serveWebpackBrowser( - options as DevServerBuilderOptions, - context as any - ); + let dependencies: DependentBuildableProjectNode[]; + if (!buildLibsFromSource) { + const buildTargetTsConfigPath = + buildTargetConfiguration?.tsConfig ?? buildTarget.options.tsConfig; + const result = calculateProjectDependencies( + readCachedProjectGraph(), + context.workspaceRoot, + context.target.project, + parsedBrowserTarget.target, + context.target.configuration + ); + dependencies = result.dependencies; + const updatedTsConfig = createTmpTsConfig( + joinPathFragments(context.workspaceRoot, buildTargetTsConfigPath), + context.workspaceRoot, + result.target.data.root, + dependencies + ); + process.env.NX_TSCONFIG_PATH = updatedTsConfig; + + // We can't just pass the tsconfig path in memory to the angular builder + // function because we can't pass the build target options to it, the build + // targets options will be retrieved by the builder from the project + // configuration. Therefore, we patch the method in the context to retrieve + // the target options to overwrite the tsconfig path to use the generated + // one with the updated path mappings. + const originalGetTargetOptions = context.getTargetOptions; + context.getTargetOptions = async (target) => { + const options = await originalGetTargetOptions(target); + options.tsConfig = updatedTsConfig; + return options; + }; + } + + return executeDevServerBuilder(options as DevServerBuilderOptions, context, { + webpackConfiguration: async (baseWebpackConfig) => { + if (!buildLibsFromSource) { + const workspaceDependencies = dependencies + .filter((dep) => !isNpmProject(dep.node)) + .map((dep) => dep.node.name); + baseWebpackConfig.plugins.push( + new WebpackNxBuildCoordinationPlugin( + `nx run-many --target=${ + parsedBrowserTarget.target + } --projects=${workspaceDependencies.join(',')}` + ) + ); + } + + if (!pathToWebpackConfig) { + return baseWebpackConfig; + } + + const customWebpackConfiguration = resolveCustomWebpackConfig( + pathToWebpackConfig, + buildTarget.options.tsConfig + ); + // The extra Webpack configuration file can also export a Promise, for instance: + // `module.exports = new Promise(...)`. If it exports a single object, but not a Promise, + // then await will just resolve that object. + const config = await customWebpackConfiguration; + + // The extra Webpack configuration file can export a synchronous or asynchronous function, + // for instance: `module.exports = async config => { ... }`. + if (typeof config === 'function') { + return config( + baseWebpackConfig, + buildTargetConfiguration, + context.target + ); + } + + return merge(baseWebpackConfig, config); + }, + }); } export default createBuilder(