diff --git a/scripts/build-plugins/service-worker.js b/scripts/build-plugins/service-worker.js index 856195453a..9939440d1b 100644 --- a/scripts/build-plugins/service-worker.js +++ b/scripts/build-plugins/service-worker.js @@ -1,6 +1,5 @@ -const fs = require('fs/promises'); -const path = require('path'); -const xxhash = require('xxhashjs'); +const path = require("path"); +const xxhash = require("xxhashjs"); function contentHash(str) { var hasher = new xxhash.h32(0); @@ -8,16 +7,21 @@ function contentHash(str) { return hasher.digest(); } -function injectServiceWorker(swFile, findUnhashedFileNamesFromBundle, placeholdersPerChunk) { +function injectServiceWorker( + swFile, + findUnhashedFileNamesFromBundle, + placeholdersPerChunk +) { const swName = path.basename(swFile); - let root; let version; let logger; + let mode; return { name: "hydrogen:injectServiceWorker", apply: "build", enforce: "post", + buildStart() { this.emitFile({ type: "chunk", @@ -25,39 +29,63 @@ function injectServiceWorker(swFile, findUnhashedFileNamesFromBundle, placeholde id: swFile, }); }, - configResolved: config => { - root = config.root; + + configResolved: (config) => { + mode = config.mode; version = JSON.parse(config.define.DEFINE_VERSION); // unquote logger = config.logger; }, - generateBundle: async function(options, bundle) { + + generateBundle: async function (options, bundle) { const otherUnhashedFiles = findUnhashedFileNamesFromBundle(bundle); const unhashedFilenames = [swName].concat(otherUnhashedFiles); - const unhashedFileContentMap = unhashedFilenames.reduce((map, fileName) => { - const chunkOrAsset = bundle[fileName]; - if (!chunkOrAsset) { - throw new Error("could not get content for uncached asset or chunk " + fileName); - } - map[fileName] = chunkOrAsset.source || chunkOrAsset.code; - return map; - }, {}); + const unhashedFileContentMap = unhashedFilenames.reduce( + (map, fileName) => { + const chunkOrAsset = bundle[fileName]; + if (!chunkOrAsset) { + throw new Error( + "could not get content for uncached asset or chunk " + + fileName + ); + } + map[fileName] = chunkOrAsset.source || chunkOrAsset.code; + return map; + }, + {} + ); const assets = Object.values(bundle); - const hashedFileNames = assets.map(o => o.fileName).filter(fileName => !unhashedFileContentMap[fileName]); - const globalHash = getBuildHash(hashedFileNames, unhashedFileContentMap); + const hashedFileNames = assets + .map((o) => o.fileName) + .filter((fileName) => !unhashedFileContentMap[fileName]); + const globalHash = getBuildHash( + hashedFileNames, + unhashedFileContentMap + ); const placeholderValues = { DEFINE_GLOBAL_HASH: `"${globalHash}"`, - ...getCacheFileNamePlaceholderValues(swName, unhashedFilenames, assets, placeholdersPerChunk) + ...getCacheFileNamePlaceholderValues( + swName, + unhashedFilenames, + assets, + mode + ), }; - replacePlaceholdersInChunks(assets, placeholdersPerChunk, placeholderValues); + replacePlaceholdersInChunks( + assets, + placeholdersPerChunk, + placeholderValues + ); logger.info(`\nBuilt ${version} (${globalHash})`); - } + }, }; } function getBuildHash(hashedFileNames, unhashedFileContentMap) { - const unhashedHashes = Object.entries(unhashedFileContentMap).map(([fileName, content]) => { - return `${fileName}-${contentHash(Buffer.from(content))}`; - }); + const unhashedHashes = Object.entries(unhashedFileContentMap).map( + ([fileName, content]) => { + return `${fileName}-${contentHash(Buffer.from(content))}`; + } + ); const globalHashAssets = hashedFileNames.concat(unhashedHashes); globalHashAssets.sort(); return contentHash(globalHashAssets.join(",")).toString(); @@ -66,60 +94,87 @@ function getBuildHash(hashedFileNames, unhashedFileContentMap) { const NON_PRECACHED_JS = [ "hydrogen-legacy", "olm_legacy.js", - // most environments don't need the worker - "main.js" + // most environments don't need the worker + "main.js", ]; function isPreCached(asset) { - const {name, fileName} = asset; - return name.endsWith(".svg") || - name.endsWith(".png") || - name.endsWith(".css") || - name.endsWith(".wasm") || - name.endsWith(".html") || - // the index and vendor chunks don't have an extension in `name`, so check extension on `fileName` - fileName.endsWith(".js") && !NON_PRECACHED_JS.includes(path.basename(name)); + const { name, fileName } = asset; + return ( + name?.endsWith(".svg") || + name?.endsWith(".png") || + name?.endsWith(".css") || + name?.endsWith(".wasm") || + name?.endsWith(".html") || + // the index and vendor chunks don't have an extension in `name`, so check extension on `fileName` + (fileName.endsWith(".js") && + !NON_PRECACHED_JS.includes(path.basename(name))) + ); } -function getCacheFileNamePlaceholderValues(swName, unhashedFilenames, assets) { +function getCacheFileNamePlaceholderValues( + swName, + unhashedFilenames, + assets, + mode +) { const unhashedPreCachedAssets = []; const hashedPreCachedAssets = []; const hashedCachedOnRequestAssets = []; - for (const asset of assets) { - const {name, fileName} = asset; - // the service worker should not be cached at all, - // it's how updates happen - if (fileName === swName) { - continue; - } else if (unhashedFilenames.includes(fileName)) { - unhashedPreCachedAssets.push(fileName); - } else if (isPreCached(asset)) { - hashedPreCachedAssets.push(fileName); - } else { - hashedCachedOnRequestAssets.push(fileName); + if (mode === "production") { + for (const asset of assets) { + const { name, fileName } = asset; + // the service worker should not be cached at all, + // it's how updates happen + if (fileName === swName) { + continue; + } else if (unhashedFilenames.includes(fileName)) { + unhashedPreCachedAssets.push(fileName); + } else if (isPreCached(asset)) { + hashedPreCachedAssets.push(fileName); + } else { + hashedCachedOnRequestAssets.push(fileName); + } } } return { - DEFINE_UNHASHED_PRECACHED_ASSETS: JSON.stringify(unhashedPreCachedAssets), + DEFINE_UNHASHED_PRECACHED_ASSETS: JSON.stringify( + unhashedPreCachedAssets + ), DEFINE_HASHED_PRECACHED_ASSETS: JSON.stringify(hashedPreCachedAssets), - DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS: JSON.stringify(hashedCachedOnRequestAssets) - } + DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS: JSON.stringify( + hashedCachedOnRequestAssets + ), + }; } -function replacePlaceholdersInChunks(assets, placeholdersPerChunk, placeholderValues) { +function replacePlaceholdersInChunks( + assets, + placeholdersPerChunk, + placeholderValues +) { for (const [name, placeholderMap] of Object.entries(placeholdersPerChunk)) { - const chunk = assets.find(a => a.type === "chunk" && a.name === name); + const chunk = assets.find((a) => a.type === "chunk" && a.name === name); if (!chunk) { - throw new Error(`could not find chunk ${name} to replace placeholders`); + throw new Error( + `could not find chunk ${name} to replace placeholders` + ); } - for (const [placeholderName, placeholderLiteral] of Object.entries(placeholderMap)) { + for (const [placeholderName, placeholderLiteral] of Object.entries( + placeholderMap + )) { const replacedValue = placeholderValues[placeholderName]; const oldCode = chunk.code; - chunk.code = chunk.code.replaceAll(placeholderLiteral, replacedValue); + 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}`); + throw new Error( + `Could not replace ${placeholderName} in ${name}, looking for literal ${placeholderLiteral}` + ); } } } @@ -134,7 +189,7 @@ function replacePlaceholdersInChunks(assets, placeholdersPerChunk, placeholderVa * 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") { + if (mode === "production" || mode === "sdk") { // 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. @@ -145,13 +200,40 @@ function definePlaceholderValue(mode, name, devValue) { } } +/** + * Returns the short sha for the latest git commit + * @see https://stackoverflow.com/a/35778030 + */ +function getLatestGitCommitHash() { + return require("child_process") + .execSync("git rev-parse --short HEAD") + .toString() + .trim(); +} + 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", []), + DEFINE_GLOBAL_HASH: definePlaceholderValue( + mode, + "DEFINE_GLOBAL_HASH", + `git commit: ${getLatestGitCommitHash()}` + ), + 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}; +module.exports = { injectServiceWorker, createPlaceholderValues }; diff --git a/scripts/sdk/build.sh b/scripts/sdk/build.sh index 68c0e47285..9f063ac085 100755 --- a/scripts/sdk/build.sh +++ b/scripts/sdk/build.sh @@ -8,8 +8,8 @@ shopt -s extglob # Only remove the directory contents instead of the whole directory to maintain # the `npm link`/`yarn link` symlink rm -rf target/* -yarn run vite build -c vite.sdk-assets-config.js -yarn run vite build -c vite.sdk-lib-config.js +yarn run vite build -c vite.sdk-assets-config.js --mode sdk +yarn run vite build -c vite.sdk-lib-config.js --mode sdk yarn tsc -p tsconfig-declaration.json ./scripts/sdk/create-manifest.js ./target/package.json mkdir target/paths diff --git a/src/platform/web/dom/ServiceWorkerHandler.js b/src/platform/web/dom/ServiceWorkerHandler.js index 0bb8c7256b..4b92d41375 100644 --- a/src/platform/web/dom/ServiceWorkerHandler.js +++ b/src/platform/web/dom/ServiceWorkerHandler.js @@ -103,8 +103,22 @@ export class ServiceWorkerHandler { if (document.hidden) { return; } - const version = await this._sendAndWaitForReply("version", null, this._registration.waiting); - if (confirm(`Version ${version.version} (${version.buildHash}) is available. Reload to apply?`)) { + const version = await this._sendAndWaitForReply( + "version", + null, + this._registration.waiting + ); + const isSdk = DEFINE_IS_SDK; + const isDev = this.version === "develop"; + // Don't ask for confirmation when being used as an sdk/ when being run in dev server + if ( + isSdk || + isDev || + confirm( + `Version ${version.version} (${version.buildHash}) is available. Reload to apply?` + ) + ) { + console.log("Service Worker has been updated!"); // prevent any fetch requests from going to the service worker // from any client, so that it is not kept active // when calling skipWaiting on the new one diff --git a/vite.common-config.js b/vite.common-config.js index 2fa09d4692..adb35852bb 100644 --- a/vite.common-config.js +++ b/vite.common-config.js @@ -1,59 +1,78 @@ -const cssvariables = require("postcss-css-variables"); +const { + createPlaceholderValues, +} = require("./scripts/build-plugins/service-worker"); const flexbugsFixes = require("postcss-flexbugs-fixes"); const compileVariables = require("./scripts/postcss/css-compile-variables"); const urlVariables = require("./scripts/postcss/css-url-to-variables"); const urlProcessor = require("./scripts/postcss/css-url-processor"); -const fs = require("fs"); -const path = require("path"); -const manifest = require("./package.json"); -const version = manifest.version; +const appManifest = require("./package.json"); +const sdkManifest = require("./scripts/sdk/base-manifest.json"); const compiledVariables = new Map(); -import {buildColorizedSVG as replacer} from "./scripts/postcss/svg-builder.mjs"; -import {derive} from "./src/platform/web/theming/shared/color.mjs"; +import { buildColorizedSVG as replacer } from "./scripts/postcss/svg-builder.mjs"; +import { derive } from "./src/platform/web/theming/shared/color.mjs"; -const commonOptions = { - logLevel: "warn", - publicDir: false, - 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: { - emptyOutDir: true, - assetsInlineLimit: 0, - polyfillModulePreload: false, - }, - assetsInclude: ['**/config.json'], - define: { - DEFINE_VERSION: JSON.stringify(version), - DEFINE_GLOBAL_HASH: JSON.stringify(null), - }, - css: { - postcss: { - plugins: [ - compileVariables({derive, compiledVariables}), - urlVariables({compiledVariables}), - urlProcessor({replacer}), - // 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() - ] - } - } +const commonOptions = (mode) => { + const definePlaceholders = createPlaceholderValues(mode); + return { + logLevel: "warn", + publicDir: false, + 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: { + emptyOutDir: true, + assetsInlineLimit: 0, + polyfillModulePreload: false, + }, + assetsInclude: ["**/config.json"], + define: Object.assign( + { + DEFINE_VERSION: `"${getVersion(mode)}"`, + DEFINE_GLOBAL_HASH: JSON.stringify(null), + DEFINE_IS_SDK: mode === "sdk" ? "true" : "false", + DEFINE_PROJECT_DIR: JSON.stringify(__dirname), + }, + definePlaceholders + ), + css: { + postcss: { + plugins: [ + compileVariables({ derive, compiledVariables }), + urlVariables({ compiledVariables }), + urlProcessor({ replacer }), + flexbugsFixes(), + ], + }, + }, + }; }; +/** + * Get the version for this build + * @param mode Vite mode for this build + * @returns string representing version + */ +function getVersion(mode) { + if (mode === "production") { + // This is an app build, so return the version from root/package.json + return appManifest.version; + } else if (mode === "sdk") { + // For the sdk build, return version from base-manifest.json + return sdkManifest.version; + } else { + // For the develop server + return "develop"; + } +} + module.exports = { commonOptions, compiledVariables }; diff --git a/vite.config.js b/vite.config.js index 0bbeb4d44b..6c36ee2cd7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,15 +1,20 @@ const injectWebManifest = require("./scripts/build-plugins/manifest"); const {injectServiceWorker, createPlaceholderValues} = require("./scripts/build-plugins/service-worker"); +const { + transformServiceWorkerInDevServer, +} = require("./scripts/build-plugins/sw-dev"); const themeBuilder = require("./scripts/build-plugins/rollup-plugin-build-themes"); -const {defineConfig} = require('vite'); -const mergeOptions = require('merge-options').bind({concatArrays: true}); -const {commonOptions, compiledVariables} = require("./vite.common-config.js"); +const { defineConfig } = require("vite"); +const mergeOptions = require("merge-options").bind({ concatArrays: true }); +const { commonOptions, compiledVariables } = require("./vite.common-config.js"); -export default defineConfig(({mode}) => { +export default defineConfig(({ mode }) => { const definePlaceholders = createPlaceholderValues(mode); - return mergeOptions(commonOptions, { + const options = commonOptions(mode); + return mergeOptions(options, { root: "src/platform/web", base: "./", + publicDir: "./public", build: { outDir: "../../../target", minify: true, @@ -19,18 +24,17 @@ export default defineConfig(({mode}) => { assetFileNames: (asset) => { if (asset.name.includes("config.json")) { return "[name][extname]"; - } - else if (asset.name.match(/theme-.+\.json/)) { + } else if (asset.name.match(/theme-.+\.json/)) { return "assets/[name][extname]"; - } - else { + } else { return "assets/[name].[hash][extname]"; } - } + }, }, }, }, plugins: [ + transformServiceWorkerInDevServer(), themeBuilder({ themeConfig: { themes: ["./src/platform/web/ui/css/themes/element"], @@ -41,17 +45,19 @@ export default defineConfig(({mode}) => { // 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", findUnhashedFileNamesFromBundle, { - // placeholders to replace at end of build by chunk name - index: { - DEFINE_GLOBAL_HASH: definePlaceholders.DEFINE_GLOBAL_HASH, - }, - sw: definePlaceholders, - }), + injectServiceWorker( + "./src/platform/web/sw.js", + findUnhashedFileNamesFromBundle, + { + // placeholders to replace at end of build by chunk name + index: { + DEFINE_GLOBAL_HASH: + definePlaceholders.DEFINE_GLOBAL_HASH, + }, + sw: definePlaceholders, + } + ), ], - define: Object.assign({ - DEFINE_PROJECT_DIR: JSON.stringify(__dirname) - }, definePlaceholders), }); }); diff --git a/vite.sdk-assets-config.js b/vite.sdk-assets-config.js index 5c1f319622..cd97cee711 100644 --- a/vite.sdk-assets-config.js +++ b/vite.sdk-assets-config.js @@ -1,7 +1,8 @@ const path = require("path"); -const mergeOptions = require('merge-options'); +const mergeOptions = require("merge-options").bind({ concatArrays: true }); const themeBuilder = require("./scripts/build-plugins/rollup-plugin-build-themes"); -const {commonOptions, compiledVariables} = require("./vite.common-config.js"); +const { commonOptions, compiledVariables } = require("./vite.common-config.js"); +const { defineConfig } = require("vite"); // These paths will be saved without their hash so they have a consisent path // that we can reference in our `package.json` `exports`. And so people can import @@ -13,33 +14,40 @@ const pathsToExport = [ "theme-element-dark.css", ]; -export default mergeOptions(commonOptions, { - root: "src/", - base: "./", - build: { - outDir: "../target/asset-build/", - rollupOptions: { - output: { - assetFileNames: (chunkInfo) => { - // Get rid of the hash so we can consistently reference these - // files in our `package.json` `exports`. And so people can - // import them with a consistent path. - if(pathsToExport.includes(path.basename(chunkInfo.name))) { - return "assets/[name].[ext]"; - } +export default defineConfig(({ mode }) => { + const options = commonOptions(mode); + return mergeOptions(options, { + root: "src/", + base: "./", + build: { + outDir: "../target/asset-build/", + rollupOptions: { + output: { + assetFileNames: (chunkInfo) => { + // Get rid of the hash so we can consistently reference these + // files in our `package.json` `exports`. And so people can + // import them with a consistent path. + if ( + pathsToExport.includes( + path.basename(chunkInfo.name) + ) + ) { + return "assets/[name].[ext]"; + } - return "assets/[name]-[hash][extname]"; - } - } - } - }, - plugins: [ - themeBuilder({ - themeConfig: { - themes: ["./src/platform/web/ui/css/themes/element"], - default: "element", + return "assets/[name]-[hash][extname]"; + }, + }, }, - compiledVariables, - }), - ], + }, + plugins: [ + themeBuilder({ + themeConfig: { + themes: ["./src/platform/web/ui/css/themes/element"], + default: "element", + }, + compiledVariables, + }), + ], + }); }); diff --git a/vite.sdk-lib-config.js b/vite.sdk-lib-config.js index a5f11a5306..2689da09c5 100644 --- a/vite.sdk-lib-config.js +++ b/vite.sdk-lib-config.js @@ -1,7 +1,12 @@ const path = require("path"); -const mergeOptions = require('merge-options'); -const {commonOptions} = require("./vite.common-config.js"); +const { defineConfig } = require("vite"); +const mergeOptions = require("merge-options").bind({ concatArrays: true }); +const { commonOptions } = require("./vite.common-config.js"); const manifest = require("./package.json"); +const { + injectServiceWorker, + createPlaceholderValues, +} = require("./scripts/build-plugins/service-worker"); const externalDependencies = Object.keys(manifest.dependencies) // just in case for safety in case fake-indexeddb wouldn't be @@ -9,39 +14,38 @@ const externalDependencies = Object.keys(manifest.dependencies) .concat(Object.keys(manifest.devDependencies)) // bundle bs58 because it uses buffer indirectly, which is a pain to bundle, // so we don't annoy our library users with it. - .filter(d => d !== "bs58"); -const moduleDir = path.join(__dirname, "node_modules"); + .filter((d) => d !== "bs58"); -export default mergeOptions(commonOptions, { - root: "src/", - build: { - lib: { - entry: path.resolve(__dirname, 'src/lib.ts'), - formats: ["cjs", "es"], - fileName: format => `hydrogen.${format}.js`, - }, - minify: false, - sourcemap: false, - outDir: "../target/lib-build", - // don't bundle any dependencies, they should be imported/required - rollupOptions: { - external(id) { - return externalDependencies.some(d => id === d || id.startsWith(d + "/")); +export default defineConfig(({ mode }) => { + const options = commonOptions(mode); + const definePlaceholders = createPlaceholderValues(mode); + return mergeOptions(options, { + root: "src/", + plugins: [ + injectServiceWorker("./src/platform/web/sw.js", () => [], { + lib: { + DEFINE_GLOBAL_HASH: definePlaceholders.DEFINE_GLOBAL_HASH, + }, + sw: definePlaceholders, + }), + ], + build: { + lib: { + entry: path.resolve(__dirname, "src/lib.ts"), + formats: ["cjs", "es"], + fileName: (format) => `hydrogen.${format}.js`, + }, + minify: false, + sourcemap: false, + outDir: "../target/lib-build", + // don't bundle any dependencies, they should be imported/required + rollupOptions: { + external(id) { + return externalDependencies.some( + (d) => id === d || id.startsWith(d + "/") + ); + }, }, - /* don't bundle, so we can override imports per file at build time to replace components */ - // output: { - // manualChunks: (id) => { - // if (id.startsWith(srcDir)) { - // const idPath = id.substring(srcDir.length); - // const pathWithoutExt = idPath.substring(0, idPath.lastIndexOf(".")); - // return pathWithoutExt; - // } else { - // return "index"; - // } - // }, - // minifyInternalExports: false, - // chunkFileNames: "[format]/[name].js" - // } - } - }, + }, + }); });