diff --git a/.eslintrc.js b/.eslintrc.js index 5341c4e0..cb28f4c8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,7 +15,11 @@ module.exports = { "no-unused-vars": "warn" }, "globals": { - "HYDROGEN_VERSION": "readonly", - "HYDROGEN_GLOBAL_HASH": "readonly" + "DEFINE_VERSION": "readonly", + "DEFINE_GLOBAL_HASH": "readonly", + // only available in sw.js + "DEFINE_UNHASHED_PRECACHED_ASSETS": "readonly", + "DEFINE_HASHED_PRECACHED_ASSETS": "readonly", + "DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS": "readonly" } }; diff --git a/scripts/build-plugins/service-worker.js b/scripts/build-plugins/service-worker.js index 78d34b7f..19ae793f 100644 --- a/scripts/build-plugins/service-worker.js +++ b/scripts/build-plugins/service-worker.js @@ -8,7 +8,7 @@ function contentHash(str) { return hasher.digest(); } -module.exports = function injectServiceWorker(swFile, otherUnhashedFiles, globalHashPlaceholderLiteral, chunkNamesWithGlobalHash) { +function injectServiceWorker(swFile, otherUnhashedFiles, placeholdersPerChunk) { const swName = path.basename(swFile); let root; let version; @@ -26,7 +26,7 @@ module.exports = function injectServiceWorker(swFile, otherUnhashedFiles, global }, configResolved: config => { root = config.root; - version = JSON.parse(config.define.HYDROGEN_VERSION); // unquote + version = JSON.parse(config.define.DEFINE_VERSION); // unquote }, generateBundle: async function(options, bundle) { const unhashedFilenames = [swName].concat(otherUnhashedFiles); @@ -41,9 +41,11 @@ module.exports = function injectServiceWorker(swFile, otherUnhashedFiles, global const assets = Object.values(bundle); const hashedFileNames = assets.map(o => o.fileName).filter(fileName => !unhashedFileContentMap[fileName]); const globalHash = getBuildHash(hashedFileNames, unhashedFileContentMap); - const sw = bundle[swName]; - sw.code = replaceCacheFilenamesInServiceWorker(sw, unhashedFilenames, assets); - replaceGlobalHashPlaceholderInChunks(assets, chunkNamesWithGlobalHash, globalHashPlaceholderLiteral, `"${globalHash}"`); + const placeholderValues = { + DEFINE_GLOBAL_HASH: `"${globalHash}"`, + ...getCacheFileNamePlaceholderValues(swName, unhashedFilenames, assets, placeholdersPerChunk) + }; + replacePlaceholdersInChunks(assets, placeholdersPerChunk, placeholderValues); console.log(`\nBuilt ${version} (${globalHash})`); } }; @@ -76,8 +78,7 @@ function isPreCached(asset) { fileName.endsWith(".js") && !NON_PRECACHED_JS.includes(path.basename(name)); } -function replaceCacheFilenamesInServiceWorker(swChunk, unhashedFilenames, assets) { - let swSource = swChunk.code; +function getCacheFileNamePlaceholderValues(swName, unhashedFilenames, assets) { const unhashedPreCachedAssets = []; const hashedPreCachedAssets = []; const hashedCachedOnRequestAssets = []; @@ -86,7 +87,7 @@ function replaceCacheFilenamesInServiceWorker(swChunk, unhashedFilenames, assets const {name, fileName} = asset; // the service worker should not be cached at all, // it's how updates happen - if (fileName === swChunk.fileName) { + if (fileName === swName) { continue; } else if (unhashedFilenames.includes(fileName)) { unhashedPreCachedAssets.push(fileName); @@ -97,33 +98,57 @@ function replaceCacheFilenamesInServiceWorker(swChunk, unhashedFilenames, assets } } - const replaceArrayInSource = (name, value) => { - const newSource = swSource.replace(`${name} = []`, `${name} = ${JSON.stringify(value)}`); - if (newSource === swSource) { - throw new Error(`${name} was not found in the service worker source: ` + swSource); - } - return newSource; - }; - const replaceStringInSource = (name, value) => { - const newSource = swSource.replace(new RegExp(`${name}\\s=\\s"[^"]*"`), `${name} = ${JSON.stringify(value)}`); - if (newSource === swSource) { - throw new Error(`${name} was not found in the service worker source: ` + swSource); - } - return newSource; - }; - - swSource = replaceArrayInSource("UNHASHED_PRECACHED_ASSETS", unhashedPreCachedAssets); - swSource = replaceArrayInSource("HASHED_PRECACHED_ASSETS", hashedPreCachedAssets); - swSource = replaceArrayInSource("HASHED_CACHED_ON_REQUEST_ASSETS", hashedCachedOnRequestAssets); - return swSource; -} - -function replaceGlobalHashPlaceholderInChunks(assets, chunkNamesWithGlobalHash, globalHashPlaceholderLiteral, globalHashLiteral) { - for (const name of chunkNamesWithGlobalHash) { - const chunk = assets.find(a => a.type === "chunk" && a.name === name); - if (!chunk) { - throw new Error(`could not find chunk ${name} to replace global hash placeholder`); - } - chunk.code = chunk.code.replaceAll(globalHashPlaceholderLiteral, globalHashLiteral); + return { + DEFINE_UNHASHED_PRECACHED_ASSETS: JSON.stringify(unhashedPreCachedAssets), + DEFINE_HASHED_PRECACHED_ASSETS: JSON.stringify(hashedPreCachedAssets), + DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS: JSON.stringify(hashedCachedOnRequestAssets) } } + +function replacePlaceholdersInChunks(assets, placeholdersPerChunk, placeholderValues) { + for (const [name, placeholderMap] of Object.entries(placeholdersPerChunk)) { + const chunk = assets.find(a => a.type === "chunk" && a.name === name); + if (!chunk) { + throw new Error(`could not find chunk ${name} to replace placeholders`); + } + for (const [placeholderName, placeholderLiteral] of Object.entries(placeholderMap)) { + const replacedValue = placeholderValues[placeholderName]; + const oldCode = chunk.code; + chunk.code = chunk.code.replaceAll(placeholderLiteral, replacedValue); + if (chunk.code === oldCode) { + throw new Error(`Could not replace ${placeholderName} in ${name}, looking for literal ${placeholderLiteral}:\n${chunk.code}`); + } + } + } +} + +/** creates a value to be include in the `define` build settings, + * but can be replace at the end of the build in certain chunks. + * We need this for injecting the global build hash and the final + * filenames in the service worker and index chunk. + * These values are only known in the generateBundle step, so we + * replace them by unique strings wrapped in a prompt call so no + * transformation will touch them (minifying, ...) and we can do a + * string replacement still at the end of the build. */ +function definePlaceholderValue(mode, name, devValue) { + if (mode === "production") { + // note that `prompt(...)` will never be in the final output, it's replaced by the final value + // once we know at the end of the build what it is and just used as a temporary value during the build + // as something that will not be transformed. + // I first considered Symbol but it's not inconceivable that babel would transform this. + return `prompt(${JSON.stringify(name)})`; + } else { + return JSON.stringify(devValue); + } +} + +function createPlaceholderValues(mode) { + return { + DEFINE_GLOBAL_HASH: definePlaceholderValue(mode, "DEFINE_GLOBAL_HASH", null), + DEFINE_UNHASHED_PRECACHED_ASSETS: definePlaceholderValue(mode, "UNHASHED_PRECACHED_ASSETS", []), + DEFINE_HASHED_PRECACHED_ASSETS: definePlaceholderValue(mode, "HASHED_PRECACHED_ASSETS", []), + DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS: definePlaceholderValue(mode, "HASHED_CACHED_ON_REQUEST_ASSETS", []), + }; +} + +module.exports = {injectServiceWorker, createPlaceholderValues}; diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index b23faa6a..569daeb4 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -277,7 +277,7 @@ export class Platform { } get version() { - return HYDROGEN_VERSION; + return DEFINE_VERSION; } dispose() { diff --git a/src/platform/web/dom/ServiceWorkerHandler.js b/src/platform/web/dom/ServiceWorkerHandler.js index c7ba663a..0bb8c725 100644 --- a/src/platform/web/dom/ServiceWorkerHandler.js +++ b/src/platform/web/dom/ServiceWorkerHandler.js @@ -181,11 +181,11 @@ export class ServiceWorkerHandler { } get version() { - return HYDROGEN_VERSION; + return DEFINE_VERSION; } get buildHash() { - return HYDROGEN_GLOBAL_HASH; + return DEFINE_GLOBAL_HASH; } async preventConcurrentSessionAccess(sessionId) { diff --git a/src/platform/web/sw.js b/src/platform/web/sw.js index 6b864444..c5f69438 100644 --- a/src/platform/web/sw.js +++ b/src/platform/web/sw.js @@ -17,11 +17,11 @@ limitations under the License. import NOTIFICATION_BADGE_ICON from "./assets/icon.png?url"; // replaced by the service worker build plugin -const UNHASHED_PRECACHED_ASSETS = []; -const HASHED_PRECACHED_ASSETS = []; -const HASHED_CACHED_ON_REQUEST_ASSETS = []; +const UNHASHED_PRECACHED_ASSETS = DEFINE_UNHASHED_PRECACHED_ASSETS; +const HASHED_PRECACHED_ASSETS = DEFINE_HASHED_PRECACHED_ASSETS; +const HASHED_CACHED_ON_REQUEST_ASSETS = DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS; -const unhashedCacheName = `hydrogen-assets-${HYDROGEN_GLOBAL_HASH}`; +const unhashedCacheName = `hydrogen-assets-${DEFINE_GLOBAL_HASH}`; const hashedCacheName = `hydrogen-assets`; const mediaThumbnailCacheName = `hydrogen-media-thumbnails-v2`; @@ -175,7 +175,7 @@ self.addEventListener('message', (event) => { } else { switch (event.data?.type) { case "version": - reply({version: HYDROGEN_VERSION, buildHash: HYDROGEN_GLOBAL_HASH}); + reply({version: DEFINE_VERSION, buildHash: DEFINE_GLOBAL_HASH}); break; case "skipWaiting": self.skipWaiting(); diff --git a/src/platform/web/ui/login/common.js b/src/platform/web/ui/login/common.js index a247a2ba..948eff34 100644 --- a/src/platform/web/ui/login/common.js +++ b/src/platform/web/ui/login/common.js @@ -15,10 +15,10 @@ limitations under the License. */ export function hydrogenGithubLink(t) { - if (HYDROGEN_VERSION && HYDROGEN_GLOBAL_HASH) { + if (DEFINE_VERSION && DEFINE_GLOBAL_HASH) { return t.a({target: "_blank", - href: `https://github.com/vector-im/hydrogen-web/releases/tag/v${HYDROGEN_VERSION}`}, - `Hydrogen v${HYDROGEN_VERSION} (${HYDROGEN_GLOBAL_HASH}) on Github`); + href: `https://github.com/vector-im/hydrogen-web/releases/tag/v${DEFINE_VERSION}`}, + `Hydrogen v${DEFINE_VERSION} (${DEFINE_GLOBAL_HASH}) on Github`); } else { return t.a({target: "_blank", href: "https://github.com/vector-im/hydrogen-web"}, "Hydrogen on Github"); diff --git a/vite.config.js b/vite.config.js index d9599cc4..6d092052 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,78 +1,78 @@ const cssvariables = require("postcss-css-variables"); -const autoprefixer = require("autoprefixer"); +//const autoprefixer = require("autoprefixer"); const flexbugsFixes = require("postcss-flexbugs-fixes"); const fs = require("fs"); const path = require("path"); -const GLOBAL_HASH_PLACEHOLDER = "hydrogen-global-hash-placeholder-4cf32306-5d61-4262-9a57-c9983f472c3c"; const injectWebManifest = require("./scripts/build-plugins/manifest"); -const injectServiceWorker = require("./scripts/build-plugins/service-worker"); +const {injectServiceWorker, createPlaceholderValues} = require("./scripts/build-plugins/service-worker"); // const legacyBuild = require("./scripts/build-plugins/legacy-build"); - -// we could also just import {version} from "../../package.json" where needed, -// but this won't work in the service worker yet as it is not transformed yet -// TODO: we should emit a chunk early on and then transform the asset again once we know all the other assets to cache +const {defineConfig} = require('vite'); const version = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")).version; -const {defineConfig} = require("vite"); -let polyfillSrc; -let polyfillRef; -export default { - public: false, - root: "src/platform/web", - base: "./", - server: { - hmr: false - }, - resolve: { - alias: { - // these should only be imported by the base-x package in any runtime code - // and works in the browser with a Uint8Array shim, - // rather than including a ton of polyfill code - "safe-buffer": "./scripts/package-overrides/safe-buffer/index.js", - "buffer": "./scripts/package-overrides/buffer/index.js", +export default defineConfig(({mode}) => { + const definePlaceholders = createPlaceholderValues(mode); + return { + public: false, + root: "src/platform/web", + base: "./", + server: { + hmr: false + }, + resolve: { + alias: { + // these should only be imported by the base-x package in any runtime code + // and works in the browser with a Uint8Array shim, + // rather than including a ton of polyfill code + "safe-buffer": "./scripts/package-overrides/safe-buffer/index.js", + "buffer": "./scripts/package-overrides/buffer/index.js", + } + }, + build: { + outDir: "../../../target", + emptyOutDir: true, + minify: false, + sourcemap: false, + assetsInlineLimit: 0, + polyfillModulePreload: false, + }, + plugins: [ + // legacyBuild(scriptTagPath(path.join(__dirname, "src/platform/web/index.html"), 0), { + // "./Platform": "./LegacyPlatform" + // }, "hydrogen-legacy", [ + // './legacy-polyfill', + // ]), + // important this comes before service worker + // otherwise the manifest and the icons it refers to won't be cached + injectWebManifest("assets/manifest.json"), + injectServiceWorker("./src/platform/web/sw.js", ["index.html"], { + // placeholders to replace at end of build by chunk name + "index": {DEFINE_GLOBAL_HASH: definePlaceholders.DEFINE_GLOBAL_HASH}, + "sw": definePlaceholders + }), + ], + define: { + DEFINE_VERSION: JSON.stringify(version), + ...definePlaceholders + }, + css: { + postcss: { + plugins: [ + cssvariables({ + preserve: (declaration) => { + return declaration.value.indexOf("var(--ios-") == 0; + } + }), + // the grid option creates some source fragment that causes the vite warning reporter to crash because + // it wants to log a warning on a line that does not exist in the source fragment. + // autoprefixer({overrideBrowserslist: ["IE 11"], grid: "no-autoplace"}), + flexbugsFixes() + ] + } } - }, - build: { - outDir: "../../../target", - emptyOutDir: true, - minify: true, - sourcemap: false, - assetsInlineLimit: 0, - polyfillModulePreload: false, - }, - plugins: [ - // legacyBuild(scriptTagPath(path.join(__dirname, "src/platform/web/index.html"), 0), { - // "./Platform": "./LegacyPlatform" - // }, "hydrogen-legacy", [ - // './legacy-polyfill', - // ]), - // important this comes before service worker - // otherwise the manifest and the icons it refers to won't be cached - injectWebManifest("assets/manifest.json"), - injectServiceWorker("./src/platform/web/sw.js", ["index.html"], JSON.stringify(GLOBAL_HASH_PLACEHOLDER), ["index", "sw"]), - ], - define: { - "HYDROGEN_VERSION": JSON.stringify(version), - "HYDROGEN_GLOBAL_HASH": JSON.stringify(GLOBAL_HASH_PLACEHOLDER) - }, - css: { - postcss: { - plugins: [ - cssvariables({ - preserve: (declaration) => { - return declaration.value.indexOf("var(--ios-") == 0; - } - }), - // the grid option creates some source fragment that causes the vite warning reporter to crash because - // it wants to log a warning on a line that does not exist in the source fragment. - // autoprefixer({overrideBrowserslist: ["IE 11"], grid: "no-autoplace"}), - flexbugsFixes() - ] - } - } -}; + }; +}); function scriptTagPath(htmlFile, index) { return `${htmlFile}?html-proxy&index=${index}.js`;