Skip to content

Commit

Permalink
Make service worker work with sdk
Browse files Browse the repository at this point in the history
  • Loading branch information
MidhunSureshR committed Aug 16, 2024
1 parent 3967d26 commit 8f7d94e
Show file tree
Hide file tree
Showing 7 changed files with 333 additions and 200 deletions.
206 changes: 144 additions & 62 deletions scripts/build-plugins/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,63 +1,91 @@
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);
hasher.update(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",
fileName: swName,
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();
Expand All @@ -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}`
);
}
}
}
Expand All @@ -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.
Expand All @@ -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 };
4 changes: 2 additions & 2 deletions scripts/sdk/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions src/platform/web/dom/ServiceWorkerHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Check failure on line 111 in src/platform/web/dom/ServiceWorkerHandler.js

View workflow job for this annotation

GitHub Actions / build (18.1.0)

'DEFINE_IS_SDK' is not defined
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
Expand Down
Loading

0 comments on commit 8f7d94e

Please sign in to comment.