mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-22 19:14:52 +01:00
244 lines
7.7 KiB
JavaScript
244 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() {
|
|
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",
|
|
`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 };
|