diff --git a/.browserslistrc b/.browserslistrc index 516fec9cec5e..f5926db66fca 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -37,3 +37,9 @@ not dead unreleased versions last 7 years > 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/.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..f74ddfeef33d 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", @@ -215,7 +220,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/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/package.json b/package.json index adf7e38ef90a..cf2d567423ee 100644 --- a/package.json +++ b/package.json @@ -231,7 +231,6 @@ "rollup-plugin-visualizer": "5.12.0", "serve-handler": "6.1.5", "sinon": "18.0.0", - "source-map-url": "0.4.1", "systemjs": "6.15.1", "tar": "7.4.0", "terser-webpack-plugin": "5.3.10", @@ -244,7 +243,7 @@ "webpack-manifest-plugin": "5.0.0", "webpack-stats-plugin": "1.1.3", "webpackbar": "6.0.1", - "workbox-build": "7.1.1" + "workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch" }, "_comment": "Polymer 3.2 contained a bug, fixed in https://github.com/Polymer/polymer/pull/5569, add as patch", "resolutions": { diff --git a/src/entrypoints/custom-panel.ts b/src/entrypoints/custom-panel.ts index c12674a98e0a..767855459bf4 100644 --- a/src/entrypoints/custom-panel.ts +++ b/src/entrypoints/custom-panel.ts @@ -72,7 +72,7 @@ function initialize( ); } - if (__BUILD__ === "es5") { + if (__BUILD__ === "legacy") { start = start.then(() => window.loadES5Adapter()); } diff --git a/src/entrypoints/service_worker.ts b/src/entrypoints/service-worker.ts similarity index 95% rename from src/entrypoints/service_worker.ts rename to src/entrypoints/service-worker.ts index 4b7e262f3bd6..3b67fee9821d 100644 --- a/src/entrypoints/service_worker.ts +++ b/src/entrypoints/service-worker.ts @@ -13,18 +13,16 @@ import { StaleWhileRevalidate, } from "workbox-strategies"; +declare const __WB_MANIFEST__: Parameters[0]; + const noFallBackRegEx = /\/(api|static|auth|frontend_latest|frontend_es5|local)\/.*/; const initRouting = () => { - precacheAndRoute( - // @ts-ignore - WB_MANIFEST, - { - // Ignore all URL parameters. - ignoreURLParametersMatching: [/.*/], - } - ); + precacheAndRoute(__WB_MANIFEST__, { + // Ignore all URL parameters. + ignoreURLParametersMatching: [/.*/], + }); // Cache static content (including translations) on first access. registerRoute( @@ -56,11 +54,8 @@ const initRouting = () => { // Get api from network. registerRoute(/\/(api|auth)\/.*/, new NetworkOnly()); - // Get manifest, service worker, onboarding from network. - registerRoute( - /\/(service_worker.js|manifest.json|onboarding.html)/, - new NetworkOnly() - ); + // Get manifest and onboarding from network. + registerRoute(/\/(?:manifest\.json|onboarding\.html)/, new NetworkOnly()); // For the root "/" we ignore search registerRoute( diff --git a/src/panels/config/info/ha-config-info.ts b/src/panels/config/info/ha-config-info.ts index 37402301fbb9..b7289a7eed79 100644 --- a/src/panels/config/info/ha-config-info.ts +++ b/src/panels/config/info/ha-config-info.ts @@ -157,7 +157,7 @@ class HaConfigInfo extends LitElement { )} - ${JS_VERSION}${JS_TYPE !== "latest" ? ` ⸱ ${JS_TYPE}` : ""} + ${JS_VERSION}${JS_TYPE !== "modern" ? ` ⸱ ${JS_TYPE}` : ""} diff --git a/src/types.ts b/src/types.ts index 31a412364034..fb2a39618b59 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,7 +21,7 @@ declare global { /* eslint-disable no-var, no-redeclare */ var __DEV__: boolean; var __DEMO__: boolean; - var __BUILD__: "latest" | "es5"; + var __BUILD__: "modern" | "legacy"; var __VERSION__: string; var __STATIC_PATH__: string; var __BACKWARDS_COMPAT__: boolean; diff --git a/src/util/custom-panel/load-custom-panel.ts b/src/util/custom-panel/load-custom-panel.ts index 1286b6588f56..a9e61a1d0052 100644 --- a/src/util/custom-panel/load-custom-panel.ts +++ b/src/util/custom-panel/load-custom-panel.ts @@ -16,7 +16,7 @@ export const getUrl = ( // if both module and JS provided, base url on frontend build if (panelConfig.module_url && panelConfig.js_url) { - if (__BUILD__ === "latest") { + if (__BUILD__ === "modern") { return { type: "module", url: panelConfig.module_url, diff --git a/src/util/register-service-worker.ts b/src/util/register-service-worker.ts index 72e3864f7e0c..87d065420d7e 100644 --- a/src/util/register-service-worker.ts +++ b/src/util/register-service-worker.ts @@ -17,7 +17,7 @@ export const registerServiceWorker = async ( location.reload(); }); - const reg = await navigator.serviceWorker.register("/service_worker.js"); + const reg = await navigator.serviceWorker.register(`/sw-${__BUILD__}.js`); if (!notifyUpdate || __DEV__ || __DEMO__) { return; diff --git a/yarn.lock b/yarn.lock index f4f862143b8b..5f3af4cbd53d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9088,7 +9088,6 @@ __metadata: serve-handler: "npm:6.1.5" sinon: "npm:18.0.0" sortablejs: "npm:1.15.2" - source-map-url: "npm:0.4.1" stacktrace-js: "npm:2.0.2" superstruct: "npm:2.0.2" systemjs: "npm:6.15.1" @@ -9113,7 +9112,7 @@ __metadata: webpack-stats-plugin: "npm:1.1.3" webpackbar: "npm:6.0.1" weekstart: "npm:2.0.0" - workbox-build: "npm:7.1.1" + workbox-build: "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch" workbox-cacheable-response: "npm:7.1.0" workbox-core: "npm:7.1.0" workbox-expiration: "npm:7.1.0" @@ -13273,13 +13272,6 @@ __metadata: languageName: node linkType: hard -"source-map-url@npm:0.4.1": - version: 0.4.1 - resolution: "source-map-url@npm:0.4.1" - checksum: 10/7fec0460ca017330568e1a4d67c80c397871f27d75b034e1117eaa802076db5cda5944659144d26eafd2a95008ada19296c8e0d5ec116302c32c6daa4e430003 - languageName: node - linkType: hard - "source-map@npm:0.5.6": version: 0.5.6 resolution: "source-map@npm:0.5.6" @@ -15185,6 +15177,51 @@ __metadata: languageName: node linkType: hard +"workbox-build@patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch": + version: 7.1.1 + resolution: "workbox-build@patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch::version=7.1.1&hash=cd5737" + dependencies: + "@apideck/better-ajv-errors": "npm:^0.3.1" + "@babel/core": "npm:^7.24.4" + "@babel/preset-env": "npm:^7.11.0" + "@babel/runtime": "npm:^7.11.2" + "@rollup/plugin-babel": "npm:^5.2.0" + "@rollup/plugin-node-resolve": "npm:^15.2.3" + "@rollup/plugin-replace": "npm:^2.4.1" + "@rollup/plugin-terser": "npm:^0.4.3" + "@surma/rollup-plugin-off-main-thread": "npm:^2.2.3" + ajv: "npm:^8.6.0" + common-tags: "npm:^1.8.0" + fast-json-stable-stringify: "npm:^2.1.0" + fs-extra: "npm:^9.0.1" + glob: "npm:^7.1.6" + lodash: "npm:^4.17.20" + pretty-bytes: "npm:^5.3.0" + rollup: "npm:^2.43.1" + source-map: "npm:^0.8.0-beta.0" + stringify-object: "npm:^3.3.0" + strip-comments: "npm:^2.0.1" + tempy: "npm:^0.6.0" + upath: "npm:^1.2.0" + workbox-background-sync: "npm:7.1.0" + workbox-broadcast-update: "npm:7.1.0" + workbox-cacheable-response: "npm:7.1.0" + workbox-core: "npm:7.1.0" + workbox-expiration: "npm:7.1.0" + workbox-google-analytics: "npm:7.1.0" + workbox-navigation-preload: "npm:7.1.0" + workbox-precaching: "npm:7.1.0" + workbox-range-requests: "npm:7.1.0" + workbox-recipes: "npm:7.1.0" + workbox-routing: "npm:7.1.0" + workbox-strategies: "npm:7.1.0" + workbox-streams: "npm:7.1.0" + workbox-sw: "npm:7.1.0" + workbox-window: "npm:7.1.0" + checksum: 10/321a4f8d914ff3c1ac36e447d0c48e99d8925f4cdcff8332722c9273e93948f5a429de692149039622ce275cab454d6ba84144f9ae00a912b44b5d65f80695cc + languageName: node + linkType: hard + "workbox-cacheable-response@npm:7.1.0": version: 7.1.0 resolution: "workbox-cacheable-response@npm:7.1.0"