Skip to content

Commit

Permalink
Merge pull request #1174 from element-hq/midhun/support-sw-everywhere
Browse files Browse the repository at this point in the history
Support service worker in all environments
  • Loading branch information
MidhunSureshR authored Aug 16, 2024
2 parents 9b68f30 + aa85c4f commit 841ed25
Show file tree
Hide file tree
Showing 12 changed files with 540 additions and 283 deletions.
35 changes: 18 additions & 17 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
module.exports = {
"env": {
"browser": true,
"es6": true
env: {
browser: true,
es6: true,
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
extends: "eslint:recommended",
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
},
"rules": {
rules: {
"no-console": "off",
"no-empty": "off",
"no-prototype-builtins": "off",
"no-unused-vars": "warn"
"no-unused-vars": "warn",
},
"globals": {
"DEFINE_VERSION": "readonly",
"DEFINE_GLOBAL_HASH": "readonly",
"DEFINE_PROJECT_DIR": "readonly",
globals: {
DEFINE_VERSION: "readonly",
DEFINE_GLOBAL_HASH: "readonly",
DEFINE_IS_SDK: "readonly",
DEFINE_PROJECT_DIR: "readonly",
// only available in sw.js
"DEFINE_UNHASHED_PRECACHED_ASSETS": "readonly",
"DEFINE_HASHED_PRECACHED_ASSETS": "readonly",
"DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS": "readonly"
}
DEFINE_UNHASHED_PRECACHED_ASSETS: "readonly",
DEFINE_HASHED_PRECACHED_ASSETS: "readonly",
DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS: "readonly",
},
};
210 changes: 148 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,44 @@ function definePlaceholderValue(mode, name, devValue) {
}
}

/**
* Returns the short sha for the latest git commit
* @see https://stackoverflow.com/a/35778030
*/
function getLatestGitCommitHash() {
try {
return require("child_process")
.execSync("git rev-parse --short HEAD")
.toString()
.trim();
} catch {
return "could_not_fetch_sha";
}
}

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

0 comments on commit 841ed25

Please sign in to comment.