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..cd422b2 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_')) { @@ -63,7 +80,10 @@ async function collectFiles(dirUrl, rootUrl = dirUrl) { files.push({ url: childUrl, - path: './' + relative(rootUrl.pathname, childUrl.pathname).replaceAll('\\\\', '/') + path: './' + relative( + fileURLToPath(rootUrl), + fileURLToPath(childUrl) + ).replaceAll('\\\\', '/') }); } @@ -86,4 +106,6 @@ serviceWorker = serviceWorker await writeFile(new URL('../dist/sw.js', import.meta.url), serviceWorker, 'utf8'); -console.log(`MathNote offline/PWA bundle written to dist/ using cache ${cacheName}.`); +console.log( + `MathNote offline/PWA bundle written to dist/ using ${appJsPath}, ${appCssPath} and cache ${cacheName}.` +); diff --git a/scripts/verify-offline.mjs b/scripts/verify-offline.mjs index 915e042..9ee3752 100644 --- a/scripts/verify-offline.mjs +++ b/scripts/verify-offline.mjs @@ -4,8 +4,6 @@ import { extname } from 'node:path'; const root = new URL('../dist/', import.meta.url); const required = [ 'index.html', - 'app.js', - 'app.css', 'manifest.webmanifest', 'sw.js', 'icons/icon-192.png', @@ -59,19 +57,43 @@ for (const forbidden of ['cdn.jsdelivr.net', 'esm.sh', 'type="module"', 'type="i if (!html.includes('rel="manifest" href="./manifest.webmanifest"')) { violations.push('index.html: manifest link missing or not relative'); } - if (!html.includes('id="examples-btn"')) { violations.push('index.html: examples button missing'); } +if (!html.includes("updateViaCache: 'none'")) { + violations.push('index.html: service worker bootstrap must bypass HTTP cache'); +} -const app = await readFile(new URL('app.js', root), 'utf8'); -const sourceApp = await readFile(new URL('../src/app.js', import.meta.url), 'utf8'); -if (!app.includes("register('./sw.js'") && !app.includes('register("./sw.js"')) { - violations.push('app.js: relative service worker registration missing'); +const jsMatch = html.match(/