vector-im-hydrogen-web/scripts/build-plugins/service-worker.js
2024-08-16 15:33:39 +05:30

240 lines
7.7 KiB
JavaScript

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
) {
const swName = path.basename(swFile);
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) => {
mode = config.mode;
version = JSON.parse(config.define.DEFINE_VERSION); // unquote
logger = config.logger;
},
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 assets = Object.values(bundle);
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,
mode
),
};
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 globalHashAssets = hashedFileNames.concat(unhashedHashes);
globalHashAssets.sort();
return contentHash(globalHashAssets.join(",")).toString();
}
const NON_PRECACHED_JS = [
"hydrogen-legacy",
"olm_legacy.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)))
);
}
function getCacheFileNamePlaceholderValues(
swName,
unhashedFilenames,
assets,
mode
) {
const unhashedPreCachedAssets = [];
const hashedPreCachedAssets = [];
const hashedCachedOnRequestAssets = [];
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_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}`
);
}
}
}
}
/** 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" || 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.
// I first considered Symbol but it's not inconceivable that babel would transform this.
return `prompt(${JSON.stringify(name)})`;
} else {
return JSON.stringify(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",
`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 };