Make service worker work with sdk

This commit is contained in:
R Midhun Suresh 2024-08-16 15:33:39 +05:30
parent 3967d26f9d
commit 8f7d94eb75
No known key found for this signature in database
7 changed files with 333 additions and 200 deletions

View File

@ -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 };

View File

@ -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

View File

@ -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

View File

@ -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 };

View File

@ -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),
});
});

View File

@ -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,
}),
],
});
});

View File

@ -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`,
},
/* 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"
// }
}
},
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 + "/")
);
},
},
},
});
});