diff --git a/.browserslistrc b/.browserslistrc index 516fec9cec5e..f500a9360514 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -1,28 +1,25 @@ [modern] -# Support for dynamic import is the main litmus test for serving modern builds. -# Although officially a ES2020 feature, browsers implemented it early, so this -# enables all of ES2017 and some features in ES2018. -supports es6-module-dynamic-import - -# Exclude Safari 11-12 because of a bug in tagged template literals -# https://bugs.webkit.org/show_bug.cgi?id=190756 -# Note: Dropping version 11 also enables several more ES2018 features -not Safari < 13 -not iOS < 13 - -# Exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data -# Babel ignores these automatically, but we need here for Webpack to output ESM with dynamic imports +# Modern builds target recent browsers supporting the latest features to minimize transpilation, polyfills, etc. +# It is served to browsers meeting the following requirements: +# - released in the last year + current alpha/beta versions +# - Firefox extended support release (ESR) +# - with global utilization at or above 0.5% +# - must support dynamic import of ES modules +# - exclude browsers no longer being maintained +# - exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data +unreleased versions +last 1 year +Firefox ESR +>= 0.5% and supports es6-module-dynamic-import +not dead not KaiOS > 0 not QQAndroid > 0 not UCAndroid > 0 -# Exclude unsupported browsers -not dead - [legacy] # Legacy builds are served when modern requirements are not met and support browsers: # - released in the last 7 years + current alpha/beta versionss -# - with global utilization above 0.05% +# - with global utilization at or above 0.05% # The lattermost query ensures that support for popular old browsers is not dropped too early # (e.g. IE 11, Android 4.4, or Samsung 4). # @@ -36,4 +33,10 @@ not dead # As of May 2023, only web sockets must be added to the query. unreleased versions last 7 years -> 0.05% and supports websockets +>= 0.05% and supports websockets + +[legacy-sw] +# Same as legacy plus supports service workers +unreleased versions +last 7 years +>= 0.05% and supports websockets and supports serviceworkers diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 17278c6123f1..8975ef3c4258 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -76,7 +76,7 @@ jobs: - name: Build wheels uses: home-assistant/wheels@2024.07.1 with: - abi: cp311 + abi: cp312 tag: musllinux_1_2 arch: amd64 wheels-key: ${{ secrets.WHEELS_KEY }} diff --git a/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch b/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch new file mode 100644 index 000000000000..81e93fcd1079 --- /dev/null +++ b/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch @@ -0,0 +1,55 @@ +diff --git a/build/inject-manifest.js b/build/inject-manifest.js +index 60e3d2bb51c11a19fbbedbad65e101082ec41c36..fed6026630f43f86e25446383982cf6fb694313b 100644 +--- a/build/inject-manifest.js ++++ b/build/inject-manifest.js +@@ -104,7 +104,7 @@ async function injectManifest(config) { + replaceString: manifestString, + searchString: options.injectionPoint, + }); +- filesToWrite[options.swDest] = source; ++ filesToWrite[options.swDest] = source.replace(url, encodeURI(upath_1.default.basename(destPath))); + filesToWrite[destPath] = map; + } + else { +diff --git a/build/lib/translate-url-to-sourcemap-paths.js b/build/lib/translate-url-to-sourcemap-paths.js +index 3220c5474eeac6e8a56ca9b2ac2bd9be48529e43..5f003879a904d4840529a42dd056d288fd213771 100644 +--- a/build/lib/translate-url-to-sourcemap-paths.js ++++ b/build/lib/translate-url-to-sourcemap-paths.js +@@ -22,7 +22,7 @@ function translateURLToSourcemapPaths(url, swSrc, swDest) { + const possibleSrcPath = upath_1.default.resolve(upath_1.default.dirname(swSrc), url); + if (fs_extra_1.default.existsSync(possibleSrcPath)) { + srcPath = possibleSrcPath; +- destPath = upath_1.default.resolve(upath_1.default.dirname(swDest), url); ++ destPath = `${swDest}.map`; + } + else { + warning = `${errors_1.errors['cant-find-sourcemap']} ${possibleSrcPath}`; +diff --git a/src/inject-manifest.ts b/src/inject-manifest.ts +index 8795ddcaa77aea7b0356417e4bc4b19e2b3f860c..fcdc68342d9ac53936c9ed40a9ccfc2f5070cad3 100644 +--- a/src/inject-manifest.ts ++++ b/src/inject-manifest.ts +@@ -129,7 +129,10 @@ export async function injectManifest( + searchString: options.injectionPoint!, + }); + +- filesToWrite[options.swDest] = source; ++ filesToWrite[options.swDest] = source.replace( ++ url!, ++ encodeURI(upath.basename(destPath)), ++ ); + filesToWrite[destPath] = map; + } else { + // If there's no sourcemap associated with swSrc, a simple string +diff --git a/src/lib/translate-url-to-sourcemap-paths.ts b/src/lib/translate-url-to-sourcemap-paths.ts +index 072eac40d4ef5d095a01cb7f7e392a9e034853bd..f0bbe69e88ef3a415de18a7e9cb264daea273d71 100644 +--- a/src/lib/translate-url-to-sourcemap-paths.ts ++++ b/src/lib/translate-url-to-sourcemap-paths.ts +@@ -28,7 +28,7 @@ export function translateURLToSourcemapPaths( + const possibleSrcPath = upath.resolve(upath.dirname(swSrc), url); + if (fse.existsSync(possibleSrcPath)) { + srcPath = possibleSrcPath; +- destPath = upath.resolve(upath.dirname(swDest), url); ++ destPath = `${swDest}.map`; + } else { + warning = `${errors['cant-find-sourcemap']} ${possibleSrcPath}`; + } diff --git a/build-scripts/bundle.cjs b/build-scripts/bundle.cjs index 9c292c792101..84e1490f995b 100644 --- a/build-scripts/bundle.cjs +++ b/build-scripts/bundle.cjs @@ -47,7 +47,7 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) => module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ __DEV__: !isProdBuild, - __BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"), + __BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"), __VERSION__: JSON.stringify(env.version()), __DEMO__: false, __SUPERVISOR__: false, @@ -79,7 +79,12 @@ module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({ sourceMap: !isTestBuild, }); -module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ +module.exports.babelOptions = ({ + latestBuild, + isProdBuild, + isTestBuild, + sw, +}) => ({ babelrc: false, compact: false, assumptions: { @@ -87,7 +92,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ setPublicClassFields: true, setSpreadProperties: true, }, - browserslistEnv: latestBuild ? "modern" : "legacy", + browserslistEnv: latestBuild ? "modern" : `legacy${sw ? "-sw" : ""}`, presets: [ [ "@babel/preset-env", @@ -135,8 +140,14 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ "@babel/plugin-transform-runtime", { version: dependencies["@babel/runtime"] }, ], - // Support some proposals still in TC39 process - ["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }], + // Transpile decorators (still in TC39 process) + // Modern browsers support class fields and private methods, but transform is required with the older decorator version dictated by Lit + [ + "@babel/plugin-proposal-decorators", + { version: "2018-09", decoratorsBeforeExport: true }, + ], + "@babel/plugin-transform-class-properties", + "@babel/plugin-transform-private-methods", ].filter(Boolean), exclude: [ // \\ for Windows, / for Mac OS and Linux @@ -215,7 +226,13 @@ module.exports.config = { return { name: "frontend" + nameSuffix(latestBuild), entry: { - service_worker: "./src/entrypoints/service_worker.ts", + "service-worker": + !env.useRollup() && !latestBuild + ? { + import: "./src/entrypoints/service-worker.ts", + layer: "sw", + } + : "./src/entrypoints/service-worker.ts", app: "./src/entrypoints/app.ts", authorize: "./src/entrypoints/authorize.ts", onboarding: "./src/entrypoints/onboarding.ts", diff --git a/build-scripts/gulp/entry-html.js b/build-scripts/gulp/entry-html.js index dd6285c3103a..3afb7510393c 100644 --- a/build-scripts/gulp/entry-html.js +++ b/build-scripts/gulp/entry-html.js @@ -1,5 +1,6 @@ // Tasks to generate entry HTML +import { getUserAgentRegex } from "browserslist-useragent-regexp"; import fs from "fs-extra"; import gulp from "gulp"; import { minify } from "html-minifier-terser"; @@ -17,6 +18,12 @@ const renderTemplate = (templateFile, data = {}) => { ...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( diff --git a/build-scripts/gulp/service-worker.js b/build-scripts/gulp/service-worker.js index f9134da76c0e..ff4d5c2b7e03 100644 --- a/build-scripts/gulp/service-worker.js +++ b/build-scripts/gulp/service-worker.js @@ -1,20 +1,19 @@ -// Generate service worker. -// Based on manifest, create a file with the content as service_worker.js +// Generate service workers -import fs from "fs-extra"; +import { deleteAsync } from "del"; import gulp from "gulp"; -import path from "path"; -import sourceMapUrl from "source-map-url"; -import workboxBuild from "workbox-build"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { join, relative } from "node:path"; +import { injectManifest } from "workbox-build"; import paths from "../paths.cjs"; -const swDest = path.resolve(paths.app_output_root, "service_worker.js"); +const SW_MAP = { + [paths.app_output_latest]: "modern", + [paths.app_output_es5]: "legacy", +}; -const writeSW = (content) => fs.outputFileSync(swDest, content.trim() + "\n"); - -gulp.task("gen-service-worker-app-dev", (done) => { - writeSW( - ` +const SW_DEV = + ` console.debug('Service worker disabled in development'); self.addEventListener('install', (event) => { @@ -22,72 +21,61 @@ self.addEventListener('install', (event) => { // removing any prod service worker the dev might have running self.skipWaiting(); }); - ` - ); - done(); -}); + `.trim() + "\n"; -gulp.task("gen-service-worker-app-prod", async () => { - // Read bundled source file - const bundleManifestLatest = fs.readJsonSync( - path.resolve(paths.app_output_latest, "manifest.json") +gulp.task("gen-service-worker-app-dev", async () => { + await mkdir(paths.app_output_root, { recursive: true }); + await Promise.all( + Object.values(SW_MAP).map((build) => + writeFile(join(paths.app_output_root, `sw-${build}.js`), SW_DEV, { + encoding: "utf-8", + }) + ) ); - let serviceWorkerContent = fs.readFileSync( - paths.app_output_root + bundleManifestLatest["service_worker.js"], - "utf-8" - ); - - // Delete old file from frontend_latest so manifest won't pick it up - fs.removeSync( - paths.app_output_root + bundleManifestLatest["service_worker.js"] - ); - fs.removeSync( - paths.app_output_root + bundleManifestLatest["service_worker.js.map"] - ); - - // Remove ES5 - const bundleManifestES5 = fs.readJsonSync( - path.resolve(paths.app_output_es5, "manifest.json") - ); - fs.removeSync(paths.app_output_root + bundleManifestES5["service_worker.js"]); - fs.removeSync( - paths.app_output_root + bundleManifestES5["service_worker.js.map"] - ); - - const workboxManifest = await workboxBuild.getManifest({ - // Files that mach this pattern will be considered unique and skip revision check - // ignore JS files + translation files - dontCacheBustURLsMatching: /(frontend_latest\/.+|static\/translations\/.+)/, - - globDirectory: paths.app_output_root, - globPatterns: [ - "frontend_latest/*.js", - // Cache all English translations because we catch them as fallback - // Using pattern to match hash instead of * to avoid caching en-GB - // 'v' added as valid hash letter because in dev we hash with 'dev' - "static/translations/**/en-+([a-fv0-9]).json", - // Icon shown on splash screen - "static/icons/favicon-192x192.png", - "static/icons/favicon.ico", - // Common fonts - "static/fonts/roboto/Roboto-Light.woff2", - "static/fonts/roboto/Roboto-Medium.woff2", - "static/fonts/roboto/Roboto-Regular.woff2", - "static/fonts/roboto/Roboto-Bold.woff2", - ], - }); - - for (const warning of workboxManifest.warnings) { - console.warn(warning); - } - - // remove source map and add WB manifest - serviceWorkerContent = sourceMapUrl.removeFrom(serviceWorkerContent); - serviceWorkerContent = serviceWorkerContent.replace( - "WB_MANIFEST", - JSON.stringify(workboxManifest.manifestEntries) - ); - - // Write new file to root - fs.writeFileSync(swDest, serviceWorkerContent); }); + +gulp.task("gen-service-worker-app-prod", () => + Promise.all( + Object.entries(SW_MAP).map(async ([outPath, build]) => { + const manifest = JSON.parse( + await readFile(join(outPath, "manifest.json"), "utf-8") + ); + const swSrc = join(paths.app_output_root, manifest["service-worker.js"]); + const buildDir = relative(paths.app_output_root, outPath); + const { warnings } = await injectManifest({ + swSrc, + swDest: join(paths.app_output_root, `sw-${build}.js`), + injectionPoint: "__WB_MANIFEST__", + // Files that mach this pattern will be considered unique and skip revision check + // ignore JS files + translation files + dontCacheBustURLsMatching: new RegExp( + `(?:${buildDir}/.+|static/translations/.+)` + ), + globDirectory: paths.app_output_root, + globPatterns: [ + `${buildDir}/*.js`, + // Cache all English translations because we catch them as fallback + // Using pattern to match hash instead of * to avoid caching en-GB + // 'v' added as valid hash letter because in dev we hash with 'dev' + "static/translations/**/en-+([a-fv0-9]).json", + // Icon shown on splash screen + "static/icons/favicon-192x192.png", + "static/icons/favicon.ico", + // Common fonts + "static/fonts/roboto/Roboto-Light.woff2", + "static/fonts/roboto/Roboto-Medium.woff2", + "static/fonts/roboto/Roboto-Regular.woff2", + "static/fonts/roboto/Roboto-Bold.woff2", + ], + globIgnores: [`${buildDir}/service-worker*`], + }); + if (warnings.length > 0) { + console.warn( + `Problems while injecting ${build} service worker:\n`, + warnings.join("\n") + ); + } + await deleteAsync(`${swSrc}?(.map)`); + }) + ) +); diff --git a/build-scripts/webpack.cjs b/build-scripts/webpack.cjs index 5ba0f35d2383..94ca35b8f553 100644 --- a/build-scripts/webpack.cjs +++ b/build-scripts/webpack.cjs @@ -63,14 +63,19 @@ const createWebpackConfig = ({ rules: [ { test: /\.m?js$|\.ts$/, - use: { + use: (info) => ({ loader: "babel-loader", options: { - ...bundle.babelOptions({ latestBuild, isProdBuild, isTestBuild }), + ...bundle.babelOptions({ + latestBuild, + isProdBuild, + isTestBuild, + sw: info.issuerLayer === "sw", + }), cacheDirectory: !isProdBuild, cacheCompression: false, }, - }, + }), resolve: { fullySpecified: false, }, @@ -235,6 +240,7 @@ const createWebpackConfig = ({ ), }, experiments: { + layers: true, outputModule: true, }, }; diff --git a/cast/src/html/faq.html.template b/cast/src/html/faq.html.template index 80fc487bef21..ba22de72849d 100644 --- a/cast/src/html/faq.html.template +++ b/cast/src/html/faq.html.template @@ -36,13 +36,7 @@
<%= renderTemplate("../../../src/html/_js_base.html.template") %> - - <%= renderTemplate("../../../src/html/_script_load_es5.html.template") %> + <%= renderTemplate("../../../src/html/_script_loader.html.template") %>