diff --git a/package.json b/package.json index 5d25879e3ec2..cc9d3ae2ae3d 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "@types/babel__core": "7.1.10", "@types/babel__template": "7.0.3", "@types/browserslist": "^4.4.0", + "@types/cacache": "^12.0.1", "@types/caniuse-lite": "^1.0.0", "@types/copy-webpack-plugin": "^6.0.0", "@types/cssnano": "^4.0.0", diff --git a/packages/angular/cli/lib/config/schema.json b/packages/angular/cli/lib/config/schema.json index 6cf85123a7c6..aff93af8a334 100644 --- a/packages/angular/cli/lib/config/schema.json +++ b/packages/angular/cli/lib/config/schema.json @@ -715,6 +715,26 @@ "type": "boolean", "description": "Enables optimization of the styles output.", "default": true + }, + "fonts": { + "description": "Enables optimization for fonts. This requires internet access.", + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "inline": { + "type": "boolean", + "description": "Reduce render blocking requests by inlining external fonts in the application's HTML index file. This requires internet access.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] } }, "additionalProperties": false diff --git a/packages/angular_devkit/build_angular/BUILD.bazel b/packages/angular_devkit/build_angular/BUILD.bazel index c0ca0c55895f..6d45ae2d4f52 100644 --- a/packages/angular_devkit/build_angular/BUILD.bazel +++ b/packages/angular_devkit/build_angular/BUILD.bazel @@ -114,6 +114,7 @@ ts_library( "@npm//@types/babel__core", "@npm//@types/babel__template", "@npm//@types/browserslist", + "@npm//@types/cacache", "@npm//@types/caniuse-lite", "@npm//@types/copy-webpack-plugin", "@npm//@types/cssnano", diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts index b593f79c52d4..fd0c50d2ca3c 100644 --- a/packages/angular_devkit/build_angular/src/browser/index.ts +++ b/packages/angular_devkit/build_angular/src/browser/index.ts @@ -34,6 +34,7 @@ import { copyAssets } from '../utils/copy-assets'; import { cachingDisabled } from '../utils/environment-options'; import { i18nInlineEmittedFiles } from '../utils/i18n-inlining'; import { I18nOptions } from '../utils/i18n-options'; +import { getHtmlTransforms } from '../utils/index-file/transforms'; import { IndexHtmlTransform, writeIndexHtml, @@ -280,6 +281,12 @@ export function buildWebpackBrowser( switchMap(({ config, projectRoot, projectSourceRoot, i18n, buildBrowserFeatures, isDifferentialLoadingNeeded, target }) => { const useBundleDownleveling = isDifferentialLoadingNeeded && !options.watch; const startTime = Date.now(); + const normalizedOptimization = normalizeOptimization(options.optimization); + const indexTransforms = getHtmlTransforms( + normalizedOptimization, + buildBrowserFeatures, + transforms.indexHtml, + ); return runWebpack(config, context, { webpackFactory: require('webpack') as typeof webpack, @@ -356,7 +363,7 @@ export function buildWebpackBrowser( // Common options for all bundle process actions const sourceMapOptions = normalizeSourceMaps(options.sourceMap || false); const actionOptions: Partial = { - optimize: normalizeOptimization(options.optimization).scripts, + optimize: normalizedOptimization.scripts, sourceMaps: sourceMapOptions.scripts, hiddenSourceMaps: sourceMapOptions.hidden, vendorSourceMaps: sourceMapOptions.vendor, @@ -738,7 +745,7 @@ export function buildWebpackBrowser( sri: options.subresourceIntegrity, scripts: options.scripts, styles: options.styles, - postTransform: transforms.indexHtml, + postTransforms: indexTransforms, crossOrigin: options.crossOrigin, // i18nLocale is used when Ivy is disabled lang: locale || options.i18nLocale, diff --git a/packages/angular_devkit/build_angular/src/browser/schema.json b/packages/angular_devkit/build_angular/src/browser/schema.json index bc5aa4002ba8..628b5dcd8dd8 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.json +++ b/packages/angular_devkit/build_angular/src/browser/schema.json @@ -73,6 +73,26 @@ "type": "boolean", "description": "Enables optimization of the styles output.", "default": true + }, + "fonts": { + "description": "Enables optimization for fonts. This requires internet access.", + "default": true, + "oneOf": [ + { + "type": "object", + "properties": { + "inline": { + "type": "boolean", + "description": "Reduce render blocking requests by inlining external fonts in the application's HTML index file. This requires internet access.", + "default": true + } + }, + "additionalProperties": false + }, + { + "type": "boolean" + } + ] } }, "additionalProperties": false diff --git a/packages/angular_devkit/build_angular/src/browser/specs/font-optimization_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/font-optimization_spec.ts new file mode 100644 index 000000000000..8075059db024 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/browser/specs/font-optimization_spec.ts @@ -0,0 +1,84 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Architect } from '@angular-devkit/architect'; +import { browserBuild, createArchitect, host } from '../../test-utils'; + +describe('Browser Builder font optimization', () => { + const target = { project: 'app', target: 'build' }; + const overrides = { + optimization: { + styles: false, + fonts: true, + }, + }; + + let architect: Architect; + + beforeEach(async () => { + await host.initialize().toPromise(); + architect = (await createArchitect(host.root())).architect; + + host.replaceInFile( + '/src/index.html', + '', + ``, + ); + }); + + afterEach(async () => host.restore().toPromise()); + + it('works', async () => { + const { files } = await browserBuild(architect, host, target, overrides); + const html = await files['index.html']; + expect(html).not.toContain('href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"'); + expect(html).toContain(`font-family: 'Roboto'`); + }); + + it('should not add woff when IE support is not needed', async () => { + const { files } = await browserBuild(architect, host, target, overrides); + const html = await files['index.html']; + expect(html).toContain(`format('woff2');`); + expect(html).not.toContain(`format('woff');`); + }); + + it('should add woff when IE support is needed', async () => { + host.writeMultipleFiles({ + '.browserslistrc': 'IE 11', + }); + + const { files } = await browserBuild(architect, host, target, overrides); + const html = await files['index.html']; + expect(html).toContain(`format('woff2');`); + expect(html).toContain(`format('woff');`); + }); + + it('should remove comments and line breaks when styles optimization is true', async () => { + const { files } = await browserBuild(architect, host, target, { + optimization: { + styles: true, + fonts: true, + }, + }); + const html = await files['index.html']; + expect(html).not.toContain('/*'); + expect(html).toContain(';font-style:normal;'); + }); + + it('should not remove comments and line breaks when styles optimization is false', async () => { + const { files } = await browserBuild(architect, host, target, { + optimization: { + styles: false, + fonts: true, + }, + }); + + const html = await files['index.html']; + expect(html).toContain('/*'); + expect(html).toContain(' font-style: normal;\n'); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/dev-server/index.ts b/packages/angular_devkit/build_angular/src/dev-server/index.ts index bd91913e9c02..56456e1a6f20 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/index.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/index.ts @@ -29,6 +29,7 @@ import { BuildBrowserFeatures, normalizeOptimization } from '../utils'; import { findCachePath } from '../utils/cache-path'; import { checkPort } from '../utils/check-port'; import { I18nOptions } from '../utils/i18n-options'; +import { getHtmlTransforms } from '../utils/index-file/transforms'; import { IndexHtmlTransform } from '../utils/index-file/write-index-html'; import { generateEntryPoints } from '../utils/package-chunk-sort'; import { createI18nPlugins } from '../utils/process-bundle'; @@ -40,7 +41,6 @@ import { normalizeExtraEntryPoints } from '../webpack/configs'; import { IndexHtmlWebpackPlugin } from '../webpack/plugins/index-html-webpack-plugin'; import { createWebpackLoggingCallback } from '../webpack/utils/stats'; import { Schema } from './schema'; -const open = require('open'); export type DevServerBuilderOptions = Schema & json.JsonObject; @@ -189,6 +189,8 @@ export function serveWebpackBrowser( }); } + const normalizedOptimization = normalizeOptimization(browserOptions.optimization); + if (browserOptions.index) { const { scripts = [], styles = [], baseHref, tsConfig } = browserOptions; const { options: compilerOptions } = readTsconfig(tsConfig, context.workspaceRoot); @@ -210,14 +212,17 @@ export function serveWebpackBrowser( deployUrl: browserOptions.deployUrl, sri: browserOptions.subresourceIntegrity, noModuleEntrypoints: ['polyfills-es5'], - postTransform: transforms.indexHtml, + postTransforms: getHtmlTransforms( + normalizedOptimization, + buildBrowserFeatures, + transforms.indexHtml, + ), crossOrigin: browserOptions.crossOrigin, lang: browserOptions.i18nLocale, }), ); } - const normalizedOptimization = normalizeOptimization(browserOptions.optimization); if (normalizedOptimization.scripts || normalizedOptimization.styles) { context.logger.error(tags.stripIndents` **************************************************************************************** @@ -257,6 +262,7 @@ export function serveWebpackBrowser( `); if (options.open) { + const open = require('open'); open(serverAddress); } } diff --git a/packages/angular_devkit/build_angular/src/utils/action-cache.ts b/packages/angular_devkit/build_angular/src/utils/action-cache.ts index eb99cd4579a4..9a2fd8f0a407 100644 --- a/packages/angular_devkit/build_angular/src/utils/action-cache.ts +++ b/packages/angular_devkit/build_angular/src/utils/action-cache.ts @@ -5,13 +5,13 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import * as cacache from 'cacache'; import { createHash } from 'crypto'; import * as fs from 'fs'; import { copyFile } from './copy-file'; import { allowMangle } from './environment-options'; import { CacheKey, ProcessBundleOptions, ProcessBundleResult } from './process-bundle'; -const cacache = require('cacache'); const packageVersion = require('../../package.json').version; export interface CacheEntry { @@ -98,7 +98,8 @@ export class BundleActionCache { } cacheEntries.push({ path: entry.path, - size: entry.size, + // tslint:disable-next-line: no-any + size: (entry as any).size, integrity: entry.metadata && entry.metadata.integrity, }); } else { diff --git a/packages/angular_devkit/build_angular/src/utils/index-file/inline-fonts.ts b/packages/angular_devkit/build_angular/src/utils/index-file/inline-fonts.ts new file mode 100644 index 000000000000..0345a5cddc31 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/index-file/inline-fonts.ts @@ -0,0 +1,173 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as cacache from 'cacache'; +import { readFile as readFileAsync } from 'fs'; +import * as https from 'https'; +import { URL } from 'url'; +import { promisify } from 'util'; +import { findCachePath } from '../cache-path'; +import { cachingDisabled } from '../environment-options'; +import { htmlRewritingStream } from './html-rewriting-stream'; + +const cacheFontsPath: string | undefined = cachingDisabled ? undefined : findCachePath('angular-build-fonts'); +const packageVersion = require('../../../package.json').version; +const readFile = promisify(readFileAsync); + +const enum UserAgent { + Chrome = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36', + IE = 'Mozilla/5.0 (Windows NT 10.0; Trident/7.0; rv:11. 0) like Gecko', +} + +const SUPPORTED_PROVIDERS = [ + 'fonts.googleapis.com', +]; + +export interface InlineFontsOptions { + minifyInlinedCSS: boolean; + WOFFSupportNeeded: boolean; +} + +export class InlineFontsProcessor { + + constructor(private options: InlineFontsOptions) { } + + async process(content: string): Promise { + const hrefList: string[] = []; + + // Collector link tags with href + const { rewriter: collectorStream } = await htmlRewritingStream(content); + + collectorStream.on('startTag', tag => { + const { tagName, attrs } = tag; + + if (tagName !== 'link') { + return; + } + + // name === 'rel' && value === 'stylesheet') + && attrs.find(({ name }) => name === 'href')?.value; + + if (href) { + hrefList.push(href); + } + }); + + await new Promise(resolve => collectorStream.on('finish', resolve)); + + // Download stylesheets + const hrefsContent = await this.processHrefs(hrefList); + if (hrefsContent.size === 0) { + return content; + } + + // Replace link with style tag. + const { rewriter, transformedContent } = await htmlRewritingStream(content); + rewriter.on('startTag', tag => { + const { tagName, attrs } = tag; + + if (tagName !== 'link') { + rewriter.emitStartTag(tag); + + return; + } + + const hrefAttr = attrs.some(({ name, value }) => name === 'rel' && value === 'stylesheet') + && attrs.find(({ name, value }) => name === 'href' && hrefsContent.has(value)); + if (hrefAttr) { + const href = hrefAttr.value; + const cssContent = hrefsContent.get(href); + rewriter.emitRaw(``); + } else { + rewriter.emitStartTag(tag); + } + }); + + return transformedContent; + } + + private async getResponse(url: URL, userAgent: UserAgent): Promise { + const key = `${packageVersion}|${url}|${userAgent}`; + + if (cacheFontsPath) { + const entry = await cacache.get.info(cacheFontsPath, key); + if (entry) { + return readFile(entry.path, 'utf8'); + } + } + + const data = await new Promise((resolve, reject) => { + let rawResponse = ''; + https.get( + url, + { + headers: { + 'user-agent': userAgent, + }, + }, + res => { + res + .on('data', chunk => rawResponse += chunk) + .on('end', () => resolve(rawResponse)); + }, + ) + .on('error', e => reject(e)); + }); + + if (cacheFontsPath) { + await cacache.put(cacheFontsPath, key, data); + } + + return data; + } + + private async processHrefs(hrefList: string[]): Promise> { + const hrefsContent = new Map(); + + for (const hrefPath of hrefList) { + // Need to convert '//' to 'https://' because the URL parser will fail with '//'. + const normalizedHref = hrefPath.startsWith('//') ? `https:${hrefPath}` : hrefPath; + if (!normalizedHref.startsWith('http')) { + // Non valid URL. + // Example: relative path styles.css. + continue; + } + + const url = new URL(normalizedHref); + // Force HTTPS protocol + url.protocol = 'https:'; + + if (!SUPPORTED_PROVIDERS.includes(url.hostname)) { + // Provider not supported. + continue; + } + + // The order IE -> Chrome is important as otherwise Chrome will load woff1. + let cssContent = ''; + if (this.options.WOFFSupportNeeded) { + cssContent += await this.getResponse(url, UserAgent.IE); + } + cssContent += await this.getResponse(url, UserAgent.Chrome); + + if (this.options.minifyInlinedCSS) { + cssContent = cssContent + // New lines. + .replace(/\n/g, '') + // Comments and new lines. + .replace(/\/\*\s.+\s\*\//g, '') + // Safe spaces. + .replace(/\s?[\{\:\;]\s+/g, s => s.trim()); + } + + hrefsContent.set(hrefPath, cssContent); + } + + return hrefsContent; + } +} diff --git a/packages/angular_devkit/build_angular/src/utils/index-file/inline-fonts_spec.ts b/packages/angular_devkit/build_angular/src/utils/index-file/inline-fonts_spec.ts new file mode 100644 index 000000000000..dbf04237ed35 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/index-file/inline-fonts_spec.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { InlineFontsProcessor } from './inline-fonts'; + +describe('InlineFontsProcessor', () => { + const content = ` + + + + + + `; + + it('should inline supported fonts and icons in HTML', async () => { + const inlineFontsProcessor = new InlineFontsProcessor({ + minifyInlinedCSS: false, + WOFFSupportNeeded: false, + }); + + const html = await inlineFontsProcessor.process(` + + + + + + + + `); + + expect(html).not.toContain('href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"'); + expect(html).not.toContain('href="https://fonts.googleapis.com/icon?family=Material+Icons"'); + expect(html).toContain('href="theme.css"'); + expect(html).toContain(`font-family: 'Roboto'`); + expect(html).toContain(`font-family: 'Material Icons'`); + }); + + it('works with http protocol', async () => { + const inlineFontsProcessor = new InlineFontsProcessor({ + WOFFSupportNeeded: false, + minifyInlinedCSS: false, + }); + + const html = await inlineFontsProcessor + .process(content.replace('https://', 'http://')); + expect(html).toContain(`format('woff2');`); + }); + + it('works with // protocol', async () => { + const inlineFontsProcessor = new InlineFontsProcessor({ + WOFFSupportNeeded: false, + minifyInlinedCSS: false, + }); + + const html = await inlineFontsProcessor + .process(content.replace('https://', '//')); + expect(html).toContain(`format('woff2');`); + }); + + it('should include WOFF1 definitions when `WOFF1SupportNeeded` is true', async () => { + const inlineFontsProcessor = new InlineFontsProcessor({ + WOFFSupportNeeded: true, + minifyInlinedCSS: false, + }); + + const html = await inlineFontsProcessor.process(content); + expect(html).toContain(`format('woff2');`); + expect(html).toContain(`format('woff');`); + }); + + it('should remove comments and line breaks when `minifyInlinedCSS` is true', async () => { + const inlineFontsProcessor = new InlineFontsProcessor({ + WOFFSupportNeeded: false, + minifyInlinedCSS: true, + }); + + const html = await inlineFontsProcessor.process(content); + expect(html).not.toContain('/*'); + expect(html).toContain(';font-style:normal;'); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/utils/index-file/transforms.ts b/packages/angular_devkit/build_angular/src/utils/index-file/transforms.ts new file mode 100644 index 000000000000..27c7e4f431af --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/index-file/transforms.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { BuildBrowserFeatures } from '../build-browser-features'; +import { NormalizeOptimizationOptions } from '../normalize-optimization'; +import { InlineFontsProcessor } from './inline-fonts'; +import { IndexHtmlTransform } from './write-index-html'; + +export function getHtmlTransforms( + optimization: NormalizeOptimizationOptions, + buildBrowserFeatures: BuildBrowserFeatures, + extraHtmlTransform?: IndexHtmlTransform, +): IndexHtmlTransform[] { + const indexTransforms: IndexHtmlTransform[] = []; + const { fonts, styles } = optimization; + + // Inline fonts + if (fonts.inline) { + const inlineFontsProcessor = new InlineFontsProcessor({ + minifyInlinedCSS: styles, + WOFFSupportNeeded: !buildBrowserFeatures.isFeatureSupported('woff2'), + }); + + indexTransforms.push(content => inlineFontsProcessor.process(content)); + } + + if (extraHtmlTransform) { + indexTransforms.push(extraHtmlTransform); + } + + return indexTransforms; +} + diff --git a/packages/angular_devkit/build_angular/src/utils/index-file/write-index-html.ts b/packages/angular_devkit/build_angular/src/utils/index-file/write-index-html.ts index efd18d8a0712..7c9f943ffe08 100644 --- a/packages/angular_devkit/build_angular/src/utils/index-file/write-index-html.ts +++ b/packages/angular_devkit/build_angular/src/utils/index-file/write-index-html.ts @@ -28,7 +28,7 @@ export interface WriteIndexHtmlOptions { sri?: boolean; scripts?: ExtraEntryPoint[]; styles?: ExtraEntryPoint[]; - postTransform?: IndexHtmlTransform; + postTransforms: IndexHtmlTransform[]; crossOrigin?: CrossOriginValue; lang?: string; } @@ -47,7 +47,7 @@ export async function writeIndexHtml({ sri = false, scripts = [], styles = [], - postTransform, + postTransforms, crossOrigin, lang, }: WriteIndexHtmlOptions): Promise { @@ -69,8 +69,8 @@ export async function writeIndexHtml({ loadOutputFile: filePath => readFile(join(dirname(outputPath), filePath)), }); - if (postTransform) { - content = await postTransform(content); + for (const transform of postTransforms) { + content = await transform(content); } await host.write(normalize(outputPath), virtualFs.stringToFileBuffer(content)).toPromise(); diff --git a/packages/angular_devkit/build_angular/src/utils/normalize-optimization.ts b/packages/angular_devkit/build_angular/src/utils/normalize-optimization.ts index 1ebec9c2a76b..9bd503355286 100644 --- a/packages/angular_devkit/build_angular/src/utils/normalize-optimization.ts +++ b/packages/angular_devkit/build_angular/src/utils/normalize-optimization.ts @@ -6,13 +6,28 @@ * found in the LICENSE file at https://angular.io/license */ -import { OptimizationClass, OptimizationUnion } from '../browser/schema'; +import { FontsClass, OptimizationClass, OptimizationUnion } from '../browser/schema'; + +export type NormalizeOptimizationOptions = Required> & { + fonts: FontsClass, +}; + +export function normalizeOptimization(optimization: OptimizationUnion = false): NormalizeOptimizationOptions { + if (typeof optimization === 'object') { + return { + scripts: !!optimization.scripts, + styles: !!optimization.styles, + fonts: typeof optimization.fonts === 'object' ? optimization.fonts : { + inline: !!optimization.fonts, + }, + }; + } -export function normalizeOptimization( - optimization: OptimizationUnion = false, -): Required { return { - scripts: typeof optimization === 'object' ? !!optimization.scripts : optimization, - styles: typeof optimization === 'object' ? !!optimization.styles : optimization, + scripts: optimization, + styles: optimization, + fonts: { + inline: optimization, + }, }; } diff --git a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts index 2433a018a3a1..c806b9fa351b 100644 --- a/packages/angular_devkit/build_angular/src/utils/process-bundle.ts +++ b/packages/angular_devkit/build_angular/src/utils/process-bundle.ts @@ -16,6 +16,7 @@ import { types, } from '@babel/core'; import templateBuilder from '@babel/template'; +import * as cacache from 'cacache'; import { createHash } from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; @@ -35,7 +36,6 @@ import { isWebpackFiveOrHigher } from './webpack-version'; type LocalizeUtilities = typeof import('@angular/localize/src/tools/src/source_file_utils'); -const cacache = require('cacache'); const deserialize = ((v8 as unknown) as { deserialize(buffer: Buffer): unknown }).deserialize; // If code size is larger than 500KB, consider lower fidelity but faster sourcemap merge @@ -97,7 +97,7 @@ export function setup(data: number[] | { cachePath: string; i18n: I18nOptions }) async function cachePut(content: string, key: string | undefined, integrity?: string): Promise { if (cachePath && key) { - await cacache.put(cachePath, key || null, content, { + await cacache.put(cachePath, key, content, { metadata: { integrity }, }); } diff --git a/packages/angular_devkit/build_angular/src/webpack/plugins/index-html-webpack-plugin.ts b/packages/angular_devkit/build_angular/src/webpack/plugins/index-html-webpack-plugin.ts index 157dc2a54bf1..18a309831f4d 100644 --- a/packages/angular_devkit/build_angular/src/webpack/plugins/index-html-webpack-plugin.ts +++ b/packages/angular_devkit/build_angular/src/webpack/plugins/index-html-webpack-plugin.ts @@ -25,7 +25,7 @@ export interface IndexHtmlWebpackPluginOptions { sri: boolean; noModuleEntrypoints: string[]; moduleEntrypoints: string[]; - postTransform?: IndexHtmlTransform; + postTransforms: IndexHtmlTransform[]; crossOrigin?: CrossOriginValue; lang?: string; } @@ -55,6 +55,7 @@ export class IndexHtmlWebpackPlugin { noModuleEntrypoints: [], moduleEntrypoints: [], sri: false, + postTransforms: [], ...options, }; } @@ -93,6 +94,7 @@ export class IndexHtmlWebpackPlugin { return typeof data === 'string' ? data : data.toString(); }; + let indexSource = await augmentIndexHtml({ input: this._options.input, inputContent, @@ -108,8 +110,8 @@ export class IndexHtmlWebpackPlugin { lang: this._options.lang, }); - if (this._options.postTransform) { - indexSource = await this._options.postTransform(indexSource); + for (const transform of this._options.postTransforms) { + indexSource = await transform(indexSource); } // Add to compilation assets diff --git a/yarn.lock b/yarn.lock index 034a8084d98d..38e2f1f3f4ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1487,6 +1487,13 @@ resolved "https://registry.yarnpkg.com/@types/browserslist/-/browserslist-4.8.0.tgz#60489aefdf0fcb56c2d8eb65267ff08dad7a526d" integrity sha512-4PyO9OM08APvxxo1NmQyQKlJdowPCOQIy5D/NLO3aO0vGC57wsMptvGp3b8IbYnupFZr92l1dlVief1JvS6STQ== +"@types/cacache@^12.0.1": + version "12.0.1" + resolved "https://registry.yarnpkg.com/@types/cacache/-/cacache-12.0.1.tgz#1067140256647139fcbb06f6167dcb99e1e6d143" + integrity sha512-w7xjwtvB8X8wItoZo16oTDDJy1ub03CbKUM/A8SORv2igg/L6AxAtnNIlV/2STbuj5PFKwb3ySV7zR/g1nqldw== + dependencies: + "@types/node" "*" + "@types/cacheable-request@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976"