UNPKG

@nx/webpack

Version:

The Nx Plugin for Webpack contains executors and generators that support building applications using Webpack.

250 lines (249 loc) • 11.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WriteIndexHtmlPlugin = void 0; const webpack = require("webpack"); const crypto_1 = require("crypto"); const fs_1 = require("fs"); const interpolate_env_variables_to_index_1 = require("../utils/webpack/interpolate-env-variables-to-index"); const package_chunk_sort_1 = require("../utils/webpack/package-chunk-sort"); const path_1 = require("path"); const parse5 = require('parse5'); class WriteIndexHtmlPlugin { constructor(options) { this.options = options; } apply(compiler) { const { outputPath, indexPath, baseHref, deployUrl, sri = false, scripts = [], styles = [], crossOrigin, } = this.options; compiler.hooks.thisCompilation.tap('WriteIndexHtmlPlugin', (compilation) => { compilation.hooks.processAssets.tap({ name: 'WriteIndexHtmlPlugin', // After minification and sourcemaps are done stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE, }, () => { const moduleFiles = this.getEmittedFiles(compilation); const files = moduleFiles.filter((x) => x.extension === '.css'); let content = (0, fs_1.readFileSync)(indexPath).toString(); content = this.stripBom(content); compilation.assets[outputPath] = this.augmentIndexHtml({ input: outputPath, inputContent: (0, interpolate_env_variables_to_index_1.interpolateEnvironmentVariablesToIndex)(content, deployUrl), baseHref, deployUrl, crossOrigin, sri, entrypoints: (0, package_chunk_sort_1.generateEntryPoints)({ scripts, styles }), files: this.filterAndMapBuildFiles(files, ['.js', '.css']), moduleFiles: this.filterAndMapBuildFiles(moduleFiles, ['.js']), loadOutputFile: (filePath) => compilation.assets[filePath].source().toString(), }); }); }); } getEmittedFiles(compilation) { const files = []; // adds all chunks to the list of emitted files such as lazy loaded modules for (const chunk of compilation.chunks) { for (const file of chunk.files) { files.push({ // The id is guaranteed to exist at this point in the compilation process id: chunk.id.toString(), name: chunk.name, file, extension: (0, path_1.extname)(file), initial: chunk.isOnlyInitial(), }); } } // other all files for (const file of Object.keys(compilation.assets)) { files.push({ file, extension: (0, path_1.extname)(file), initial: false, asset: true, }); } // dedupe return files.filter(({ file, name }, index) => files.findIndex((f) => f.file === file && (!name || name === f.name)) === index); } stripBom(data) { return data.replace(/^\uFEFF/, ''); } augmentIndexHtml(params) { const { loadOutputFile, files, moduleFiles = [], entrypoints } = params; let { crossOrigin = 'none' } = params; if (params.sri && crossOrigin === 'none') { crossOrigin = 'anonymous'; } const stylesheets = new Set(); const scripts = new Set(); // Sort files in the order we want to insert them by entrypoint and dedupes duplicates const mergedFiles = [...moduleFiles, ...files]; for (const entrypoint of entrypoints) { for (const { extension, file, name } of mergedFiles) { if (name !== entrypoint) { continue; } switch (extension) { case '.js': scripts.add(file); break; case '.css': stylesheets.add(file); break; } } } // Find the head and body elements const treeAdapter = parse5.treeAdapters.default; const document = parse5.parse(params.inputContent, { treeAdapter, locationInfo: true, }); let headElement; let bodyElement; for (const docChild of document.childNodes) { if (docChild.tagName === 'html') { for (const htmlChild of docChild.childNodes) { if (htmlChild.tagName === 'head') { headElement = htmlChild; } else if (htmlChild.tagName === 'body') { bodyElement = htmlChild; } } } } if (!headElement || !bodyElement) { throw new Error('Missing head and/or body elements'); } // Determine script insertion point let scriptInsertionPoint; if (bodyElement.__location && bodyElement.__location.endTag) { scriptInsertionPoint = bodyElement.__location.endTag.startOffset; } else { // Less accurate fallback // parse5 4.x does not provide locations if malformed html is present scriptInsertionPoint = params.inputContent.indexOf('</body>'); } let styleInsertionPoint; if (headElement.__location && headElement.__location.endTag) { styleInsertionPoint = headElement.__location.endTag.startOffset; } else { // Less accurate fallback // parse5 4.x does not provide locations if malformed html is present styleInsertionPoint = params.inputContent.indexOf('</head>'); } // Inject into the html const indexSource = new webpack.sources.ReplaceSource(new webpack.sources.RawSource(params.inputContent, false), params.input); let scriptElements = ''; for (const script of scripts) { const attrs = [ { name: 'src', value: (params.deployUrl || '') + script }, ]; if (crossOrigin !== 'none') { attrs.push({ name: 'crossorigin', value: crossOrigin }); } // We want to include nomodule or module when a file is not common amongs all // such as runtime.js const scriptPredictor = ({ file, }) => file === script; if (!files.some(scriptPredictor)) { // in some cases for differential loading file with the same name is avialable in both // nomodule and module such as scripts.js // we shall not add these attributes if that's the case const isModuleType = moduleFiles.some(scriptPredictor); if (isModuleType) { attrs.push({ name: 'type', value: 'module' }); } else { attrs.push({ name: 'defer', value: null }); } } else { attrs.push({ name: 'type', value: 'module' }); } if (params.sri) { const content = loadOutputFile(script); attrs.push(...this.generateSriAttributes(content)); } const attributes = attrs .map((attr) => attr.value === null ? attr.name : `${attr.name}="${attr.value}"`) .join(' '); scriptElements += `<script ${attributes}></script>`; } indexSource.insert(scriptInsertionPoint, scriptElements); // Adjust base href if specified if (typeof params.baseHref == 'string') { let baseElement; for (const headChild of headElement.childNodes) { if (headChild.tagName === 'base') { baseElement = headChild; } } const baseFragment = treeAdapter.createDocumentFragment(); if (!baseElement) { baseElement = treeAdapter.createElement('base', undefined, [ { name: 'href', value: params.baseHref }, ]); treeAdapter.appendChild(baseFragment, baseElement); indexSource.insert(headElement.__location.startTag.endOffset, parse5.serialize(baseFragment, { treeAdapter })); } else { let hrefAttribute; for (const attribute of baseElement.attrs) { if (attribute.name === 'href') { hrefAttribute = attribute; } } if (hrefAttribute) { hrefAttribute.value = params.baseHref; } else { baseElement.attrs.push({ name: 'href', value: params.baseHref }); } treeAdapter.appendChild(baseFragment, baseElement); indexSource.replace(baseElement.__location.startOffset, baseElement.__location.endOffset, parse5.serialize(baseFragment, { treeAdapter })); } } const styleElements = treeAdapter.createDocumentFragment(); for (const stylesheet of stylesheets) { const attrs = [ { name: 'rel', value: 'stylesheet' }, { name: 'href', value: (params.deployUrl || '') + stylesheet }, ]; if (crossOrigin !== 'none') { attrs.push({ name: 'crossorigin', value: crossOrigin }); } if (params.sri) { const content = loadOutputFile(stylesheet); attrs.push(...this.generateSriAttributes(content)); } const element = treeAdapter.createElement('link', undefined, attrs); treeAdapter.appendChild(styleElements, element); } indexSource.insert(styleInsertionPoint, parse5.serialize(styleElements, { treeAdapter })); return indexSource; } generateSriAttributes(content) { const algo = 'sha384'; const hash = (0, crypto_1.createHash)(algo).update(content, 'utf8').digest('base64'); return [{ name: 'integrity', value: `${algo}-${hash}` }]; } filterAndMapBuildFiles(files, extensionFilter) { const filteredFiles = []; // This test excludes files generated by HMR (e.g. main.hot-update.js). const hotUpdateAsset = /hot-update\.[cm]?js$/; for (const { file, name, extension, initial } of files) { if (name && initial && extensionFilter.includes(extension) && !hotUpdateAsset.test(file)) { filteredFiles.push({ file, extension, name }); } } return filteredFiles; } } exports.WriteIndexHtmlPlugin = WriteIndexHtmlPlugin;