mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-29 16:33:43 +01:00
* Remove web components polyfills and ES5 custom elements adapter All supported browsers (legacy floor: Chrome 59, Safari/iOS 12, Firefox 94) have native shadow DOM and custom elements, so the webcomponents bundle, ShadyCSS branch, and lit polyfill-support are unreachable. The legacy build now emits ES2017 classes, so custom-elements-es5-adapter and the window.loadES5Adapter hook (no known third-party consumers) are removed. Also stops shipping the never-loaded dialog-polyfill css and drops both now-unused dependencies. * Remove keyed-es5 Terser workaround The custom keyed directive existed because Terser with ecma: 5 miscompiled the destructured update() parameters (#28732). The legacy build now minifies with ecma: 2017, so the stock lit-html keyed directive works in both builds. * Remove old-browser JS shims and stale ES5 build references Drops the IE-only navigator.msMaxTouchPoints check, replaces the toggleAttribute helper with the native method (polyfilled automatically for Chrome < 69 in the legacy build), and removes babel excludes for the uninstalled proxy-polyfill and unfetch packages. Updates comments that still described the legacy build as ES5. * Remove vendor prefixes for no-longer-supported browsers Deletes -ms- prefixes (IE/EdgeHTML only) and -webkit-/-moz- prefixed declarations that every supported browser understands unprefixed, or that Lightning CSS re-adds automatically from the unprefixed property in production builds. Blocks that only had prefixed user-select now use the standard property (previously Firefox got no user-select there at all). Converts the four -webkit-linear-gradient() declarations - the sole gradient syntax on those sliders - to standard linear-gradient(). * Remove dead html_url custom panel support html_url pointed to an HTML Import, a Polymer-era feature removed from Chrome in 2019 and never shipped elsewhere. The loader has not handled the html type for years (it fell through to a rejection), and core's panel_custom integration no longer accepts the option, so the branch was unreachable. Also drops the ha-panel-${name} legacy tag naming that was keyed on html_url. * Repair list-plugins-and-polyfills script for Babel 8 The audit script died at startup since the Babel 8 update: preset-env no longer exposes lib/debug.js (logPlugin is now inlined locally, built on the public getInclusionReasons helper) and babel-plugin-polyfill-corejs3 v1 no longer ships lib/shipped-proposals.js (list inlined). Instead of invoking the preset with a hand-mocked plugin API - the part that kept drifting - the plugin listing now runs a real transform of an empty file with preset-env in debug mode, declaring the same caller capabilities as babel-loader. The polyfill listing now passes the configured core-js version to core-js-compat, mirroring the provider's own filtering so the report cannot list modules the installed core-js lacks. Output is byte-identical to the direct-invocation approach. Also documents the script in the build-scripts README. * Correct macOS floor for Safari 26 in companion app UA regex Safari 26 ships for macOS 14 Sonoma and 15 Sequoia, not only macOS 26, so the SAFARI_TO_MACOS entry breaking the minimum-supported-macOS pattern would have sent updated macOS 14/15 companion apps to the legacy build once the modern floor reaches Safari 26. * Remove orphaned values left behind by vendor prefix removal * Require macOS 14.6 for Safari 26 in companion app UA regex
330 lines
8.7 KiB
JavaScript
330 lines
8.7 KiB
JavaScript
// Tasks to generate entry HTML
|
|
|
|
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 { dirname, extname, resolve } from "node:path";
|
|
import { htmlMinifierOptions, terserOptions } from "../bundle.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],
|
|
26: [14, 6, 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 {
|
|
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
|
|
hassUrl: process.env.HASS_URL || "",
|
|
// Single source for the stale-build recovery patterns, shared with the
|
|
// bundled src/util/recover-stale-build.ts and injected into the inline
|
|
// boot guard (_bootstrap_recovery.html.template).
|
|
staleBuildPatterns: fs.readJsonSync(
|
|
resolve(paths.root_dir, "src/util/stale-build-patterns.json")
|
|
),
|
|
};
|
|
};
|
|
|
|
const renderTemplate = (templateFile, data = {}) => {
|
|
const compiled = template(
|
|
fs.readFileSync(templateFile, { encoding: "utf-8" })
|
|
);
|
|
return compiled({
|
|
...data,
|
|
// Resolve any child/nested templates relative to the parent and pass the same data
|
|
renderTemplate: (childTemplate) =>
|
|
renderTemplate(resolve(dirname(templateFile), childTemplate), data),
|
|
});
|
|
};
|
|
|
|
const WRAP_TAGS = { ".js": "script", ".css": "style" };
|
|
|
|
const minifyHtml = (content, ext) => {
|
|
const wrapTag = WRAP_TAGS[ext] || "";
|
|
const begTag = wrapTag && `<${wrapTag}>`;
|
|
const endTag = wrapTag && `</${wrapTag}>`;
|
|
return minify(begTag + content + endTag, {
|
|
...htmlMinifierOptions,
|
|
conservativeCollapse: false,
|
|
minifyJS: terserOptions({
|
|
latestBuild: false, // Shared scripts must satisfy the legacy targets
|
|
isTestBuild: true, // Don't need source maps
|
|
}),
|
|
}).then((wrapped) =>
|
|
wrapTag ? wrapped.slice(begTag.length, -endTag.length) : wrapped
|
|
);
|
|
};
|
|
|
|
// Function to generate a dev task for each project's configuration
|
|
const genPagesDevTask =
|
|
(
|
|
pageEntries,
|
|
inputRoot,
|
|
outputRoot,
|
|
inputSub = "src/html",
|
|
publicRoot = ""
|
|
) =>
|
|
async () => {
|
|
const commonVars = getCommonTemplateVars();
|
|
for (const [page, entries] of Object.entries(pageEntries)) {
|
|
const content = renderTemplate(
|
|
resolve(inputRoot, inputSub, `${page}.template`),
|
|
{
|
|
...commonVars,
|
|
// Dev entries are unhashed, so the stale-index recovery guard has
|
|
// nothing to key off and rebuild churn could cause spurious reloads.
|
|
useCacheRecovery: false,
|
|
latestEntryJS: entries.map(
|
|
(entry) => `${publicRoot}/frontend_latest/${entry}.js`
|
|
),
|
|
es5EntryJS: entries.map(
|
|
(entry) => `${publicRoot}/frontend_es5/${entry}.js`
|
|
),
|
|
latestCustomPanelJS: `${publicRoot}/frontend_latest/custom-panel.js`,
|
|
es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`,
|
|
}
|
|
);
|
|
fs.outputFileSync(resolve(outputRoot, page), content);
|
|
}
|
|
};
|
|
|
|
// Same as previous but for production builds
|
|
// (includes minification and hashed file names from manifest)
|
|
const genPagesProdTask =
|
|
(
|
|
pageEntries,
|
|
inputRoot,
|
|
outputRoot,
|
|
outputLatest,
|
|
outputES5,
|
|
inputSub = "src/html"
|
|
) =>
|
|
async () => {
|
|
const latestManifest = fs.readJsonSync(
|
|
resolve(outputLatest, "manifest.json")
|
|
);
|
|
const es5Manifest = outputES5
|
|
? fs.readJsonSync(resolve(outputES5, "manifest.json"))
|
|
: {};
|
|
const commonVars = getCommonTemplateVars();
|
|
const minifiedHTML = [];
|
|
for (const [page, entries] of Object.entries(pageEntries)) {
|
|
const content = renderTemplate(
|
|
resolve(inputRoot, inputSub, `${page}.template`),
|
|
{
|
|
...commonVars,
|
|
// Recover from a stale index.html that pins deleted hashed entry
|
|
// bundles (see _bootstrap_recovery.html.template).
|
|
useCacheRecovery: true,
|
|
latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]),
|
|
es5EntryJS: outputES5
|
|
? entries.map((entry) => es5Manifest[`${entry}.js`])
|
|
: [],
|
|
latestCustomPanelJS: latestManifest["custom-panel.js"],
|
|
es5CustomPanelJS: outputES5 ? es5Manifest["custom-panel.js"] : "",
|
|
}
|
|
);
|
|
minifiedHTML.push(
|
|
minifyHtml(content, extname(page)).then((minified) =>
|
|
fs.outputFileSync(resolve(outputRoot, page), minified)
|
|
)
|
|
);
|
|
}
|
|
await Promise.all(minifiedHTML);
|
|
};
|
|
|
|
// Map HTML pages to their required entrypoints
|
|
const APP_PAGE_ENTRIES = {
|
|
"authorize.html": ["authorize"],
|
|
"onboarding.html": ["onboarding"],
|
|
"index.html": ["core", "app"],
|
|
};
|
|
|
|
gulp.task(
|
|
"gen-pages-app-dev",
|
|
genPagesDevTask(APP_PAGE_ENTRIES, paths.root_dir, paths.app_output_root)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-app-prod",
|
|
genPagesProdTask(
|
|
APP_PAGE_ENTRIES,
|
|
paths.root_dir,
|
|
paths.app_output_root,
|
|
paths.app_output_latest,
|
|
paths.app_output_es5
|
|
)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-app-prod-modern",
|
|
genPagesProdTask(
|
|
APP_PAGE_ENTRIES,
|
|
paths.root_dir,
|
|
paths.app_output_root,
|
|
paths.app_output_latest,
|
|
undefined
|
|
)
|
|
);
|
|
|
|
const CAST_PAGE_ENTRIES = {
|
|
"faq.html": ["launcher"],
|
|
"index.html": ["launcher"],
|
|
"media.html": ["media"],
|
|
"receiver.html": ["receiver"],
|
|
};
|
|
|
|
gulp.task(
|
|
"gen-pages-cast-dev",
|
|
genPagesDevTask(CAST_PAGE_ENTRIES, paths.cast_dir, paths.cast_output_root)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-cast-prod",
|
|
genPagesProdTask(
|
|
CAST_PAGE_ENTRIES,
|
|
paths.cast_dir,
|
|
paths.cast_output_root,
|
|
paths.cast_output_latest,
|
|
paths.cast_output_es5
|
|
)
|
|
);
|
|
|
|
const DEMO_PAGE_ENTRIES = { "index.html": ["main"] };
|
|
|
|
gulp.task(
|
|
"gen-pages-demo-dev",
|
|
genPagesDevTask(DEMO_PAGE_ENTRIES, paths.demo_dir, paths.demo_output_root)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-demo-prod",
|
|
genPagesProdTask(
|
|
DEMO_PAGE_ENTRIES,
|
|
paths.demo_dir,
|
|
paths.demo_output_root,
|
|
paths.demo_output_latest,
|
|
paths.demo_output_es5
|
|
)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-demo-prod-e2e",
|
|
genPagesProdTask(
|
|
DEMO_PAGE_ENTRIES,
|
|
paths.demo_dir,
|
|
paths.demo_output_root,
|
|
paths.demo_output_latest
|
|
)
|
|
);
|
|
|
|
const GALLERY_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
|
|
|
gulp.task(
|
|
"gen-pages-gallery-dev",
|
|
genPagesDevTask(
|
|
GALLERY_PAGE_ENTRIES,
|
|
paths.gallery_dir,
|
|
paths.gallery_output_root
|
|
)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-gallery-prod",
|
|
genPagesProdTask(
|
|
GALLERY_PAGE_ENTRIES,
|
|
paths.gallery_dir,
|
|
paths.gallery_output_root,
|
|
paths.gallery_output_latest
|
|
)
|
|
);
|
|
|
|
const LANDING_PAGE_PAGE_ENTRIES = { "index.html": ["entrypoint"] };
|
|
|
|
gulp.task(
|
|
"gen-pages-landing-page-dev",
|
|
genPagesDevTask(
|
|
LANDING_PAGE_PAGE_ENTRIES,
|
|
paths.landingPage_dir,
|
|
paths.landingPage_output_root
|
|
)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-landing-page-prod",
|
|
genPagesProdTask(
|
|
LANDING_PAGE_PAGE_ENTRIES,
|
|
paths.landingPage_dir,
|
|
paths.landingPage_output_root,
|
|
paths.landingPage_output_latest,
|
|
paths.landingPage_output_es5
|
|
)
|
|
);
|
|
|
|
const E2E_TEST_APP_PAGE_ENTRIES = {
|
|
"index.html": ["main"],
|
|
"dashboard.html": ["dashboard"],
|
|
"onboarding.html": ["onboarding"],
|
|
};
|
|
|
|
gulp.task(
|
|
"gen-pages-e2e-test-app-dev",
|
|
genPagesDevTask(
|
|
E2E_TEST_APP_PAGE_ENTRIES,
|
|
paths.e2eTestApp_dir,
|
|
paths.e2eTestApp_output_root
|
|
)
|
|
);
|
|
|
|
gulp.task(
|
|
"gen-pages-e2e-test-app-prod",
|
|
genPagesProdTask(
|
|
E2E_TEST_APP_PAGE_ENTRIES,
|
|
paths.e2eTestApp_dir,
|
|
paths.e2eTestApp_output_root,
|
|
paths.e2eTestApp_output_latest
|
|
)
|
|
);
|