"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;
}