mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 03:25:12 +01:00
keep hashed files in cache as long as they don't change
This commit is contained in:
parent
23ec98fb2f
commit
8dc2816d6e
@ -15,6 +15,7 @@
|
|||||||
<body class="hydrogen">
|
<body class="hydrogen">
|
||||||
<script id="version" type="disabled">
|
<script id="version" type="disabled">
|
||||||
window.HYDROGEN_VERSION = "%%VERSION%%";
|
window.HYDROGEN_VERSION = "%%VERSION%%";
|
||||||
|
window.HYDROGEN_GLOBAL_HASH = "%%GLOBAL_HASH%%";
|
||||||
</script>
|
</script>
|
||||||
<script id="main" type="module">
|
<script id="main" type="module">
|
||||||
import {main} from "./src/main.js";
|
import {main} from "./src/main.js";
|
||||||
|
@ -48,12 +48,10 @@ const cssSrcDir = path.join(projectDir, "src/ui/web/css/");
|
|||||||
|
|
||||||
const program = new commander.Command();
|
const program = new commander.Command();
|
||||||
program
|
program
|
||||||
.option("--no-offline", "make a build without a service worker or appcache manifest")
|
.option("--modern-only", "don't make a legacy build")
|
||||||
program.parse(process.argv);
|
program.parse(process.argv);
|
||||||
const {noOffline} = program;
|
|
||||||
const offline = !noOffline;
|
|
||||||
|
|
||||||
async function build() {
|
async function build({modernOnly}) {
|
||||||
// get version number
|
// get version number
|
||||||
const version = JSON.parse(await fs.readFile(path.join(projectDir, "package.json"), "utf8")).version;
|
const version = JSON.parse(await fs.readFile(path.join(projectDir, "package.json"), "utf8")).version;
|
||||||
|
|
||||||
@ -72,17 +70,22 @@ async function build() {
|
|||||||
const olmAssets = await copyFolder(path.join(projectDir, "lib/olm/"), assets.directory);
|
const olmAssets = await copyFolder(path.join(projectDir, "lib/olm/"), assets.directory);
|
||||||
assets.addSubMap(olmAssets);
|
assets.addSubMap(olmAssets);
|
||||||
await assets.write(`hydrogen.js`, await buildJs("src/main.js"));
|
await assets.write(`hydrogen.js`, await buildJs("src/main.js"));
|
||||||
|
if (!modernOnly) {
|
||||||
await assets.write(`hydrogen-legacy.js`, await buildJsLegacy(["src/main.js", 'src/legacy-polyfill.js', 'src/legacy-extras.js']));
|
await assets.write(`hydrogen-legacy.js`, await buildJsLegacy(["src/main.js", 'src/legacy-polyfill.js', 'src/legacy-extras.js']));
|
||||||
await assets.write(`worker.js`, await buildJsLegacy(["src/worker.js", 'src/worker-polyfill.js']));
|
await assets.write(`worker.js`, await buildJsLegacy(["src/worker.js", 'src/worker-polyfill.js']));
|
||||||
|
}
|
||||||
// creates the directories where the theme css bundles are placed in,
|
// creates the directories where the theme css bundles are placed in,
|
||||||
// and writes to assets, so the build bundles can translate them, so do it first
|
// and writes to assets, so the build bundles can translate them, so do it first
|
||||||
await copyThemeAssets(themes, assets);
|
await copyThemeAssets(themes, assets);
|
||||||
await buildCssBundles(buildCssLegacy, themes, assets);
|
await buildCssBundles(buildCssLegacy, themes, assets);
|
||||||
if (offline) {
|
await buildManifest(assets);
|
||||||
await buildOffline(version, assets);
|
// all assets have been added, create a hash from all assets name to cache unhashed files like index.html by
|
||||||
}
|
const globalHashAssets = Array.from(assets).map(([, resolved]) => resolved);
|
||||||
await buildHtml(doc, version, assets);
|
globalHashAssets.sort();
|
||||||
console.log(`built hydrogen ${version} successfully with ${assets.all().length} files`);
|
const globalHash = contentHash(globalHashAssets.join(","));
|
||||||
|
await buildServiceWorker(globalHash, assets);
|
||||||
|
await buildHtml(doc, version, globalHash, modernOnly, assets);
|
||||||
|
console.log(`built hydrogen ${version} (${globalHash}) successfully with ${assets.size} files`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findThemes(doc, callback) {
|
async function findThemes(doc, callback) {
|
||||||
@ -121,7 +124,7 @@ async function copyThemeAssets(themes, assets) {
|
|||||||
return assets;
|
return assets;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildHtml(doc, version, assets) {
|
async function buildHtml(doc, version, globalHash, modernOnly, assets) {
|
||||||
// transform html file
|
// transform html file
|
||||||
// change path to main.css to css bundle
|
// change path to main.css to css bundle
|
||||||
doc("link[rel=stylesheet]:not([title])").attr("href", assets.resolve(`hydrogen.css`));
|
doc("link[rel=stylesheet]:not([title])").attr("href", assets.resolve(`hydrogen.css`));
|
||||||
@ -130,29 +133,32 @@ async function buildHtml(doc, version, assets) {
|
|||||||
theme.attr("href", assets.resolve(`themes/${themeName}/bundle.css`));
|
theme.attr("href", assets.resolve(`themes/${themeName}/bundle.css`));
|
||||||
});
|
});
|
||||||
const pathsJSON = JSON.stringify({
|
const pathsJSON = JSON.stringify({
|
||||||
worker: assets.resolve(`worker.js`),
|
worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null,
|
||||||
olm: {
|
olm: {
|
||||||
wasm: assets.resolve("olm.wasm"),
|
wasm: assets.resolve("olm.wasm"),
|
||||||
legacyBundle: assets.resolve("olm_legacy.js"),
|
legacyBundle: assets.resolve("olm_legacy.js"),
|
||||||
wasmBundle: assets.resolve("olm.js"),
|
wasmBundle: assets.resolve("olm.js"),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
doc("script#main").replaceWith(
|
const mainScripts = [
|
||||||
`<script type="module">import {main} from "./${assets.resolve(`hydrogen.js`)}"; main(document.body, ${pathsJSON});</script>` +
|
`<script type="module">import {main} from "./${assets.resolve(`hydrogen.js`)}"; main(document.body, ${pathsJSON});</script>`
|
||||||
`<script type="text/javascript" nomodule src="${assets.resolve(`hydrogen-legacy.js`)}"></script>` +
|
];
|
||||||
`<script type="text/javascript" nomodule>hydrogenBundle.main(document.body, ${pathsJSON}, hydrogenBundle.legacyExtras);</script>`);
|
if (!modernOnly) {
|
||||||
removeOrEnableScript(doc("script#service-worker"), offline);
|
mainScripts.push(
|
||||||
|
`<script type="text/javascript" nomodule src="${assets.resolve(`hydrogen-legacy.js`)}"></script>`,
|
||||||
|
`<script type="text/javascript" nomodule>hydrogenBundle.main(document.body, ${pathsJSON}, hydrogenBundle.legacyExtras);</script>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
doc("script#main").replaceWith(mainScripts.join(""));
|
||||||
|
doc("script#service-worker").attr("type", "text/javascript");
|
||||||
|
|
||||||
const versionScript = doc("script#version");
|
const versionScript = doc("script#version");
|
||||||
versionScript.attr("type", "text/javascript");
|
versionScript.attr("type", "text/javascript");
|
||||||
let vSource = versionScript.contents().text();
|
let vSource = versionScript.contents().text();
|
||||||
vSource = vSource.replace(`"%%VERSION%%"`, `"${version}"`);
|
vSource = vSource.replace(`"%%VERSION%%"`, `"${version}"`);
|
||||||
|
vSource = vSource.replace(`"%%GLOBAL_HASH%%"`, `"${globalHash}"`);
|
||||||
versionScript.text(vSource);
|
versionScript.text(vSource);
|
||||||
|
|
||||||
if (offline) {
|
|
||||||
doc("html").attr("manifest", assets.resolve("manifest.appcache"));
|
|
||||||
doc("head").append(`<link rel="manifest" href="${assets.resolve("manifest.json")}">`);
|
doc("head").append(`<link rel="manifest" href="${assets.resolve("manifest.json")}">`);
|
||||||
}
|
|
||||||
await assets.writeUnhashed("index.html", doc.html());
|
await assets.writeUnhashed("index.html", doc.html());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +211,22 @@ async function buildJsLegacy(inputFiles) {
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildOffline(version, assets) {
|
const SERVICEWORKER_NONCACHED_ASSETS = [
|
||||||
|
"hydrogen-legacy.js",
|
||||||
|
"olm_legacy.js",
|
||||||
|
"sw.js",
|
||||||
|
];
|
||||||
|
|
||||||
|
function isPreCached(asset) {
|
||||||
|
return asset.endsWith(".svg") ||
|
||||||
|
asset.endsWith(".png") ||
|
||||||
|
asset.endsWith(".css") ||
|
||||||
|
asset.endsWith(".wasm") ||
|
||||||
|
// most environments don't need the worker
|
||||||
|
asset.endsWith(".js") && asset !== "worker.js";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildManifest(assets) {
|
||||||
const webManifest = JSON.parse(await fs.readFile(path.join(projectDir, "assets/manifest.json"), "utf8"));
|
const webManifest = JSON.parse(await fs.readFile(path.join(projectDir, "assets/manifest.json"), "utf8"));
|
||||||
// copy manifest icons
|
// copy manifest icons
|
||||||
for (const icon of webManifest.icons) {
|
for (const icon of webManifest.icons) {
|
||||||
@ -213,25 +234,33 @@ async function buildOffline(version, assets) {
|
|||||||
const iconTargetPath = path.basename(icon.src);
|
const iconTargetPath = path.basename(icon.src);
|
||||||
icon.src = await assets.write(iconTargetPath, iconData);
|
icon.src = await assets.write(iconTargetPath, iconData);
|
||||||
}
|
}
|
||||||
// write appcache manifest
|
await assets.write("manifest.json", JSON.stringify(webManifest));
|
||||||
const appCacheLines = [
|
}
|
||||||
`CACHE MANIFEST`,
|
|
||||||
`# v${version}`,
|
async function buildServiceWorker(globalHash, assets) {
|
||||||
`NETWORK`,
|
const unhashedPreCachedAssets = ["index.html"];
|
||||||
`"*"`,
|
const hashedPreCachedAssets = [];
|
||||||
`CACHE`,
|
const hashedCachedOnRequestAssets = [];
|
||||||
];
|
|
||||||
appCacheLines.push(...assets.allWithout(["hydrogen.js"]));
|
for (const [unresolved, resolved] of assets) {
|
||||||
const swOfflineFiles = assets.allWithout(["hydrogen-legacy.js", "olm_legacy.js"]);
|
if (SERVICEWORKER_NONCACHED_ASSETS.includes(unresolved)) {
|
||||||
const appCacheManifest = appCacheLines.join("\n") + "\n";
|
continue;
|
||||||
await assets.writeUnhashed("manifest.appcache", appCacheManifest);
|
} else if (unresolved === resolved) {
|
||||||
|
unhashedPreCachedAssets.push(resolved);
|
||||||
|
} else if (isPreCached(unresolved)) {
|
||||||
|
hashedPreCachedAssets.push(resolved);
|
||||||
|
} else {
|
||||||
|
hashedCachedOnRequestAssets.push(resolved);
|
||||||
|
}
|
||||||
|
}
|
||||||
// write service worker
|
// write service worker
|
||||||
let swSource = await fs.readFile(path.join(projectDir, "src/service-worker.template.js"), "utf8");
|
let swSource = await fs.readFile(path.join(projectDir, "src/service-worker.template.js"), "utf8");
|
||||||
swSource = swSource.replace(`"%%VERSION%%"`, `"${version}"`);
|
swSource = swSource.replace(`"%%GLOBAL_HASH%%"`, `"${globalHash}"`);
|
||||||
swSource = swSource.replace(`"%%OFFLINE_FILES%%"`, JSON.stringify(swOfflineFiles));
|
swSource = swSource.replace(`"%%UNHASHED_PRECACHED_ASSETS%%"`, JSON.stringify(unhashedPreCachedAssets));
|
||||||
|
swSource = swSource.replace(`"%%HASHED_PRECACHED_ASSETS%%"`, JSON.stringify(hashedPreCachedAssets));
|
||||||
|
swSource = swSource.replace(`"%%HASHED_CACHED_ON_REQUEST_ASSETS%%"`, JSON.stringify(hashedCachedOnRequestAssets));
|
||||||
// service worker should not have a hashed name as it is polled by the browser for updates
|
// service worker should not have a hashed name as it is polled by the browser for updates
|
||||||
await assets.writeUnhashed("sw.js", swSource);
|
await assets.writeUnhashed("sw.js", swSource);
|
||||||
await assets.write("manifest.json", JSON.stringify(webManifest));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildCssBundles(buildFn, themes, assets) {
|
async function buildCssBundles(buildFn, themes, assets) {
|
||||||
@ -281,14 +310,6 @@ async function buildCssLegacy(entryPath, urlMapper = null) {
|
|||||||
return result.css;
|
return result.css;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeOrEnableScript(scriptNode, enable) {
|
|
||||||
if (enable) {
|
|
||||||
scriptNode.attr("type", "text/javascript");
|
|
||||||
} else {
|
|
||||||
scriptNode.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeDirIfExists(targetDir) {
|
async function removeDirIfExists(targetDir) {
|
||||||
try {
|
try {
|
||||||
await fs.rmdir(targetDir, {recursive: true});
|
await fs.rmdir(targetDir, {recursive: true});
|
||||||
@ -397,16 +418,25 @@ class AssetMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
all() {
|
[Symbol.iterator]() {
|
||||||
return Array.from(this._assets.values());
|
return this._assets.entries();
|
||||||
}
|
}
|
||||||
|
|
||||||
allWithout(excluded) {
|
isUnhashed(relPath) {
|
||||||
excluded = excluded.map(p => this.resolve(p));
|
const resolvedPath = this._assets.get(relPath);
|
||||||
return this.all().filter(p => excluded.indexOf(p) === -1);
|
if (!resolvedPath) {
|
||||||
|
throw new Error("Unknown asset: " + relPath);
|
||||||
|
}
|
||||||
|
return relPath === resolvedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this._assets.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(relPath) {
|
||||||
|
return this._assets.has(relPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
build(program).catch(err => console.error(err));
|
||||||
|
|
||||||
build().catch(err => console.error(err));
|
|
||||||
|
@ -14,38 +14,90 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const VERSION = "%%VERSION%%";
|
const GLOBAL_HASH = "%%GLOBAL_HASH%%";
|
||||||
const OFFLINE_FILES = "%%OFFLINE_FILES%%";
|
const UNHASHED_PRECACHED_ASSETS = "%%UNHASHED_PRECACHED_ASSETS%%";
|
||||||
// TODO: cache these files when requested
|
const HASHED_PRECACHED_ASSETS = "%%HASHED_PRECACHED_ASSETS%%";
|
||||||
// The difficulty is that these are relative filenames, and we don't have access to document.baseURI
|
const HASHED_CACHED_ON_REQUEST_ASSETS = "%%HASHED_CACHED_ON_REQUEST_ASSETS%%";
|
||||||
// Clients.match({type: "window"}).url and assume they are all the same? they really should be ... safari doesn't support this though
|
const unhashedCacheName = `hydrogen-assets-${GLOBAL_HASH}`;
|
||||||
const CACHE_FILES = "%%CACHE_FILES%%";
|
const hashedCacheName = `hydrogen-assets`;
|
||||||
const cacheName = `hydrogen-${VERSION}`;
|
|
||||||
|
|
||||||
self.addEventListener('install', function(e) {
|
self.addEventListener('install', function(e) {
|
||||||
e.waitUntil(
|
e.waitUntil((async () => {
|
||||||
caches.open(cacheName).then(function(cache) {
|
const unhashedCache = await caches.open(unhashedCacheName);
|
||||||
return cache.addAll(OFFLINE_FILES);
|
await unhashedCache.addAll(UNHASHED_PRECACHED_ASSETS);
|
||||||
})
|
const hashedCache = await caches.open(hashedCacheName);
|
||||||
);
|
await Promise.all(HASHED_PRECACHED_ASSETS.map(async asset => {
|
||||||
});
|
if (!await hashedCache.match(asset)) {
|
||||||
|
await hashedCache.add(asset);
|
||||||
self.addEventListener('activate', (event) => {
|
|
||||||
event.waitUntil(
|
|
||||||
caches.keys().then((keyList) => {
|
|
||||||
return Promise.all(keyList.map((key) => {
|
|
||||||
if (key !== cacheName) {
|
|
||||||
return caches.delete(key);
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
})
|
})());
|
||||||
);
|
});
|
||||||
|
|
||||||
|
async function purgeOldCaches() {
|
||||||
|
// remove any caches we don't know about
|
||||||
|
const keyList = await caches.keys();
|
||||||
|
for (const key of keyList) {
|
||||||
|
if (key !== unhashedCacheName && key !== hashedCacheName) {
|
||||||
|
await caches.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove the cache for any old hashed resource
|
||||||
|
const hashedCache = await caches.open(hashedCacheName);
|
||||||
|
const keys = await hashedCache.keys();
|
||||||
|
const hashedAssetURLs =
|
||||||
|
HASHED_PRECACHED_ASSETS
|
||||||
|
.concat(HASHED_CACHED_ON_REQUEST_ASSETS)
|
||||||
|
.map(a => new URL(a, self.registration.scope).href);
|
||||||
|
|
||||||
|
for (const request of keys) {
|
||||||
|
if (!hashedAssetURLs.some(url => url === request.url)) {
|
||||||
|
hashedCache.delete(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(purgeOldCaches());
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('fetch', (event) => {
|
self.addEventListener('fetch', (event) => {
|
||||||
event.respondWith(
|
event.respondWith(handleRequest(event.request));
|
||||||
caches.open(cacheName)
|
|
||||||
.then(cache => cache.match(event.request))
|
|
||||||
.then((response) => response || fetch(event.request))
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function handleRequest(request) {
|
||||||
|
const baseURL = self.registration.scope;
|
||||||
|
if (request.url === baseURL) {
|
||||||
|
request = new Request(new URL("index.html", baseURL));
|
||||||
|
}
|
||||||
|
let response = await readCache(request);
|
||||||
|
if (!response) {
|
||||||
|
response = await fetch(request);
|
||||||
|
await updateCache(request, response);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateCache(request, response) {
|
||||||
|
const baseURL = self.registration.scope;
|
||||||
|
if(!request.url.startsWith(baseURL)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let assetName = request.url.substr(baseURL.length);
|
||||||
|
if (HASHED_CACHED_ON_REQUEST_ASSETS.includes(assetName)) {
|
||||||
|
const cache = await caches.open(hashedCacheName);
|
||||||
|
await cache.put(request, response.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readCache(request) {
|
||||||
|
const unhashedCache = await caches.open(unhashedCacheName);
|
||||||
|
let response = await unhashedCache.match(request);
|
||||||
|
if (response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
const hashedCache = await caches.open(hashedCacheName);
|
||||||
|
response = await hashedCache.match(request);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export function hydrogenGithubLink(t) {
|
|||||||
if (window.HYDROGEN_VERSION) {
|
if (window.HYDROGEN_VERSION) {
|
||||||
return t.a({target: "_blank",
|
return t.a({target: "_blank",
|
||||||
href: `https://github.com/vector-im/hydrogen-web/releases/tag/v${window.HYDROGEN_VERSION}`},
|
href: `https://github.com/vector-im/hydrogen-web/releases/tag/v${window.HYDROGEN_VERSION}`},
|
||||||
`Hydrogen v${window.HYDROGEN_VERSION} on Github`);
|
`Hydrogen v${window.HYDROGEN_VERSION} (${window.HYDROGEN_GLOBAL_HASH}) on Github`);
|
||||||
} else {
|
} else {
|
||||||
return t.a({target: "_blank", href: "https://github.com/vector-im/hydrogen-web"},
|
return t.a({target: "_blank", href: "https://github.com/vector-im/hydrogen-web"},
|
||||||
"Hydrogen on Github");
|
"Hydrogen on Github");
|
||||||
|
Loading…
Reference in New Issue
Block a user