"""Stamping for the service worker. The dev server stamps sw.js on every request; the deploy stamps it once into the uploaded copy. Both import this, because a digest computed two different ways is a worker that thinks its caches are current when they are not. """ from __future__ import annotations import hashlib import json import os WORKER = "sw.js" VERSION_LINE = "const VERSION = 'dev';" LIBRARY_VERSION_LINE = "const LIBRARY_VERSION = 'dev';" LIBRARY_LINE = "const LIBRARY = [];" CHUNK = 1 << 20 def walk(roots): found = {} for root in roots: for dirpath, _dirs, names in os.walk(root): for name in sorted(names): path = os.path.join(dirpath, name) rel = os.path.relpath(path, root).replace(os.sep, "/") found.setdefault(rel, path) return found def digest(files): # Contents, not size and mtime: a checkout, a rebuild and a rsync all move # mtimes without changing a byte, and each one of those would otherwise # throw away every cache on every device for nothing. out = hashlib.sha256() for rel in sorted(files): out.update(rel.encode("utf-8") + b"\0") with open(files[rel], "rb") as fh: for chunk in iter(lambda: fh.read(CHUNK), b""): out.update(chunk) out.update(b"\0") return out.hexdigest()[:12] def page_url(rel): if rel == "index.html": return "./" return "./" + (rel[:-len(".html")] if rel.endswith(".html") else rel) def stamp_worker(roots): chrome = walk(roots[:1]) library = {rel: path for rel, path in walk(roots).items() if rel not in chrome} src = chrome.pop(WORKER, None) if src is None: return None with open(src, encoding="utf-8") as fh: body = fh.read() for line, value in ( (VERSION_LINE, "const VERSION = '%s';" % digest(chrome)), (LIBRARY_VERSION_LINE, "const LIBRARY_VERSION = '%s';" % digest(library)), (LIBRARY_LINE, "const LIBRARY = %s;" % json.dumps(sorted(page_url(rel) for rel in library), indent=2)), ): if line not in body: raise RuntimeError("%s: expected to find %r to stamp" % (WORKER, line)) body = body.replace(line, value, 1) return body.encode("utf-8")