81 lines
3.1 KiB
Python
81 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
cache_bust.py -- stamp a content-hash version onto local CSS/JS references so a
|
|
deploy is picked up immediately instead of serving stale cached assets.
|
|
|
|
Why: assets are served without far-future immutable caching AND `main.css` pulls
|
|
its 25 partials via `@import` (whose URLs the browser caches independently). So a
|
|
CSS/JS change could stay invisible until the browser revalidated. This appends
|
|
`?v=<hash>` to:
|
|
- every local `<link href=... .css>` / `<script src=... .js>` in the pages, and
|
|
- every `@import '...css'` inside css/main.css (so partial edits bust too).
|
|
|
|
The version is one global short md5 of ALL css+js content (with any existing
|
|
`?v=` stripped first), so it is deterministic and idempotent: same content ->
|
|
same stamp (no diff); any asset edit -> new stamp everywhere. External
|
|
(https://) references are never touched.
|
|
|
|
Run last in the build (after content/SEO/listings). Idempotent.
|
|
"""
|
|
import io, os, re, sys, glob, hashlib
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
VER = re.compile(r'\?v=[0-9a-f]{8}')
|
|
|
|
def rd(p):
|
|
with io.open(p, encoding="utf-8") as f: return f.read()
|
|
def wr(p, s):
|
|
with io.open(p, "w", encoding="utf-8", newline="\n") as f: f.write(s)
|
|
|
|
def asset_files():
|
|
files = glob.glob(os.path.join(ROOT, "css", "**", "*.css"), recursive=True)
|
|
files += glob.glob(os.path.join(ROOT, "*.css"))
|
|
files += glob.glob(os.path.join(ROOT, "*.js"))
|
|
return sorted(set(files))
|
|
|
|
def compute_hash():
|
|
h = hashlib.md5()
|
|
for p in asset_files():
|
|
rel = os.path.relpath(p, ROOT).replace("\\", "/")
|
|
# strip any existing ?v= so the hash depends only on real content
|
|
h.update((rel + "\0" + VER.sub("", rd(p))).encode("utf-8"))
|
|
h.update(b"\0")
|
|
return h.hexdigest()[:8]
|
|
|
|
def html_pages():
|
|
pats = []
|
|
for lang in ("en", "mk", "si"):
|
|
pats += glob.glob(os.path.join(ROOT, lang, "**", "*.html"), recursive=True)
|
|
pats += glob.glob(os.path.join(ROOT, "*.html"))
|
|
return sorted(set(pats))
|
|
|
|
# local href="....css" / src="....js" (not http), optional existing ?v
|
|
REF = re.compile(r'((?:href|src)=")(?!https?:)([^"?]+\.(?:css|js))(?:\?v=[0-9a-f]{8})?(")')
|
|
# @import '....css' (single- or double-quoted), optional existing ?v
|
|
IMP = re.compile(r"""(@import\s+['"][^'"?]+?\.css)(?:\?v=[0-9a-f]{8})?(['"])""")
|
|
|
|
def stamp(text, ver):
|
|
text = REF.sub(lambda m: f'{m.group(1)}{m.group(2)}?v={ver}{m.group(3)}', text)
|
|
text = IMP.sub(lambda m: f'{m.group(1)}?v={ver}{m.group(2)}', text)
|
|
return text
|
|
|
|
def run(check=False):
|
|
ver = compute_hash()
|
|
changed = 0
|
|
targets = html_pages() + [os.path.join(ROOT, "css", "main.css")]
|
|
for p in targets:
|
|
s = rd(p)
|
|
s2 = stamp(s, ver)
|
|
if s2 != s:
|
|
changed += 1
|
|
if not check:
|
|
wr(p, s2)
|
|
print(f"cache-bust v={ver}: {'would update' if check else 'updated'} {changed} file(s) of {len(targets)}")
|
|
return changed
|
|
|
|
if __name__ == "__main__":
|
|
check = "--check" in sys.argv
|
|
n = run(check=check)
|
|
# In --check mode, a non-zero count means the site is out of sync.
|
|
sys.exit(1 if (check and n) else 0)
|