Skip to content

Commit e333450

Browse files
AlanKeen Yee Liau
Alan
authored and
Keen Yee Liau
committed
feat(@angular-devkit/build-angular): add a post transformation hook to index generation
Fixes angular#14392
1 parent 3bf929f commit e333450

File tree

10 files changed

+99
-64
lines changed

10 files changed

+99
-64
lines changed

packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/browser.ts

+1-16
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,15 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
9-
import * as path from 'path';
109
import * as webpack from 'webpack';
11-
import { IndexHtmlWebpackPlugin } from '../../plugins/index-html-webpack-plugin';
12-
import { generateEntryPoints } from '../../utilities/package-chunk-sort';
1310
import { WebpackConfigOptions } from '../build-options';
1411
import { getSourceMapDevTool, isPolyfillsEntry, normalizeExtraEntryPoints } from './utils';
1512

1613
const SubresourceIntegrityPlugin = require('webpack-subresource-integrity');
1714

1815

1916
export function getBrowserConfig(wco: WebpackConfigOptions): webpack.Configuration {
20-
const { root, buildOptions } = wco;
17+
const { buildOptions } = wco;
2118
const extraPlugins = [];
2219

2320
let isEval = false;
@@ -37,18 +34,6 @@ export function getBrowserConfig(wco: WebpackConfigOptions): webpack.Configurati
3734
isEval = true;
3835
}
3936

40-
if (buildOptions.index) {
41-
extraPlugins.push(new IndexHtmlWebpackPlugin({
42-
input: path.resolve(root, buildOptions.index),
43-
output: path.basename(buildOptions.index),
44-
baseHref: buildOptions.baseHref,
45-
entrypoints: generateEntryPoints(buildOptions),
46-
deployUrl: buildOptions.deployUrl,
47-
sri: buildOptions.subresourceIntegrity,
48-
noModuleEntrypoints: ['polyfills-es5'],
49-
}));
50-
}
51-
5237
if (buildOptions.subresourceIntegrity) {
5338
extraPlugins.push(new SubresourceIntegrityPlugin({
5439
hashFuncNames: ['sha384'],

packages/angular_devkit/build_angular/src/angular-cli-files/plugins/index-html-webpack-plugin.ts

+11-14
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
*/
88
import * as path from 'path';
99
import { Compiler, compilation } from 'webpack';
10+
import { RawSource } from 'webpack-sources';
1011
import { FileInfo, augmentIndexHtml } from '../utilities/index-file/augment-index-html';
12+
import { IndexHtmlTransform } from '../utilities/index-file/write-index-html';
13+
import { stripBom } from '../utilities/strip-bom';
1114

1215
export interface IndexHtmlWebpackPluginOptions {
1316
input: string;
@@ -17,6 +20,7 @@ export interface IndexHtmlWebpackPluginOptions {
1720
deployUrl?: string;
1821
sri: boolean;
1922
noModuleEntrypoints: string[];
23+
postTransform?: IndexHtmlTransform;
2024
}
2125

2226
function readFile(filename: string, compilation: compilation.Compilation): Promise<string> {
@@ -28,18 +32,7 @@ function readFile(filename: string, compilation: compilation.Compilation): Promi
2832
return;
2933
}
3034

31-
let content;
32-
if (data.length >= 3 && data[0] === 0xEF && data[1] === 0xBB && data[2] === 0xBF) {
33-
// Strip UTF-8 BOM
34-
content = data.toString('utf8', 3);
35-
} else if (data.length >= 2 && data[0] === 0xFF && data[1] === 0xFE) {
36-
// Strip UTF-16 LE BOM
37-
content = data.toString('utf16le', 2);
38-
} else {
39-
content = data.toString();
40-
}
41-
42-
resolve(content);
35+
resolve(stripBom(data.toString()));
4336
});
4437
});
4538
}
@@ -86,7 +79,7 @@ export class IndexHtmlWebpackPlugin {
8679
}
8780

8881
const loadOutputFile = (name: string) => compilation.assets[name].source();
89-
const indexSource = await augmentIndexHtml({
82+
let indexSource = await augmentIndexHtml({
9083
input: this._options.input,
9184
inputContent,
9285
baseHref: this._options.baseHref,
@@ -98,8 +91,12 @@ export class IndexHtmlWebpackPlugin {
9891
entrypoints: this._options.entrypoints,
9992
});
10093

94+
if (this._options.postTransform) {
95+
indexSource = await this._options.postTransform(indexSource);
96+
}
97+
10198
// Add to compilation assets
102-
compilation.assets[this._options.output] = indexSource;
99+
compilation.assets[this._options.output] = new RawSource(indexSource);
103100
});
104101
}
105102
}

packages/angular_devkit/build_angular/src/angular-cli-files/utilities/index-file/augment-index-html.ts

+3-7
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@
77
*/
88

99
import { createHash } from 'crypto';
10-
import {
11-
RawSource,
12-
ReplaceSource,
13-
Source,
14-
} from 'webpack-sources';
10+
import { RawSource, ReplaceSource } from 'webpack-sources';
1511

1612
const parse5 = require('parse5');
1713

@@ -57,7 +53,7 @@ export interface FileInfo {
5753
* after processing several configurations in order to build different sets of
5854
* bundles for differential serving.
5955
*/
60-
export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise<Source> {
56+
export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise<string> {
6157
const {
6258
loadOutputFile,
6359
files,
@@ -236,7 +232,7 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
236232
parse5.serialize(styleElements, { treeAdapter }),
237233
);
238234

239-
return indexSource;
235+
return indexSource.source();
240236
}
241237

242238
function _generateSriAttributes(content: string) {

packages/angular_devkit/build_angular/src/angular-cli-files/utilities/index-file/augment-index-html_spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe('augment-index-html', () => {
3434
],
3535
});
3636

37-
const html = (await source).source();
37+
const html = await source;
3838
expect(html).toEqual(oneLineHtml`
3939
<html>
4040
<head><base href="/">
@@ -74,7 +74,7 @@ describe('augment-index-html', () => {
7474
noModuleFiles: es5JsFiles,
7575
});
7676

77-
const html = (await source).source();
77+
const html = await source;
7878
expect(html).toEqual(oneLineHtml`
7979
<html>
8080
<head>
@@ -116,7 +116,7 @@ describe('augment-index-html', () => {
116116
noModuleFiles: es5JsFiles,
117117
});
118118

119-
const html = (await source).source();
119+
const html = await source;
120120
expect(html).toEqual(oneLineHtml`
121121
<html>
122122
<head>

packages/angular_devkit/build_angular/src/angular-cli-files/utilities/index-file/write-index-html.ts

+24-11
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,45 @@
88

99
import { EmittedFiles } from '@angular-devkit/build-webpack';
1010
import { Path, basename, getSystemPath, join, virtualFs } from '@angular-devkit/core';
11-
import { Observable } from 'rxjs';
11+
import { Observable, of } from 'rxjs';
1212
import { map, switchMap } from 'rxjs/operators';
1313
import { ExtraEntryPoint } from '../../../browser/schema';
1414
import { generateEntryPoints } from '../package-chunk-sort';
1515
import { stripBom } from '../strip-bom';
1616
import { FileInfo, augmentIndexHtml } from './augment-index-html';
1717

18+
type ExtensionFilter = '.js' | '.css';
19+
1820
export interface WriteIndexHtmlOptions {
1921
host: virtualFs.Host;
2022
outputPath: Path;
2123
indexPath: Path;
22-
ES5BuildFiles: EmittedFiles[];
23-
ES2015BuildFiles: EmittedFiles[];
24+
files?: EmittedFiles[];
25+
noModuleFiles?: EmittedFiles[];
26+
moduleFiles?: EmittedFiles[];
2427
baseHref?: string;
2528
deployUrl?: string;
2629
sri?: boolean;
2730
scripts?: ExtraEntryPoint[];
2831
styles?: ExtraEntryPoint[];
32+
postTransform?: IndexHtmlTransform;
2933
}
3034

35+
export type IndexHtmlTransform = (content: string) => Promise<string>;
36+
3137
export function writeIndexHtml({
3238
host,
3339
outputPath,
3440
indexPath,
35-
ES5BuildFiles,
36-
ES2015BuildFiles,
41+
files = [],
42+
noModuleFiles = [],
43+
moduleFiles = [],
3744
baseHref,
3845
deployUrl,
3946
sri = false,
4047
scripts = [],
4148
styles = [],
49+
postTransform,
4250
}: WriteIndexHtmlOptions): Observable<void> {
4351

4452
return host.read(indexPath)
@@ -51,9 +59,9 @@ export function writeIndexHtml({
5159
deployUrl,
5260
sri,
5361
entrypoints: generateEntryPoints({ scripts, styles }),
54-
files: filterAndMapBuildFiles(ES5BuildFiles, '.css'),
55-
noModuleFiles: filterAndMapBuildFiles(ES5BuildFiles, '.js'),
56-
moduleFiles: filterAndMapBuildFiles(ES2015BuildFiles, '.js'),
62+
files: filterAndMapBuildFiles(files, ['.js', '.css']),
63+
noModuleFiles: filterAndMapBuildFiles(noModuleFiles, '.js'),
64+
moduleFiles: filterAndMapBuildFiles(moduleFiles, '.js'),
5765
loadOutputFile: async filePath => {
5866
return host.read(join(outputPath, filePath))
5967
.pipe(
@@ -63,18 +71,23 @@ export function writeIndexHtml({
6371
},
6472
}),
6573
),
66-
map(content => virtualFs.stringToFileBuffer(content.source())),
74+
switchMap(content => postTransform ? postTransform(content) : of(content)),
75+
map(content => virtualFs.stringToFileBuffer(content)),
6776
switchMap(content => host.write(join(outputPath, basename(indexPath)), content)),
6877
);
6978
}
7079

7180
function filterAndMapBuildFiles(
7281
files: EmittedFiles[],
73-
extensionFilter: '.js' | '.css',
82+
extensionFilter: ExtensionFilter | ExtensionFilter[],
7483
): FileInfo[] {
7584
const filteredFiles: FileInfo[] = [];
85+
const validExtensions: string[] = Array.isArray(extensionFilter)
86+
? extensionFilter
87+
: [extensionFilter];
88+
7689
for (const { file, name, extension, initial } of files) {
77-
if (name && initial && extension === extensionFilter) {
90+
if (name && initial && validExtensions.includes(extension)) {
7891
filteredFiles.push({ file, extension, name });
7992
}
8093
}

packages/angular_devkit/build_angular/src/browser/index.ts

+31-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import {
1010
BuilderOutput,
1111
createBuilder,
1212
} from '@angular-devkit/architect';
13-
import { BuildResult, WebpackLoggingCallback, runWebpack } from '@angular-devkit/build-webpack';
13+
import {
14+
BuildResult,
15+
EmittedFiles,
16+
WebpackLoggingCallback,
17+
runWebpack,
18+
} from '@angular-devkit/build-webpack';
1419
import {
1520
experimental,
1621
getSystemPath,
@@ -40,7 +45,10 @@ import {
4045
getStylesConfig,
4146
getWorkerConfig,
4247
} from '../angular-cli-files/models/webpack-configs';
43-
import { writeIndexHtml } from '../angular-cli-files/utilities/index-file/write-index-html';
48+
import {
49+
IndexHtmlTransform,
50+
writeIndexHtml,
51+
} from '../angular-cli-files/utilities/index-file/write-index-html';
4452
import { readTsconfig } from '../angular-cli-files/utilities/read-tsconfig';
4553
import { augmentAppWithServiceWorker } from '../angular-cli-files/utilities/service-worker';
4654
import {
@@ -165,6 +173,7 @@ export function buildWebpackBrowser(
165173
transforms: {
166174
webpackConfiguration?: ExecutionTransformer<webpack.Configuration>,
167175
logging?: WebpackLoggingCallback,
176+
indexHtml?: IndexHtmlTransform,
168177
} = {},
169178
) {
170179
const host = new NodeJsSyncHost();
@@ -217,21 +226,36 @@ export function buildWebpackBrowser(
217226
bufferCount(configs.length),
218227
switchMap(buildEvents => {
219228
const success = buildEvents.every(r => r.success);
220-
if (success && buildEvents.length === 2 && options.index) {
221-
const { emittedFiles: ES5BuildFiles = [] } = buildEvents[0];
222-
const { emittedFiles: ES2015BuildFiles = [] } = buildEvents[1];
229+
if (success && options.index) {
230+
let noModuleFiles: EmittedFiles[] | undefined;
231+
let moduleFiles: EmittedFiles[] | undefined;
232+
let files: EmittedFiles[] | undefined;
233+
234+
const [ES5Result, ES2015Result] = buildEvents;
235+
236+
if (buildEvents.length === 2) {
237+
noModuleFiles = ES5Result.emittedFiles;
238+
moduleFiles = ES2015Result.emittedFiles || [];
239+
files = moduleFiles.filter(x => x.extension === '.css');
240+
} else {
241+
const { emittedFiles = [] } = ES5Result;
242+
files = emittedFiles.filter(x => x.name !== 'polyfills-es5');
243+
noModuleFiles = emittedFiles.filter(x => x.name === 'polyfills-es5');
244+
}
223245

224246
return writeIndexHtml({
225247
host,
226248
outputPath: join(root, options.outputPath),
227249
indexPath: join(root, options.index),
228-
ES5BuildFiles,
229-
ES2015BuildFiles,
250+
files,
251+
noModuleFiles,
252+
moduleFiles,
230253
baseHref: options.baseHref,
231254
deployUrl: options.deployUrl,
232255
sri: options.subresourceIntegrity,
233256
scripts: options.scripts,
234257
styles: options.styles,
258+
postTransform: transforms.indexHtml,
235259
})
236260
.pipe(
237261
map(() => ({ success: true })),

packages/angular_devkit/build_angular/src/dev-server/index.ts

+24-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
WebpackLoggingCallback,
1717
runWebpackDevServer,
1818
} from '@angular-devkit/build-webpack';
19-
import { experimental, json, logging, tags } from '@angular-devkit/core';
19+
import { json, logging, tags } from '@angular-devkit/core';
2020
import { NodeJsSyncHost } from '@angular-devkit/core/node';
2121
import { existsSync, readFileSync } from 'fs';
2222
import * as path from 'path';
@@ -25,7 +25,10 @@ import { map, switchMap } from 'rxjs/operators';
2525
import * as url from 'url';
2626
import * as webpack from 'webpack';
2727
import * as WebpackDevServer from 'webpack-dev-server';
28+
import { IndexHtmlWebpackPlugin } from '../angular-cli-files/plugins/index-html-webpack-plugin';
2829
import { checkPort } from '../angular-cli-files/utilities/check-port';
30+
import { IndexHtmlTransform } from '../angular-cli-files/utilities/index-file/write-index-html';
31+
import { generateEntryPoints } from '../angular-cli-files/utilities/package-chunk-sort';
2932
import {
3033
buildBrowserWebpackConfigFromContext,
3134
createBrowserLoggingCallback,
@@ -73,6 +76,7 @@ export function serveWebpackBrowser(
7376
transforms: {
7477
webpackConfiguration?: ExecutionTransformer<webpack.Configuration>,
7578
logging?: WebpackLoggingCallback,
79+
indexHtml?: IndexHtmlTransform,
7680
} = {},
7781
): Observable<DevServerBuilderOutput> {
7882
// Check Angular version.
@@ -158,17 +162,34 @@ export function serveWebpackBrowser(
158162
context.logger.warn('Live reload is disabled. HMR option ignored.');
159163
}
160164

165+
webpackConfig.plugins = [...(webpackConfig.plugins || [])];
166+
161167
if (!options.watch) {
162168
// There's no option to turn off file watching in webpack-dev-server, but
163169
// we can override the file watcher instead.
164-
webpackConfig.plugins = [...(webpackConfig.plugins || []), {
170+
webpackConfig.plugins.push({
165171
// tslint:disable-next-line:no-any
166172
apply: (compiler: any) => {
167173
compiler.hooks.afterEnvironment.tap('angular-cli', () => {
168174
compiler.watchFileSystem = { watch: () => { } };
169175
});
170176
},
171-
}];
177+
});
178+
}
179+
180+
if (browserOptions.index) {
181+
const { scripts = [], styles = [], index, baseHref } = browserOptions;
182+
183+
webpackConfig.plugins.push(new IndexHtmlWebpackPlugin({
184+
input: path.resolve(root, index),
185+
output: path.basename(index),
186+
baseHref,
187+
entrypoints: generateEntryPoints({ scripts, styles }),
188+
deployUrl: browserOptions.deployUrl,
189+
sri: browserOptions.subresourceIntegrity,
190+
noModuleEntrypoints: ['polyfills-es5'],
191+
postTransform: transforms.indexHtml,
192+
}));
172193
}
173194

174195
const normalizedOptimization = normalizeOptimization(browserOptions.optimization);

packages/angular_devkit/build_angular/src/server/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ async function buildServerWebpackConfig(
9292
const { config } = await generateBrowserWebpackConfigFromContext(
9393
{
9494
...options,
95-
index: '',
9695
buildOptimizer: false,
9796
aot: true,
9897
platform: 'server',

0 commit comments

Comments
 (0)