Skip to content

Commit e8dab63

Browse files
committed
feat(@angular-devkit/build-angular): add Browser Builder v2
It exposes a lot of functions that can be reused to run webpack with different config, so third party builders can reuse those.
1 parent 041907d commit e8dab63

File tree

2 files changed

+299
-0
lines changed

2 files changed

+299
-0
lines changed

packages/angular_devkit/build_angular/builders.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"description": "Build a server app and a browser app, then render the index.html and use it for the browser output."
88
},
99
"browser": {
10+
"implementation": "./src/browser/index2",
1011
"class": "./src/browser",
1112
"schema": "./src/browser/schema.json",
1213
"description": "Build a browser app."
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect/src/index2';
9+
import {
10+
WebpackLoggingCallback,
11+
runWebpack,
12+
} from '@angular-devkit/build-webpack/src/webpack/index2';
13+
import {
14+
Path,
15+
experimental,
16+
getSystemPath,
17+
join,
18+
json,
19+
logging,
20+
normalize,
21+
resolve,
22+
schema,
23+
virtualFs,
24+
} from '@angular-devkit/core';
25+
import { NodeJsSyncHost } from '@angular-devkit/core/node';
26+
import * as fs from 'fs';
27+
import { EMPTY, Observable, from, of } from 'rxjs';
28+
import { concatMap, last, map, switchMap } from 'rxjs/operators';
29+
import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies
30+
import { WebpackConfigOptions } from '../angular-cli-files/models/build-options';
31+
import {
32+
getAotConfig,
33+
getBrowserConfig,
34+
getCommonConfig,
35+
getNonAotConfig,
36+
getStatsConfig,
37+
getStylesConfig,
38+
} from '../angular-cli-files/models/webpack-configs';
39+
import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig';
40+
import { requireProjectModule } from '../angular-cli-files/utilities/require-project-module';
41+
import { augmentAppWithServiceWorker } from '../angular-cli-files/utilities/service-worker';
42+
import {
43+
statsErrorsToString,
44+
statsToString,
45+
statsWarningsToString,
46+
} from '../angular-cli-files/utilities/stats';
47+
import { NormalizedBrowserBuilderSchema, defaultProgress, normalizeBrowserSchema } from '../utils';
48+
import { Schema as BrowserBuilderSchema } from './schema';
49+
50+
import webpack = require('webpack');
51+
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
52+
const webpackMerge = require('webpack-merge');
53+
54+
55+
export type BrowserBuilderOutput = json.JsonObject & BuilderOutput & {
56+
outputPath: string;
57+
};
58+
59+
60+
function _deleteOutputDir(root: Path, outputPath: Path, host: virtualFs.Host) {
61+
const resolvedOutputPath = resolve(root, outputPath);
62+
if (resolvedOutputPath === root) {
63+
throw new Error('Output path MUST not be project root directory!');
64+
}
65+
66+
return host.exists(resolvedOutputPath).pipe(
67+
concatMap(exists => exists ? host.delete(resolvedOutputPath) : EMPTY),
68+
last(null, null),
69+
);
70+
}
71+
72+
73+
export function createBrowserLoggingCallback(
74+
verbose: boolean,
75+
logger: logging.LoggerApi,
76+
): WebpackLoggingCallback {
77+
return (stats, config) => {
78+
// config.stats contains our own stats settings, added during buildWebpackConfig().
79+
const json = stats.toJson(config.stats);
80+
if (verbose) {
81+
logger.info(stats.toString(config.stats));
82+
} else {
83+
logger.info(statsToString(json, config.stats));
84+
}
85+
86+
if (stats.hasWarnings()) {
87+
logger.warn(statsWarningsToString(json, config.stats));
88+
}
89+
if (stats.hasErrors()) {
90+
logger.error(statsErrorsToString(json, config.stats));
91+
}
92+
};
93+
}
94+
95+
export function buildWebpackConfig(
96+
root: Path,
97+
projectRoot: Path,
98+
host: virtualFs.Host<fs.Stats>,
99+
options: NormalizedBrowserBuilderSchema,
100+
logger: logging.LoggerApi,
101+
): webpack.Configuration {
102+
// Ensure Build Optimizer is only used with AOT.
103+
if (options.buildOptimizer && !options.aot) {
104+
throw new Error(`The 'buildOptimizer' option cannot be used without 'aot'.`);
105+
}
106+
107+
let wco: WebpackConfigOptions<NormalizedBrowserBuilderSchema>;
108+
109+
const tsConfigPath = getSystemPath(normalize(resolve(root, normalize(options.tsConfig))));
110+
const tsConfig = readTsconfig(tsConfigPath);
111+
112+
const projectTs = requireProjectModule(getSystemPath(projectRoot), 'typescript') as typeof ts;
113+
114+
const supportES2015 = tsConfig.options.target !== projectTs.ScriptTarget.ES3
115+
&& tsConfig.options.target !== projectTs.ScriptTarget.ES5;
116+
117+
wco = {
118+
root: getSystemPath(root),
119+
logger: logger.createChild('webpackConfigOptions'),
120+
projectRoot: getSystemPath(projectRoot),
121+
buildOptions: options,
122+
tsConfig,
123+
tsConfigPath,
124+
supportES2015,
125+
};
126+
127+
wco.buildOptions.progress = defaultProgress(wco.buildOptions.progress);
128+
129+
const webpackConfigs: {}[] = [
130+
getCommonConfig(wco),
131+
getBrowserConfig(wco),
132+
getStylesConfig(wco),
133+
getStatsConfig(wco),
134+
];
135+
136+
if (wco.buildOptions.main || wco.buildOptions.polyfills) {
137+
const typescriptConfigPartial = wco.buildOptions.aot
138+
? getAotConfig(wco, host)
139+
: getNonAotConfig(wco, host);
140+
webpackConfigs.push(typescriptConfigPartial);
141+
}
142+
143+
const webpackConfig = webpackMerge(webpackConfigs);
144+
145+
if (options.profile) {
146+
const smp = new SpeedMeasurePlugin({
147+
outputFormat: 'json',
148+
outputTarget: getSystemPath(join(root, 'speed-measure-plugin.json')),
149+
});
150+
151+
return smp.wrap(webpackConfig);
152+
}
153+
154+
return webpackConfig;
155+
}
156+
157+
158+
export async function buildBrowserWebpackConfigFromWorkspace(
159+
options: BrowserBuilderSchema,
160+
projectName: string,
161+
workspace: experimental.workspace.Workspace,
162+
host: virtualFs.Host<fs.Stats>,
163+
logger: logging.LoggerApi,
164+
): Promise<webpack.Configuration> {
165+
// TODO: Use a better interface for workspace access.
166+
const projectRoot = resolve(workspace.root, normalize(workspace.getProject(projectName).root));
167+
const sourceRoot = workspace.getProject(projectName).sourceRoot;
168+
169+
const normalizedOptions = normalizeBrowserSchema(
170+
host,
171+
workspace.root,
172+
projectRoot,
173+
sourceRoot ? resolve(workspace.root, normalize(sourceRoot)) : undefined,
174+
options,
175+
);
176+
177+
return buildWebpackConfig(workspace.root, projectRoot, host, normalizedOptions, logger);
178+
}
179+
180+
181+
export async function buildBrowserWebpackConfigFromContext(
182+
options: BrowserBuilderSchema,
183+
context: BuilderContext,
184+
host: virtualFs.Host<fs.Stats>,
185+
): Promise<{ workspace: experimental.workspace.Workspace, config: webpack.Configuration }> {
186+
const registry = new schema.CoreSchemaRegistry();
187+
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
188+
189+
const workspace = await experimental.workspace.Workspace.fromPath(
190+
host,
191+
normalize(context.workspaceRoot),
192+
registry,
193+
);
194+
195+
const projectName = context.target ? context.target.project : workspace.getDefaultProjectName();
196+
197+
if (!projectName) {
198+
throw new Error('Must either have a target from the context or a default project.');
199+
}
200+
201+
const config = await buildBrowserWebpackConfigFromWorkspace(
202+
options,
203+
projectName,
204+
workspace,
205+
host,
206+
context.logger,
207+
);
208+
209+
return { workspace, config };
210+
}
211+
212+
213+
export type BrowserConfigTransformFn = (
214+
workspace: experimental.workspace.Workspace,
215+
config: webpack.Configuration,
216+
) => Observable<webpack.Configuration>;
217+
218+
export function buildWebpackBrowser(
219+
options: BrowserBuilderSchema,
220+
context: BuilderContext,
221+
transforms: {
222+
config?: BrowserConfigTransformFn,
223+
output?: (output: BrowserBuilderOutput) => Observable<BuilderOutput>,
224+
logging?: WebpackLoggingCallback,
225+
} = {},
226+
) {
227+
const host = new NodeJsSyncHost();
228+
const root = normalize(context.workspaceRoot);
229+
230+
const configFn = transforms.config;
231+
const outputFn = transforms.output;
232+
const loggingFn = transforms.logging
233+
|| createBrowserLoggingCallback(!!options.verbose, context.logger);
234+
235+
// This makes a host observable into a cold one. This is because we want to wait until
236+
// subscription before calling buildBrowserWebpackConfigFromContext, which can throw.
237+
return of(null).pipe(
238+
switchMap(() => from(buildBrowserWebpackConfigFromContext(options, context, host))),
239+
switchMap(({ workspace, config }) => {
240+
if (configFn) {
241+
return configFn(workspace, config).pipe(
242+
map(config => ({ workspace, config })),
243+
);
244+
} else {
245+
return of({ workspace, config });
246+
}
247+
}),
248+
switchMap(({workspace, config}) => {
249+
if (options.deleteOutputPath) {
250+
return _deleteOutputDir(
251+
normalize(context.workspaceRoot),
252+
normalize(options.outputPath),
253+
host,
254+
).pipe(map(() => ({ workspace, config })));
255+
} else {
256+
return of({ workspace, config });
257+
}
258+
}),
259+
switchMap(({ workspace, config }) => {
260+
const projectName = context.target
261+
? context.target.project : workspace.getDefaultProjectName();
262+
263+
if (!projectName) {
264+
throw new Error('Must either have a target from the context or a default project.');
265+
}
266+
267+
const projectRoot = resolve(
268+
workspace.root,
269+
normalize(workspace.getProject(projectName).root),
270+
);
271+
272+
return runWebpack(config, context, { logging: loggingFn }).pipe(
273+
concatMap(buildEvent => {
274+
if (buildEvent.success && !options.watch && options.serviceWorker) {
275+
return from(augmentAppWithServiceWorker(
276+
host,
277+
root,
278+
projectRoot,
279+
resolve(root, normalize(options.outputPath)),
280+
options.baseHref || '/',
281+
options.ngswConfigPath,
282+
).then(() => ({ success: true })));
283+
} else {
284+
return of(buildEvent);
285+
}
286+
}),
287+
map(event => ({
288+
...event,
289+
outputPath: config.output && config.output.path || '',
290+
} as BrowserBuilderOutput)),
291+
concatMap(output => outputFn ? outputFn(output) : of(output)),
292+
);
293+
}),
294+
);
295+
}
296+
297+
298+
export default createBuilder<json.JsonObject & BrowserBuilderSchema>(buildWebpackBrowser);

0 commit comments

Comments
 (0)