diff --git a/build-scripts/gulp/entry-html.js b/build-scripts/gulp/entry-html.js index 3afb751039..86e2e22ee0 100644 --- a/build-scripts/gulp/entry-html.js +++ b/build-scripts/gulp/entry-html.js @@ -1,35 +1,76 @@ // Tasks to generate entry HTML -import { getUserAgentRegex } from "browserslist-useragent-regexp"; +import { + applyVersionsToRegexes, + compileRegex, + getPreUserAgentRegexes, +} from "browserslist-useragent-regexp"; import fs from "fs-extra"; import gulp from "gulp"; import { minify } from "html-minifier-terser"; import template from "lodash.template"; -import path from "path"; +import { dirname, extname, resolve } from "node:path"; import { htmlMinifierOptions, terserOptions } from "../bundle.cjs"; import env from "../env.cjs"; import paths from "../paths.cjs"; +// macOS companion app has no way to obtain the Safari version used by WKWebView, +// and it is not in the default user agent string. So we add an additional regex +// to serve modern based on a minimum macOS version. We take the minimum Safari +// major version from browserslist and manually map that to a supported macOS +// version. Note this assumes the user has kept Safari updated. +const HA_MACOS_REGEX = + /Home Assistant\/[\d.]+ \(.+; macOS (\d+)\.(\d+)(?:\.(\d+))?\)/; +const SAFARI_TO_MACOS = { + 15: [10, 15, 0], + 16: [11, 0, 0], + 17: [12, 0, 0], + 18: [13, 0, 0], +}; + +const getCommonTemplateVars = () => { + const browserRegexes = getPreUserAgentRegexes({ + env: "modern", + allowHigherVersions: true, + mobileToDesktop: true, + throwOnMissing: true, + }); + const minSafariVersion = browserRegexes.find( + (regex) => regex.family === "safari" + )?.matchedVersions[0][0]; + const minMacOSVersion = SAFARI_TO_MACOS[minSafariVersion]; + if (!minMacOSVersion) { + throw Error( + `Could not find minimum MacOS version for Safari ${minSafariVersion}.` + ); + } + const haMacOSRegex = applyVersionsToRegexes( + [ + { + family: "ha_macos", + regex: HA_MACOS_REGEX, + matchedVersions: [minMacOSVersion], + requestVersions: [minMacOSVersion], + }, + ], + { ignorePatch: true, allowHigherVersions: true } + ); + return { + useRollup: env.useRollup(), + useWDS: env.useWDS(), + modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(), + }; +}; + const renderTemplate = (templateFile, data = {}) => { const compiled = template( fs.readFileSync(templateFile, { encoding: "utf-8" }) ); return compiled({ ...data, - useRollup: env.useRollup(), - useWDS: env.useWDS(), - modernRegex: getUserAgentRegex({ - env: "modern", - allowHigherVersions: true, - mobileToDesktop: true, - throwOnMissing: true, - }).toString(), // Resolve any child/nested templates relative to the parent and pass the same data renderTemplate: (childTemplate) => - renderTemplate( - path.resolve(path.dirname(templateFile), childTemplate), - data - ), + renderTemplate(resolve(dirname(templateFile), childTemplate), data), }); }; @@ -63,10 +104,12 @@ const genPagesDevTask = publicRoot = "" ) => async () => { + const commonVars = getCommonTemplateVars(); for (const [page, entries] of Object.entries(pageEntries)) { const content = renderTemplate( - path.resolve(inputRoot, inputSub, `${page}.template`), + resolve(inputRoot, inputSub, `${page}.template`), { + ...commonVars, latestEntryJS: entries.map((entry) => useWDS ? `http://localhost:8000/src/entrypoints/${entry}.ts` @@ -81,7 +124,7 @@ const genPagesDevTask = es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`, } ); - fs.outputFileSync(path.resolve(outputRoot, page), content); + fs.outputFileSync(resolve(outputRoot, page), content); } }; @@ -98,16 +141,18 @@ const genPagesProdTask = ) => async () => { const latestManifest = fs.readJsonSync( - path.resolve(outputLatest, "manifest.json") + resolve(outputLatest, "manifest.json") ); const es5Manifest = outputES5 - ? fs.readJsonSync(path.resolve(outputES5, "manifest.json")) + ? fs.readJsonSync(resolve(outputES5, "manifest.json")) : {}; + const commonVars = getCommonTemplateVars(); const minifiedHTML = []; for (const [page, entries] of Object.entries(pageEntries)) { const content = renderTemplate( - path.resolve(inputRoot, inputSub, `${page}.template`), + resolve(inputRoot, inputSub, `${page}.template`), { + ...commonVars, latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]), es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]), latestCustomPanelJS: latestManifest["custom-panel.js"], @@ -115,8 +160,8 @@ const genPagesProdTask = } ); minifiedHTML.push( - minifyHtml(content, path.extname(page)).then((minified) => - fs.outputFileSync(path.resolve(outputRoot, page), minified) + minifyHtml(content, extname(page)).then((minified) => + fs.outputFileSync(resolve(outputRoot, page), minified) ) ); }