From 18500b01ea7e99e15558a69cc792ae8d58ed7fc6 Mon Sep 17 00:00:00 2001 From: Ali Orhun Akkirman Date: Mon, 28 Sep 2026 15:19:12 +0300 Subject: [PATCH 1/2] Version application assets per build --- README.md | 4 +-- index.html | 19 +++++++++++++ scripts/build.mjs | 36 +++++++++++++++++++----- scripts/verify-offline.mjs | 57 ++++++++++++++++++++++++++++---------- src/app.js | 19 ++----------- src/sw.js | 4 ++- 6 files changed, 97 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index b6bd5ae..2306c40 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ Bu kontrol, build çıktısında uzak script/style/module bağlantısı kalmadı ## Bağımlılıklar -Çalışma zamanı bağımlılıkları CDN'den yüklenmez. npm paketleri sabit sürümlere pinlenir ve esbuild ile `dist/app.js` / `dist/app.css` içine bundle edilir. KaTeX fontları da `dist/assets/` altına kopyalanır. +Çalışma zamanı bağımlılıkları CDN'den yüklenmez. npm paketleri sabit sürümlere pinlenir ve esbuild ile içerik hash'li `dist/assets/app-.js` / `app-.css` bundle'larına dönüştürülür. KaTeX fontları da `dist/assets/` altına kopyalanır. | Kütüphane | Sürüm | Amaç | |-----------|-------|------| @@ -156,7 +156,7 @@ GitHub Pages sürümü installable PWA olarak yayınlanır. Manifest, uygulama i - `start_url` ve `scope` relative (`./`) tutulduğu için proje `/mathnote/` gibi GitHub Pages alt yollarında çalışır. - İlk başarılı ziyarette uygulama shell'i, JavaScript/CSS, KaTeX fontları, manifest ve ikonlar precache edilir. -- `app.js`, `app.css` ve manifest gibi değişebilir çekirdek dosyalar ağ varken **network-first**, çevrimdışıyken cache fallback ile yüklenir. Bu, yeni deploy sonrasında eski JavaScript'in yanlışlıkla servis edilmesini önler. +- Uygulama JavaScript/CSS dosyaları her build'de içerik hash'li yeni URL'ler alır. Böylece eski HTML yalnızca kendi bundle sürümünü, yeni HTML de yalnızca yeni bundle sürümünü yükler; HTML/JS sürüm karışması engellenir. Manifest ve navigasyon ağ varken **network-first**, çevrimdışıyken cache fallback ile çalışır. - Yeni service worker çalışan sayfayı zorla devralmaz; bekleyen service worker eski sekmeler kapandıktan sonra aktive olur. Böylece eski HTML ile yeni JavaScript aynı sayfada karışmaz. - Yeni build'de dosya içeriğinden yeni bir cache sürümü üretilir; service worker aktive olduğunda eski MathNote cache'leri temizlenir. - Service worker yalnızca HTTP/HTTPS üzerinde kaydolur. Bu nedenle `dist/index.html` dosyasını doğrudan `file://` ile açma desteği korunur. diff --git a/index.html b/index.html index 334bae5..7b9f382 100644 --- a/index.html +++ b/index.html @@ -1163,6 +1163,25 @@ + + diff --git a/scripts/build.mjs b/scripts/build.mjs index be525e4..263bd8e 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -2,26 +2,30 @@ import { build } from 'esbuild'; import { createHash } from 'node:crypto'; import { cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; import { relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; const outdir = new URL('../dist/', import.meta.url); +const assetsDir = new URL('../dist/assets/', import.meta.url); const templateUrl = new URL('../index.html', import.meta.url); const publicDir = new URL('../public/', import.meta.url); const serviceWorkerTemplateUrl = new URL('../src/sw.js', import.meta.url); +const entryPoint = fileURLToPath(new URL('../src/app.js', import.meta.url)); await rm(outdir, { recursive: true, force: true }); await mkdir(outdir, { recursive: true }); await build({ - entryPoints: [new URL('../src/app.js', import.meta.url).pathname], + entryPoints: { app: entryPoint }, bundle: true, - outfile: new URL('../dist/app.js', import.meta.url).pathname, + outdir: fileURLToPath(outdir), + entryNames: 'assets/[name]-[hash]', + assetNames: 'assets/[name]-[hash]', format: 'iife', platform: 'browser', target: ['es2020'], minify: false, sourcemap: false, legalComments: 'none', - assetNames: 'assets/[name]-[hash]', loader: { '.woff2': 'file', '.woff': 'file', @@ -29,16 +33,29 @@ await build({ } }); +const emittedAssets = await readdir(assetsDir); +const jsBundles = emittedAssets.filter(name => /^app-[A-Z0-9]+\.js$/i.test(name)); +const cssBundles = emittedAssets.filter(name => /^app-[A-Z0-9]+\.css$/i.test(name)); + +if (jsBundles.length !== 1 || cssBundles.length !== 1) { + throw new Error( + `Expected one hashed JS and CSS entry bundle, found JS=${jsBundles.length}, CSS=${cssBundles.length}.` + ); +} + +const appJsPath = `./assets/${jsBundles[0]}`; +const appCssPath = `./assets/${cssBundles[0]}`; + await cp(publicDir, outdir, { recursive: true }); let html = await readFile(templateUrl, 'utf8'); html = html.replace( '', - '' + `` ); html = html.replace( '', - '` ); if (html.includes('MATHNOTE_BUILD_')) {