Skip to content

Commit

Permalink
feat(angular): add support for incremental builds to the webpack-serv…
Browse files Browse the repository at this point in the history
…er executor
  • Loading branch information
leosvelperez committed Jun 15, 2022
1 parent a097997 commit c3ab94d
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 51 deletions.
5 changes: 5 additions & 0 deletions docs/generated/packages/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -2999,6 +2999,11 @@
"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.",
"default": true
}
},
"additionalProperties": false,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@ngrx/schematics": "~13.2.0",
"@ngrx/store": "~13.2.0",
"@ngrx/store-devtools": "~13.2.0",
"@ngtools/webpack": "~14.0.0",
"@nrwl/eslint-plugin-nx": "14.3.2",
"@nrwl/jest": "14.3.2",
"@nrwl/next": "14.3.2",
Expand Down
1 change: 1 addition & 0 deletions packages/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { BuilderContext } from '@angular-devkit/architect';
import { executeDevServerBuilder } from '@angular-devkit/build-angular';
import { AngularWebpackPlugin } from '@ngtools/webpack';
import { joinPathFragments } from '@nrwl/devkit';
import { WebpackNxBuildCoordinationPlugin } from '@nrwl/web/src/plugins/webpack-nx-build-coordination-plugin';
import {
createTmpTsConfig,
DependentBuildableProjectNode,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { isNpmProject } from 'nx/src/project-graph/operators';

type Transforms = Parameters<typeof executeDevServerBuilder>[2];
type WebpackConfiguration = Parameters<Transforms['webpackConfiguration']>[0];

export function configureBuildableLibsSupport(
baseWebpackConfig: WebpackConfiguration,
context: BuilderContext,
projectRoot: string,
buildTargetName: string,
buildTargetTsConfigPath: string,
dependencies: DependentBuildableProjectNode[]
) {
const tsConfig = createTmpTsConfig(
joinPathFragments(context.workspaceRoot, buildTargetTsConfigPath),
context.workspaceRoot,
projectRoot,
dependencies
);
process.env.NX_TSCONFIG_PATH = tsConfig;

// 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 from the project configuration.
// Therefore, we overwrite the tsconfig path that comes in the webpack
// configuration for the AngularWebpackPlugin to use the temporary tsconfig.
const angularWebpackPlugin = baseWebpackConfig.plugins.find(
(plugin) => plugin.constructor.name === AngularWebpackPlugin.name
) as unknown as AngularWebpackPlugin;
angularWebpackPlugin.options.tsconfig = tsConfig;

const deps = dependencies
.filter((dep) => !isNpmProject(dep.node))
.map((dep) => dep.node.name);
baseWebpackConfig.plugins.push(
new WebpackNxBuildCoordinationPlugin(
`nx run-many --target=${buildTargetName} --projects=${deps.join(',')}`
)
);
}
1 change: 1 addition & 0 deletions packages/angular/src/builders/webpack-server/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './configure-buildable-libs-support';
export * from './normalize-options';
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export function normalizeOptions(schema: Schema): Schema {
liveReload: true,
open: false,
ssl: false,
buildLibsFromSource: true,
...schema,
};
}
1 change: 1 addition & 0 deletions packages/angular/src/builders/webpack-server/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ export interface Schema {
hmr?: boolean;
watch?: boolean;
poll?: number;
buildLibsFromSource?: boolean;
}
5 changes: 5 additions & 0 deletions packages/angular/src/builders/webpack-server/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@
"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.",
"default": true
}
},
"additionalProperties": false,
Expand Down
157 changes: 107 additions & 50 deletions packages/angular/src/builders/webpack-server/webpack-server.impl.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,41 @@
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';
DevServerBuilderOutput,
executeDevServerBuilder,
} from '@angular-devkit/build-angular';
import { JsonObject } from '@angular-devkit/core';
import {
joinPathFragments,
parseTargetString,
readAllWorkspaceConfiguration,
Workspaces,
readCachedProjectGraph,
Target,
} from '@nrwl/devkit';
import {
calculateProjectDependencies,
checkDependentProjectsHaveBeenBuilt,
DependentBuildableProjectNode,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { existsSync } from 'fs';
import { Observable, of } from 'rxjs';
import { merge } from 'webpack-merge';
import { resolveCustomWebpackConfig } from '../utilities/webpack';
import { normalizeOptions } from './lib';
import { configureBuildableLibsSupport, normalizeOptions } from './lib';
import type { Schema } from './schema';

export function executeWebpackServerBuilder(
schema: Schema,
context: BuilderContext
) {
process.env.NX_TSCONFIG_PATH = joinPathFragments(
context.workspaceRoot,
'tsconfig.base.json'
);

const options = normalizeOptions(schema);
function runServe(
options: Schema,
context: BuilderContext,
parsedBrowserTarget: Target,
projectRoot?: string,
dependencies?: DependentBuildableProjectNode[]
): Observable<DevServerBuilderOutput> {
const workspaceConfig = readAllWorkspaceConfiguration();

const parsedBrowserTarget = parseTargetString(options.browserTarget);
const buildTarget =
workspaceConfig.projects[parsedBrowserTarget.project].targets[
parsedBrowserTarget.target
Expand All @@ -44,54 +51,104 @@ export function executeWebpackServerBuilder(
selectedConfiguration.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(
return executeDevServerBuilder(
options as DevServerBuilderOptions,
context as any
context as any,
{
webpackConfiguration: async (baseWebpackConfig) => {
if (!options.buildLibsFromSource) {
configureBuildableLibsSupport(
baseWebpackConfig,
context,
projectRoot,
parsedBrowserTarget.target,
buildTarget.options.tsConfig,
dependencies
);
}

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,
selectedConfiguration,
context.target
);
}

return merge(baseWebpackConfig, config);
},
}
);
}

export function executeWebpackServerBuilder(
rawOptions: Schema,
context: BuilderContext
): Observable<BuilderOutput> {
const options = normalizeOptions(rawOptions);
const parsedBrowserTarget = parseTargetString(options.browserTarget);

if (options.buildLibsFromSource) {
const { dependencies, target } = calculateProjectDependencies(
readCachedProjectGraph(),
context.workspaceRoot,
context.target.project,
parsedBrowserTarget.target,
context.target.configuration
);

if (
!checkDependentProjectsHaveBeenBuilt(
context.workspaceRoot,
context.target.project,
context.target.target,
dependencies
)
) {
return of({ success: false });
}

return runServe(
options,
context,
parsedBrowserTarget,
target.data.root,
dependencies
);
}

return runServe(options, context, parsedBrowserTarget);
}

export default createBuilder<JsonObject & Schema>(
executeWebpackServerBuilder
) as any;
1 change: 1 addition & 0 deletions packages/angular/src/generators/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ function updateDependencies(host: Tree): GeneratorCallback {
'@angular/compiler-cli': angularVersion,
'@angular/language-service': angularVersion,
'@angular-devkit/build-angular': angularDevkitVersion,
'@ngtools/webpack': angularDevkitVersion,
}
);
}
Expand Down
1 change: 1 addition & 0 deletions scripts/depcheck/missing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const IGNORE_MATCHES = {
'@angular/compiler-cli',
'@angular/core',
'@angular/router',
'@ngtools/webpack',
'@ngrx/effects',
'@ngrx/router-store',
'@ngrx/store',
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3969,7 +3969,7 @@
dependencies:
tslib "^2.0.0"

"@ngtools/[email protected]":
"@ngtools/[email protected]", "@ngtools/webpack@~14.0.0":
version "14.0.0"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-14.0.0.tgz#13a37933f13a8aa587697b7db7be6f1f20a360c1"
integrity sha512-lUcJ5DiRCY+Xj01R38bDbAlELbO+0yT3n4zshBeiuzo/Lc+yQM2XhVe9stsI5I+SwH6YsCU9kp+jb7iaZh7L7w==
Expand Down

0 comments on commit c3ab94d

Please sign in to comment.