"use strict"; /** * @license * Copyright Google LLC 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.dev/license */ Object.defineProperty(exports, "__esModule", { value: true }); exports.augmentIndexHtml = augmentIndexHtml; const node_crypto_1 = require("node:crypto"); const node_path_1 = require("node:path"); const load_esm_1 = require("../load-esm"); const html_rewriting_stream_1 = require("./html-rewriting-stream"); const valid_self_closing_tags_1 = require("./valid-self-closing-tags"); /* * Helper function used by the IndexHtmlWebpackPlugin. * Can also be directly used by builder, e. g. in order to generate an index.html * after processing several configurations in order to build different sets of * bundles for differential serving. */ // eslint-disable-next-line max-lines-per-function async function augmentIndexHtml(params) { const { loadOutputFile, files, entrypoints, sri, deployUrl, lang, baseHref, html, imageDomains } = params; const warnings = []; const errors = []; let { crossOrigin = 'none' } = params; if (sri && crossOrigin === 'none') { crossOrigin = 'anonymous'; } const stylesheets = new Set(); const scripts = new Map(); // Sort files in the order we want to insert them by entrypoint for (const [entrypoint, isModule] of entrypoints) { for (const { extension, file, name } of files) { if (name !== entrypoint || scripts.has(file) || stylesheets.has(file)) { continue; } switch (extension) { case '.js': // Also, non entrypoints need to be loaded as no module as they can contain problematic code. scripts.set(file, isModule); break; case '.mjs': if (!isModule) { // It would be very confusing to link an `*.mjs` file in a non-module script context, // so we disallow it entirely. throw new Error('`.mjs` files *must* set `isModule` to `true`.'); } scripts.set(file, true /* isModule */); break; case '.css': stylesheets.add(file); break; } } } let scriptTags = []; for (const [src, isModule] of scripts) { const attrs = [`src="${generateUrl(src, deployUrl)}"`]; // This is also need for non entry-points as they may contain problematic code. if (isModule) { attrs.push('type="module"'); } else { attrs.push('defer'); } if (crossOrigin !== 'none') { attrs.push(`crossorigin="${crossOrigin}"`); } if (sri) { const content = await loadOutputFile(src); attrs.push(generateSriAttributes(content)); } scriptTags.push(``); } let headerLinkTags = []; let bodyLinkTags = []; for (const src of stylesheets) { const attrs = [`rel="stylesheet"`, `href="${generateUrl(src, deployUrl)}"`]; if (crossOrigin !== 'none') { attrs.push(`crossorigin="${crossOrigin}"`); } if (sri) { const content = await loadOutputFile(src); attrs.push(generateSriAttributes(content)); } headerLinkTags.push(``); } if (params.hints?.length) { for (const hint of params.hints) { const attrs = [`rel="${hint.mode}"`, `href="${generateUrl(hint.url, deployUrl)}"`]; if (hint.mode !== 'modulepreload' && crossOrigin !== 'none') { // Value is considered anonymous by the browser when not present or empty attrs.push(crossOrigin === 'anonymous' ? 'crossorigin' : `crossorigin="${crossOrigin}"`); } if (hint.mode === 'preload' || hint.mode === 'prefetch') { switch ((0, node_path_1.extname)(hint.url)) { case '.js': attrs.push('as="script"'); break; case '.css': attrs.push('as="style"'); break; default: if (hint.as) { attrs.push(`as="${hint.as}"`); } break; } } if (sri && (hint.mode === 'preload' || hint.mode === 'prefetch' || hint.mode === 'modulepreload')) { const content = await loadOutputFile(hint.url); attrs.push(generateSriAttributes(content)); } const tag = ``; if (hint.mode === 'modulepreload') { // Module preloads should be placed by the inserted script elements in the body since // they are only useful in combination with the scripts. bodyLinkTags.push(tag); } else { headerLinkTags.push(tag); } } } const dir = lang ? await getLanguageDirection(lang, warnings) : undefined; const { rewriter, transformedContent } = await (0, html_rewriting_stream_1.htmlRewritingStream)(html); const baseTagExists = html.includes(' { switch (tag.tagName) { case 'html': // Adjust document locale if specified if (isString(lang)) { updateAttribute(tag, 'lang', lang); } if (dir) { updateAttribute(tag, 'dir', dir); } break; case 'head': // Base href should be added before any link, meta tags if (!baseTagExists && isString(baseHref)) { rewriter.emitStartTag(tag); rewriter.emitRaw(``); return; } break; case 'base': // Adjust base href if specified if (isString(baseHref)) { updateAttribute(tag, 'href', baseHref); } break; case 'link': if (readAttribute(tag, 'rel') === 'preconnect') { const href = readAttribute(tag, 'href'); if (href) { foundPreconnects.add(href); } } break; default: if (tag.selfClosing && !valid_self_closing_tags_1.VALID_SELF_CLOSING_TAGS.has(tag.tagName)) { errors.push(`Invalid self-closing element in index HTML file: '${rawTagHtml}'.`); return; } } rewriter.emitStartTag(tag); }) .on('endTag', (tag) => { switch (tag.tagName) { case 'head': for (const linkTag of headerLinkTags) { rewriter.emitRaw(linkTag); } if (imageDomains) { for (const imageDomain of imageDomains) { if (!foundPreconnects.has(imageDomain)) { rewriter.emitRaw(``); } } } headerLinkTags = []; break; case 'body': for (const linkTag of bodyLinkTags) { rewriter.emitRaw(linkTag); } bodyLinkTags = []; // Add script tags for (const scriptTag of scriptTags) { rewriter.emitRaw(scriptTag); } scriptTags = []; break; } rewriter.emitEndTag(tag); }); const content = await transformedContent(); return { content: headerLinkTags.length || scriptTags.length ? // In case no body/head tags are not present (dotnet partial templates) headerLinkTags.join('') + scriptTags.join('') + content : content, warnings, errors, }; } function generateSriAttributes(content) { const algo = 'sha384'; const hash = (0, node_crypto_1.createHash)(algo).update(content, 'utf8').digest('base64'); return `integrity="${algo}-${hash}"`; } function generateUrl(value, deployUrl) { if (!deployUrl) { return value; } // Skip if root-relative, absolute or protocol relative url if (/^((?:\w+:)?\/\/|data:|chrome:|\/)/.test(value)) { return value; } return `${deployUrl}${value}`; } function updateAttribute(tag, name, value) { const index = tag.attrs.findIndex((a) => a.name === name); const newValue = { name, value }; if (index === -1) { tag.attrs.push(newValue); } else { tag.attrs[index] = newValue; } } function readAttribute(tag, name) { const targetAttr = tag.attrs.find((attr) => attr.name === name); return targetAttr ? targetAttr.value : undefined; } function isString(value) { return typeof value === 'string'; } async function getLanguageDirection(locale, warnings) { const dir = await getLanguageDirectionFromLocales(locale); if (!dir) { warnings.push(`Locale data for '${locale}' cannot be found. 'dir' attribute will not be set for this locale.`); } return dir; } async function getLanguageDirectionFromLocales(locale) { try { const localeData = (await (0, load_esm_1.loadEsmModule)(`@angular/common/locales/${locale}`)).default; const dir = localeData[localeData.length - 2]; return isString(dir) ? dir : undefined; } catch { // In some cases certain locales might map to files which are named only with language id. // Example: `en-US` -> `en`. const [languageId] = locale.split('-', 1); if (languageId !== locale) { return getLanguageDirectionFromLocales(languageId); } } return undefined; }