keep hashed files in cache as long as they don't change
This commit is contained in:
parent
23ec98fb2f
commit
8dc2816d6e
4 changed files with 165 additions and 82 deletions
|
@ -15,6 +15,7 @@
|
|||
<body class="hydrogen">
|
||||
<script id="version" type="disabled">
|
||||
window.HYDROGEN_VERSION = "%%VERSION%%";
|
||||
window.HYDROGEN_GLOBAL_HASH = "%%GLOBAL_HASH%%";
|
||||
</script>
|
||||
<script id="main" type="module">
|
||||
import {main} from "./src/main.js";
|
||||
|
|
|
@ -48,12 +48,10 @@ const cssSrcDir = path.join(projectDir, "src/ui/web/css/");
|
|||
|
||||
const program = new commander.Command();
|
||||
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);
|
||||
const {noOffline} = program;
|
||||
const offline = !noOffline;
|
||||
|
||||
async function build() {
|
||||
async function build({modernOnly}) {
|
||||
// get version number
|
||||
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);
|
||||
assets.addSubMap(olmAssets);
|
||||
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(`worker.js`, await buildJsLegacy(["src/worker.js", 'src/worker-polyfill.js']));
|
||||
}
|
||||
// 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
|
||||
await copyThemeAssets(themes, assets);
|
||||
await buildCssBundles(buildCssLegacy, themes, assets);
|
||||
if (offline) {
|
||||
await buildOffline(version, assets);
|
||||
}
|
||||
await buildHtml(doc, version, assets);
|
||||
console.log(`built hydrogen ${version} successfully with ${assets.all().length} files`);
|
||||
await buildManifest(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);
|
||||
globalHashAssets.sort();
|
||||
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) {
|
||||
|
@ -121,7 +124,7 @@ async function copyThemeAssets(themes, assets) {
|
|||
return assets;
|
||||
}
|
||||
|
||||
async function buildHtml(doc, version, assets) {
|
||||
async function buildHtml(doc, version, globalHash, modernOnly, assets) {
|
||||
// transform html file
|
||||
// change path to main.css to css bundle
|
||||
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`));
|
||||
});
|
||||
const pathsJSON = JSON.stringify({
|
||||
worker: assets.resolve(`worker.js`),
|
||||
worker: assets.has("worker.js") ? assets.resolve(`worker.js`) : null,
|
||||
olm: {
|
||||
wasm: assets.resolve("olm.wasm"),
|
||||
legacyBundle: assets.resolve("olm_legacy.js"),
|
||||
wasmBundle: assets.resolve("olm.js"),
|
||||
}
|
||||
});
|
||||
doc("script#main").replaceWith(
|
||||
`<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>`);
|
||||
removeOrEnableScript(doc("script#service-worker"), offline);
|
||||
const mainScripts = [
|
||||
`<script type="module">import {main} from "./${assets.resolve(`hydrogen.js`)}"; main(document.body, ${pathsJSON});</script>`
|
||||
];
|
||||
if (!modernOnly) {
|
||||
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");
|
||||
versionScript.attr("type", "text/javascript");
|
||||
let vSource = versionScript.contents().text();
|
||||
vSource = vSource.replace(`"%%VERSION%%"`, `"${version}"`);
|
||||
vSource = vSource.replace(`"%%GLOBAL_HASH%%"`, `"${globalHash}"`);
|
||||
versionScript.text(vSource);
|
||||
|
||||
if (offline) {
|
||||
doc("html").attr("manifest", assets.resolve("manifest.appcache"));
|
||||
doc("head").append(`<link rel="manifest" href="${assets.resolve("manifest.json")}">`);
|
||||
}
|
||||
await assets.writeUnhashed("index.html", doc.html());
|
||||
}
|
||||
|
||||
|
@ -205,7 +211,22 @@ async function buildJsLegacy(inputFiles) {
|
|||
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"));
|
||||
// copy manifest icons
|
||||
for (const icon of webManifest.icons) {
|
||||
|
@ -213,25 +234,33 @@ async function buildOffline(version, assets) {
|
|||
const iconTargetPath = path.basename(icon.src);
|
||||
icon.src = await assets.write(iconTargetPath, iconData);
|
||||
}
|
||||
// write appcache manifest
|
||||
const appCacheLines = [
|
||||
`CACHE MANIFEST`,
|
||||
`# v${version}`,
|
||||
`NETWORK`,
|
||||
`"*"`,
|
||||
`CACHE`,
|
||||
];
|
||||
appCacheLines.push(...assets.allWithout(["hydrogen.js"]));
|
||||
const swOfflineFiles = assets.allWithout(["hydrogen-legacy.js", "olm_legacy.js"]);
|
||||
const appCacheManifest = appCacheLines.join("\n") + "\n";
|
||||
await assets.writeUnhashed("manifest.appcache", appCacheManifest);
|
||||
await assets.write("manifest.json", JSON.stringify(webManifest));
|
||||
}
|
||||
|
||||
async function buildServiceWorker(globalHash, assets) {
|
||||
const unhashedPreCachedAssets = ["index.html"];
|
||||
const hashedPreCachedAssets = [];
|
||||
const hashedCachedOnRequestAssets = [];
|
||||
|
||||
for (const [unresolved, resolved] of assets) {
|
||||
if (SERVICEWORKER_NONCACHED_ASSETS.includes(unresolved)) {
|
||||
continue;
|
||||
} else if (unresolved === resolved) {
|
||||
unhashedPreCachedAssets.push(resolved);
|
||||
} else if (isPreCached(unresolved)) {
|
||||
hashedPreCachedAssets.push(resolved);
|
||||
} else {
|
||||
hashedCachedOnRequestAssets.push(resolved);
|
||||
}
|
||||
}
|
||||
// write service worker
|
||||
let swSource = await fs.readFile(path.join(projectDir, "src/service-worker.template.js"), "utf8");
|
||||
swSource = swSource.replace(`"%%VERSION%%"`, `"${version}"`);
|
||||
swSource = swSource.replace(`"%%OFFLINE_FILES%%"`, JSON.stringify(swOfflineFiles));
|
||||
swSource = swSource.replace(`"%%GLOBAL_HASH%%"`, `"${globalHash}"`);
|
||||
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
|
||||
await assets.writeUnhashed("sw.js", swSource);
|
||||
await assets.write("manifest.json", JSON.stringify(webManifest));
|
||||
}
|
||||
|
||||
async function buildCssBundles(buildFn, themes, assets) {
|
||||
|
@ -281,14 +310,6 @@ async function buildCssLegacy(entryPath, urlMapper = null) {
|
|||
return result.css;
|
||||
}
|
||||
|
||||
function removeOrEnableScript(scriptNode, enable) {
|
||||
if (enable) {
|
||||
scriptNode.attr("type", "text/javascript");
|
||||
} else {
|
||||
scriptNode.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDirIfExists(targetDir) {
|
||||
try {
|
||||
await fs.rmdir(targetDir, {recursive: true});
|
||||
|
@ -397,16 +418,25 @@ class AssetMap {
|
|||
}
|
||||
}
|
||||
|
||||
all() {
|
||||
return Array.from(this._assets.values());
|
||||
[Symbol.iterator]() {
|
||||
return this._assets.entries();
|
||||
}
|
||||
|
||||
allWithout(excluded) {
|
||||
excluded = excluded.map(p => this.resolve(p));
|
||||
return this.all().filter(p => excluded.indexOf(p) === -1);
|
||||
isUnhashed(relPath) {
|
||||
const resolvedPath = this._assets.get(relPath);
|
||||
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().catch(err => console.error(err));
|
||||
build(program).catch(err => console.error(err));
|
||||
|
|
|
@ -14,38 +14,90 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
const VERSION = "%%VERSION%%";
|
||||
const OFFLINE_FILES = "%%OFFLINE_FILES%%";
|
||||
// TODO: cache these files when requested
|
||||
// The difficulty is that these are relative filenames, and we don't have access to document.baseURI
|
||||
// Clients.match({type: "window"}).url and assume they are all the same? they really should be ... safari doesn't support this though
|
||||
const CACHE_FILES = "%%CACHE_FILES%%";
|
||||
const cacheName = `hydrogen-${VERSION}`;
|
||||
const GLOBAL_HASH = "%%GLOBAL_HASH%%";
|
||||
const UNHASHED_PRECACHED_ASSETS = "%%UNHASHED_PRECACHED_ASSETS%%";
|
||||
const HASHED_PRECACHED_ASSETS = "%%HASHED_PRECACHED_ASSETS%%";
|
||||
const HASHED_CACHED_ON_REQUEST_ASSETS = "%%HASHED_CACHED_ON_REQUEST_ASSETS%%";
|
||||
const unhashedCacheName = `hydrogen-assets-${GLOBAL_HASH}`;
|
||||
const hashedCacheName = `hydrogen-assets`;
|
||||
|
||||
self.addEventListener('install', function(e) {
|
||||
e.waitUntil(
|
||||
caches.open(cacheName).then(function(cache) {
|
||||
return cache.addAll(OFFLINE_FILES);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keyList) => {
|
||||
return Promise.all(keyList.map((key) => {
|
||||
if (key !== cacheName) {
|
||||
return caches.delete(key);
|
||||
e.waitUntil((async () => {
|
||||
const unhashedCache = await caches.open(unhashedCacheName);
|
||||
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);
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
})());
|
||||
});
|
||||
|
||||
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) => {
|
||||
event.respondWith(
|
||||
caches.open(cacheName)
|
||||
.then(cache => cache.match(event.request))
|
||||
.then((response) => response || fetch(event.request))
|
||||
);
|
||||
event.respondWith(handleRequest(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) {
|
||||
return t.a({target: "_blank",
|
||||
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 {
|
||||
return t.a({target: "_blank", href: "https://github.com/vector-im/hydrogen-web"},
|
||||
"Hydrogen on Github");
|
||||
|
|
Reference in a new issue