/** * @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 fs from 'fs'; import { join } from 'path'; import { NormalizedOptimizationOptions } from '../normalize-optimization'; import { stripBom } from '../strip-bom'; import { CrossOriginValue, FileInfo, augmentIndexHtml } from './augment-index-html'; import { InlineCriticalCssProcessor } from './inline-critical-css'; import { InlineFontsProcessor } from './inline-fonts'; type IndexHtmlGeneratorPlugin = (html: string, options: IndexHtmlGeneratorProcessOptions) => Promise<string | IndexHtmlTransformResult>; export interface IndexHtmlGeneratorProcessOptions { lang: string | undefined; baseHref: string | undefined; outputPath: string; files: FileInfo[]; noModuleFiles: FileInfo[]; moduleFiles: FileInfo[]; } export interface IndexHtmlGeneratorOptions { indexPath: string; deployUrl?: string; sri?: boolean; entrypoints: string[]; postTransform?: IndexHtmlTransform; crossOrigin?: CrossOriginValue; optimization?: NormalizedOptimizationOptions; WOFFSupportNeeded: boolean; } export type IndexHtmlTransform = (content: string) => Promise<string>; export interface IndexHtmlTransformResult { content: string; warnings: string[]; errors: string[]; } export class IndexHtmlGenerator { private readonly plugins: IndexHtmlGeneratorPlugin[]; constructor(readonly options: IndexHtmlGeneratorOptions) { const extraPlugins: IndexHtmlGeneratorPlugin[] = []; if (this.options.optimization?.fonts.inline) { extraPlugins.push(inlineFontsPlugin(this)); } if (this.options.optimization?.styles.inlineCritical) { extraPlugins.push(inlineCriticalCssPlugin(this)); } this.plugins = [ augmentIndexHtmlPlugin(this), ...extraPlugins, postTransformPlugin(this), ]; } async process(options: IndexHtmlGeneratorProcessOptions): Promise<IndexHtmlTransformResult> { let content = stripBom(await this.readIndex(this.options.indexPath)); const warnings: string[] = []; const errors: string[] = []; for (const plugin of this.plugins) { const result = await plugin(content, options); if (typeof result === 'string') { content = result; } else { content = result.content; if (result.warnings.length) { warnings.push(...result.warnings); } if (result.errors.length) { errors.push(...result.errors); } } } return { content, warnings, errors, }; } async readAsset(path: string): Promise<string> { return fs.promises.readFile(path, 'utf-8'); } protected async readIndex(path: string): Promise<string> { return fs.promises.readFile(path, 'utf-8'); } } function augmentIndexHtmlPlugin(generator: IndexHtmlGenerator): IndexHtmlGeneratorPlugin { const { deployUrl, crossOrigin, sri = false, entrypoints, } = generator.options; return async (html, options) => { const { lang, baseHref, outputPath = '', noModuleFiles, files, moduleFiles, } = options; return augmentIndexHtml({ html, baseHref, deployUrl, crossOrigin, sri, lang, entrypoints, loadOutputFile: filePath => generator.readAsset(join(outputPath, filePath)), noModuleFiles, moduleFiles, files, }); }; } function inlineFontsPlugin({ options }: IndexHtmlGenerator): IndexHtmlGeneratorPlugin { const inlineFontsProcessor = new InlineFontsProcessor({ minify: options.optimization?.styles.minify, WOFFSupportNeeded: options.WOFFSupportNeeded, }); return async html => inlineFontsProcessor.process(html); } function inlineCriticalCssPlugin(generator: IndexHtmlGenerator): IndexHtmlGeneratorPlugin { const inlineCriticalCssProcessor = new InlineCriticalCssProcessor({ minify: generator.options.optimization?.styles.minify, deployUrl: generator.options.deployUrl, readAsset: filePath => generator.readAsset(filePath), }); return async (html, options) => inlineCriticalCssProcessor.process(html, { outputPath: options.outputPath }); } function postTransformPlugin({ options }: IndexHtmlGenerator): IndexHtmlGeneratorPlugin { return async html => options.postTransform ? options.postTransform(html) : html; }