diff --git a/.github/workflows/bit.ci.Bswup.yml b/.github/workflows/bit.ci.Bswup.yml index 5192c57506..3387fba9c7 100644 --- a/.github/workflows/bit.ci.Bswup.yml +++ b/.github/workflows/bit.ci.Bswup.yml @@ -53,3 +53,11 @@ jobs: - name: dotnet build run: dotnet build src/Bswup/Bit.Bswup.slnx + + # Runs against Bit.Bswup/wwwroot/*.js, which the build above produces, so this also covers + # the build step itself (e.g. the minifier re-introducing downleveled syntax). + - name: JS tests (service worker, page script, progress UI) + run: | + cd src/Bswup/Tests/bit-bswup-js + npm ci + npm test diff --git a/src/Bit.CI.Release.slnx b/src/Bit.CI.Release.slnx index 5c0e39348f..c7772d3912 100644 --- a/src/Bit.CI.Release.slnx +++ b/src/Bit.CI.Release.slnx @@ -46,12 +46,15 @@ - + + + + - - - + + + diff --git a/src/Bit.slnx b/src/Bit.slnx index e77eddfcc9..ed9ec97842 100644 --- a/src/Bit.slnx +++ b/src/Bit.slnx @@ -78,14 +78,15 @@ - - - + + + + - - - + + + diff --git a/src/Bswup/Bit.Bswup.Demo/App.razor b/src/Bswup/Bit.Bswup.Demo/App.razor index add97e129b..b063da0764 100644 --- a/src/Bswup/Bit.Bswup.Demo/App.razor +++ b/src/Bswup/Bit.Bswup.Demo/App.razor @@ -1,11 +1,16 @@ - - - - - - - Not found -

Sorry, there's nothing at this address.

-
-
-
+ + + + + + + + Not found - bit Bswup +
+

404

+

Sorry, there's nothing at this address.

+ Back to the docs +
+
+
+
diff --git a/src/Bswup/Bit.Bswup.Demo/Bit.Bswup.Demo.csproj b/src/Bswup/Bit.Bswup.Demo/Bit.Bswup.Demo.csproj index 2b8520fff4..cd2808a4cf 100644 --- a/src/Bswup/Bit.Bswup.Demo/Bit.Bswup.Demo.csproj +++ b/src/Bswup/Bit.Bswup.Demo/Bit.Bswup.Demo.csproj @@ -20,4 +20,4 @@ -
\ No newline at end of file +
diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/CleanupPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/CleanupPage.razor new file mode 100644 index 0000000000..6489a8db60 --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/Pages/CleanupPage.razor @@ -0,0 +1,52 @@ +@page "/cleanup" + +Backing Out of Bswup - bit Bswup + +

Backing Out of Bswup

+

+ Fully remove Bswup from a deployed app - dropping offline support, or recovering clients stuck on a + broken worker or cache - with the self-destructing cleanup worker. +

+ +

+ Replace the content of your service-worker.js and + service-worker.published.js (the file deployed builds actually ship, via the + ServiceWorker item's PublishedContent mapping) with: +

+ + +

What happens next

+

On its next update check, every client installs this self-destructing worker instead. It:

+
    +
  1. activates immediately,
  2. +
  3. purges this app's Bswup and Blazor caches,
  4. +
  5. unregisters its own registration, and
  6. +
  7. signals open tabs to detach.
  8. +
+

+ Tabs the previous worker controlled reload once; everything afterwards runs purely from the network, + even while the page keeps referencing bit-bswup.js - each later load just repeats the + register/self-unregister cycle silently, with no reloads. +

+ + Once no client has loaded the old app for as long as your cache headers require, the + bit-bswup.js script tag can be removed from the host document too. + + +

Client-side reset without backing out

+

+ If you only need to recover a single broken client (not remove Bswup from the deployment), + prefer BitBswup.forceRefresh() - it clears this + app's caches, unregisters its worker, and reloads, without touching the deployed service worker file. +

+ + diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/EventsPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/EventsPage.razor new file mode 100644 index 0000000000..c7e8796b26 --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/Pages/EventsPage.razor @@ -0,0 +1,220 @@ +@page "/events" + +Events & Handler - bit Bswup + +

Events & Handler

+

+ Bswup reports its whole lifecycle through a single global handler function. Use the built-in + progress UI, or write your own handler and drive any markup you like. +

+ +

+ The handler's name is configured through the handler + script attribute (default bitBswupHandler). It receives (type, data) where + type is one of the BswupMessage constants: +

+ +

Event catalog

+
+ + + + + + + + + + + + + + + + +
BswupMessageValueRaised when
updateFoundUPDATE_FOUNDThe browser found a new service worker version and started installing it.
stateChangedSTATE_CHANGEDThe installing worker's lifecycle state changed (data.currentTarget.state).
downloadStartedDOWNLOAD_STARTEDAsset download began. data.version, data.firstInstall.
downloadProgressDOWNLOAD_PROGRESSAn asset finished. data.percent (0-100), data.index (1-based count), data.asset (url, reqUrl, hash).
downloadFinishedDOWNLOAD_FINISHEDAll assets are staged. data.firstInstall, data.reload(), data.cleanup().
updateReadyUPDATE_READYA fully staged update is waiting to be activated (data.reload()).
activateACTIVATEA new version activated (data.version).
updateNotFoundUPDATE_NOT_FOUNDAn update check completed and the app is already on the latest version.
updateCheckFailedUPDATE_CHECK_FAILEDAn update check itself failed transiently (offline, server hiccup). Non-blocking - the app keeps running.
errorERRORA structured install failure - see below.
+
+ +

A complete handler

+ + +

Finishing a download: reload and cleanup

+
    +
  • + data.reload() activates the staged version. On a first install it + claims the clients and starts Blazor with no reload; on an update it + performs SKIP_WAITING and reloads. +
  • +
  • + data.cleanup() (optional) asks the active service worker to prune this app's stale + cache buckets right away. It is safe to call at any time - the worker declines while an update is + staged or staging (pruning then happens automatically on activation), and it never touches another + app's caches. Most apps never need it: the same pruning already runs on activation and after every + accepted update. +
  • +
+ + + Since v-10-6-0, the built-in BswupProgress component's AutoReload parameter + defaults to false: when an update finishes downloading, the reload button is shown and the + new version activates when the user accepts it - an unprompted reload discards whatever in-page state + the user has mid-session. Set AutoReload="true" to restore the old behavior. First + installs are unaffected: they always complete the seamless claim-and-start flow (no reload). + + +

Multi-tab updates

+

+ Service workers are single-instance per origin, so accepting an update in one tab activates the new + version for every open tab. When that happens, Bswup has the new worker claim all clients and each + other tab reloads itself automatically (via controllerchange) onto the new + version. This keeps every tab consistent and avoids the classic failure where an old tab keeps running + old app code while its asset requests are served from the new version's cache (mismatched boot config / + DLL hashes). The first install is exempt: claiming a client for the first time starts Blazor and does + not trigger a reload. +

+ +

The error payload

+

Install failures are structured:

+
+ + + + + + + + + + + + + + + + + + + + +
FieldMeaning
reason + One of manifest, integrity, fetch, + cache, request, install-incomplete, + install-aborted, or install-infra (the install died + before/while touching CacheStorage - storage pressure, a broken private mode - always + fatal). +
messageHuman-readable description.
url / hashThe offending asset, when known.
fatal + Whether the install actually stopped. Under the default lax tolerance a + failed asset is reported with fatal: false - the install still succeeds and + the asset is fetched from the network on first use - so treat it as a warning, not a + dead app. fatal: true means no usable staged version is available to this + page - though a worker may still have been installed (a lax + install-infra failure resolves the install so the worker can keep serving + as a network pass-through). +
firstInstall + Where a fatal failure landed. true: before the app ever booted - Bswup + starts the app without a service worker so it still boots. false: a + background update failed - the app keeps running on the current version (the + previous worker keeps serving). +
+
+ + + + +@code { + private const string HandlerCode = @"const appEl = document.getElementById('app'); +const bswupEl = document.getElementById('bit-bswup'); +const progressBar = document.getElementById('bit-bswup-progress-bar'); +const reloadButton = document.getElementById('bit-bswup-reload'); + +function bitBswupHandler(type, data) { + switch (type) { + case BswupMessage.updateFound: return console.log('an update found.'); + + case BswupMessage.stateChanged: return console.log('state:', data.currentTarget.state); + + case BswupMessage.activate: return console.log('new version activated:', data.version); + + case BswupMessage.downloadStarted: + // A background update downloads behind the running app - only a first + // install owns the screen (firstInstall rides on every message). + if (data?.firstInstall === false) return; + appEl.style.display = 'none'; + bswupEl.style.display = 'block'; + return console.log('downloading assets started:', data?.version); + + case BswupMessage.downloadProgress: + progressBar.style.width = `${Math.round(data.percent)}%`; + return console.log('asset downloaded:', data.asset.url, data); + + case BswupMessage.downloadFinished: + if (data.firstInstall) { + // First install: claim + start Blazor, no page reload. Reveal the app even if + // the claim/start stalls or rejects - a hidden #app with no way out is worse + // than an app shown behind a stale splash. + const reveal = () => { + appEl.style.display = 'block'; + bswupEl.style.display = 'none'; + }; + const failSafe = setTimeout(reveal, 10000); + data.reload() + .catch(err => console.error('Bswup first install failed to start:', err)) + .then(() => { clearTimeout(failSafe); reveal(); }); + } else { + // Update: let the user accept it. + reloadButton.style.display = 'block'; + reloadButton.onclick = data.reload; + } + return console.log('downloading assets finished.'); + + case BswupMessage.updateReady: + reloadButton.style.display = 'block'; + reloadButton.onclick = data.reload; + return console.log('new update ready.'); + + case BswupMessage.updateNotFound: + return console.log('already on the latest version.'); + + case BswupMessage.updateCheckFailed: + return console.warn('could not check for updates right now:', data); + + case BswupMessage.error: + if (data.fatal === false) { + // lax tolerance: the install continued; the asset lazy-fills later. + return console.warn('Bswup asset skipped:', data.reason, data.message); + } + console.error('Bswup install error:', data.reason, data.message, data); + if (data.firstInstall) { + // Fatal first-install failure: Bswup force-starts Blazor from the network; + // reveal the app instead of leaving it booted behind the splash. + appEl.style.display = 'block'; + bswupEl.style.display = 'none'; + } + return; + } +}"; + + private const string ErrorHandlerCode = @"case BswupMessage.error: + if (data.fatal === false) { + console.warn('Bswup asset skipped:', data.reason, data.message, data); + return; + } + if (data.firstInstall === false) { + // A background update failed - the running app is untouched. + return; + } + // Fatal first-install failure: Bswup force-starts Blazor from the network; + // reveal the app instead of leaving it booted behind the splash. + appEl.style.display = 'block'; + bswupEl.style.display = 'none'; + return;"; +} diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/GettingStartedPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/GettingStartedPage.razor new file mode 100644 index 0000000000..33058d424c --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/Pages/GettingStartedPage.razor @@ -0,0 +1,118 @@ +@page "/getting-started" + +Getting Started - bit Bswup + +

Getting Started

+

+ Add install progress, controlled updates, and offline support to a Blazor WebAssembly app + in six small steps. +

+ +

1. Install the NuGet package

+ + +

2. Enable static file caching (server-hosted apps)

+

+ Long-lived HTTP caching makes repeat visits and updates dramatically faster - the service worker + revalidates content by hash, so stale HTTP caches are never a correctness problem. +

+ + +

3. Defer Blazor's startup

+

+ Bswup must start Blazor at the right moment (after the first install completes, or immediately when + the app is already cached), so disable auto-start on the Blazor script in your host document + (index.html or App.razor): +

+ + +

4. Add the Bswup script

+

Reference bit-bswup.js right after the Blazor script:

+ +

+ Every attribute is optional - see the Script Options reference for all + of them and their defaults. +

+ +

5. Configure the service worker

+

+ Create (or edit) wwwroot/service-worker.js and wwwroot/service-worker.published.js. + The only mandatory line is the importScripts call; everything else is tuning: +

+ +

Then make sure the service worker is registered in your project file:

+ +

See the Service Worker Settings reference for every available option.

+ +

6. Pick a progress UI

+

You have two options:

+
    +
  • + Use the built-in progress UI - the BswupProgress component + plus bit-bswup.progress.js - and you are done. This is what this site uses. +
  • +
  • + Write your own handler function and drive any markup you like - see + Events & Handler for the full event catalog and a complete sample. +
  • +
+ + + This documentation site is a live Bswup app. Open dev tools → Application → Service Workers to + inspect the registration, or visit the Live Playground to watch events and + drive the update lifecycle by hand. + + + + +@code { + private const string ServerCacheCode = @"app.UseStaticFiles(new StaticFileOptions +{ + OnPrepareResponse = ctx => + { + if (env.IsDevelopment() is false) + { + // https://bitplatform.dev/templates/cache-mechanism + ctx.Context.Response.GetTypedHeaders().CacheControl = new() + { + MaxAge = TimeSpan.FromDays(7), + Public = true + }; + } + } +});"; + + private const string AutostartCode = @""; + + private const string ScriptTagCode = @""; + + private const string ServiceWorkerCode = @"// wwwroot/service-worker.published.js +self.assetsExclude = [/\.scp\.css$/]; +self.caseInsensitiveUrl = true; + +// The one mandatory line - imports the Bswup service worker engine: +self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js');"; + + private const string CsprojCode = @" + service-worker-assets.js + + + + +"; +} diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/HomePage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/HomePage.razor index 4ef62a7a95..6492af9f25 100644 --- a/src/Bswup/Bit.Bswup.Demo/Pages/HomePage.razor +++ b/src/Bswup/Bit.Bswup.Demo/Pages/HomePage.razor @@ -1,22 +1,122 @@ @page "/" -@page "/home" -Home +bit Bswup - Blazor Service Worker Update Progress -

222

+
+ Blazor Service Worker Update Progress +

Ship Blazor PWAs that install, update & work offline - beautifully.

+

+ Bswup replaces the default Blazor service worker experience with a real install progress UI, + controlled background updates, resilient downloads, and a tiny JavaScript API - all with a + single script tag. +

+ +
+ dotnet add package Bit.Bswup +
+
-

Hello, world!

+
+ +
📥
+

First-install splash

+

A branded full-screen progress UI while assets download - the very splash this site booted with.

+
+ +
🔄
+

Controlled updates

+

Updates download silently in the background and wait for the user to accept - no unprompted reloads, consistent across every open tab.

+
+ +
📴
+

Offline support

+

Full precache or lazy passive mode, SPA navigation fallback, media range requests, and external asset caching.

+
+ +
🛡️
+

Resilient installs

+

Retries with backoff, lax/strict error tolerance, a first-install stall watchdog, and structured error reporting.

+
+ +
🧭
+

Update polling

+

Interval and on-focus update checks, a check-for-update API, and up-to-date / check-failed events.

+
+ +
⚙️
+

Configurable, not magical

+

Everything is opt-in via script-tag attributes and service worker settings, with sensible defaults.

+
+ +
💾
+

Durable storage

+

One call to request eviction-resistant storage so offline apps survive browser storage pressure.

+
+ +
🧹
+

Clean exit path

+

A self-destructing cleanup worker recovers broken clients or removes Bswup entirely from a deployed app.

+
+
-Welcome to your new app. +

What is Bswup?

+

+ bit Bswup (Blazor service-worker update progress) is a + NuGet package for Blazor WebAssembly apps that takes over the service worker lifecycle: + it downloads and caches your app's assets with visible progress, verifies their integrity, serves them + offline, and manages version updates without ever leaving your users staring at a blank page - or + running a stale build for days. +

+

+ This documentation site is itself a Blazor WebAssembly app powered by Bswup - open the browser dev + tools, go offline, and reload. Then head over to the Live Playground to watch + the service worker events in real time. +

-
+

How it fits together

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
PieceWhat it does
bit-bswup.jsPage-side script: registers the service worker, coordinates the install/update handshake, starts Blazor at the right moment, polls for updates, and raises lifecycle events.
bit-bswup.sw.jsThe service worker engine: precaches assets (with retries and integrity checks), serves fetches from cache, handles SPA navigation fallback, and stages new versions. Imported from your service-worker.js.
bit-bswup.progress.js + BswupProgressThe optional built-in progress UI: splash screen, progress bar, reload button, and failure panel - CSP-friendly and screen-reader aware.
BitBswupA small JavaScript API to check for updates, activate staged versions, request persistent storage, and force-reset a broken client.
bit-bswup.sw-cleanup.jsA self-destructing worker to back out of Bswup or recover clients stuck on a broken worker or cache.
+
-Counter +

Demo projects

+

Besides this documentation site, the repository ships two runnable samples:

+
    +
  • Demos/BasicDemo (Bit.Bswup.BasicDemo) - a minimal standalone Blazor WebAssembly app with a hand-written handler, deliberately including a failing external asset to exercise the error flow.
  • +
  • Demos/FullDemo (Bit.Bswup.FullDemo.*) - a Blazor Web App (server + client) using the BswupProgress component with a custom progress bar.
  • +
-
-
-
- - - - \ No newline at end of file + diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/JsApiPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/JsApiPage.razor new file mode 100644 index 0000000000..14c5e79668 --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/Pages/JsApiPage.razor @@ -0,0 +1,129 @@ +@page "/javascript-api" + +JavaScript API - bit Bswup + +

JavaScript API

+

+ Bswup exposes a small global BitBswup object so you can drive the update lifecycle from + your own code - a "check for updates" button, a custom poller, a "reset app" action. +

+ + + The Live Playground wires each of these methods to a button on this very site. + + +

BitBswup.checkForUpdate()

+

+ Asks the browser to re-fetch the service worker script and check for a new version. If a new version is + found, the normal update flow runs (updateFoundstateChanged → + updateReady/downloadFinished). If the app is already on the latest version, + Bswup raises updateNotFound so you can stop a spinner or show an "up to date" message. +

+

+ If the check itself fails for a transient reason (offline, server hiccup, a throttled background tab), + Bswup raises the non-blocking updateCheckFailed event instead of the install-path + error event, so the default progress handler does not hide the app or + show the install-failed UI; the payload still carries reason/message so you + can surface it yourself. Registration-aware and safe to call as often as you like - it is what powers + the built-in polling. +

+ +

BitBswup.persistStorage()

+

+ Requests durable, eviction-resistant storage for the origin via navigator.storage.persist() + and resolves with a boolean saying whether storage is now persistent. Without it the caches are + best-effort and can be reclaimed by the browser (Safari deletes all storage for a site not interacted + with for seven days). Calling it from a user gesture - after login, from an "install app" button - has + the best chance of being granted. Safe to call repeatedly: an already-persistent origin resolves + true without prompting again, and unsupported browsers resolve false with a + console warning. +

+ +

BitBswup.skipWaiting()

+

+ If an update has finished downloading and is waiting, this activates it immediately (equivalent to + calling the reload callback from updateReady/downloadFinished). + Returns true when there was a waiting worker to activate, otherwise false. +

+ +

BitBswup.forceRefresh(cacheFilter?)

+

+ Clears caches, unregisters the service worker controlling the current page, and reloads. Use this as a + last-resort "reset" when a client gets into a bad state. It only removes this app's own registration - + other apps mounted under different scopes on the same origin are left untouched. +

+

+ By default it clears only the caches this app and Blazor own: the app's scope-qualified Bswup buckets, + legacy scope-less buckets, and blazor-resources caches. A sibling Bswup app's scoped + buckets are spared, and app-owned CacheStorage buckets (Workbox add-ons, offline app data, cached API + responses) are left intact, since those can hold data with no other copy. To change what gets cleared, + pass an optional cacheFilter: a string (prefix match), a RegExp, or a + predicate (key) => boolean: +

+ + +

Polling for updates

+

+ By default a service worker is only re-checked by the browser on navigation and roughly every 24 hours, + so a tab that stays open for a long time can keep running an old version. There are two ways to check + more often: +

+
    +
  1. + Set updateInterval (and/or + updateOnVisibility) on the script tag + for built-in polling. Simplest, no extra code - this site uses both. +
  2. +
  3. Call BitBswup.checkForUpdate() yourself, from a timer or after a user action:
  4. +
+ +

+ Either way, the result surfaces through your handler: a found update flows through + updateFound/updateReady, "nothing new" flows through + updateNotFound, and a transient check failure flows through + updateCheckFailed: +

+ + + Built-in polling skips checks while the tab is in the background (the browser throttles those timers + anyway); the next timer tick after the tab is foregrounded runs normally. For an immediate + check the moment the user comes back, also set updateOnVisibility="true". + + + + +@code { + private const string ForceRefreshCode = @"BitBswup.forceRefresh(); // Bswup + Blazor caches (default) +BitBswup.forceRefresh(() => true); // every cache on the origin +BitBswup.forceRefresh('bit-bswup'); // only Bswup's own caches +BitBswup.forceRefresh(/^(bit-bswup|my-app-data)/) // a specific set"; + + private const string PollingCode = @"// an ordinary failed check (offline, server hiccup) does NOT reject - it is reported through +// the UPDATE_CHECK_FAILED message below. This catch is only for unexpected errors, such as a +// call made before the registration is ready. +const checkNow = () => BitBswup.checkForUpdate() + .catch(err => console.warn('unexpected update check error:', err)); + +// check every hour from your own code (equivalent to updateInterval=""3600"") +setInterval(checkNow, 60 * 60 * 1000); + +// or check whenever the user clicks a button, and react to the result +document.getElementById('check-updates').onclick = checkNow;"; + + private const string PollingHandlerCode = @"window.bitBswupHandler = (message, data) => { + switch (message) { + case 'UPDATE_NOT_FOUND': /* already up to date - stop the spinner */ break; + case 'UPDATE_CHECK_FAILED': /* transient failure - keep running, optionally notify */ break; + // updateFound / stateChanged / updateReady / downloadFinished drive the update UI + } +};"; +} diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/MigrationPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/MigrationPage.razor new file mode 100644 index 0000000000..cc189317bf --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/Pages/MigrationPage.razor @@ -0,0 +1,58 @@ +@page "/migration" + +Migrating to 10-6-0 - bit Bswup + +

Migrating to v-10-6-0

+

+ v-10-6-0 is a resilience-focused release. Most changes are internal hardening; this page lists what is + visible when upgrading. +

+ +

Action items

+ +

1. Updates no longer auto-reload. BswupProgress.AutoReload now defaults to false - updates announce themselves through the reload button. Set AutoReload="true" to restore the old behavior.

+

2. Blocked URLs answer 403. prohibitedUrls matches are answered with 403 Forbidden (previously 405). Code detecting a blocked URL by status must check for 403.

+

3. String entries now take effect. Strings in the URL-matching lists (assetsInclude, assetsExclude, prohibitedUrls, serverHandledUrls, serverRenderedUrls) are now matched literally as substrings. Previous releases silently ignored them - audit any strings sitting in those lists, because they take effect for the first time after upgrading.

+

4. Hand-written splash markup needs one edit. Move <button id="bit-bswup-reload"> outside the #bit-bswup overlay (and give it a z-index), optionally adding a visually-hidden <span id="bit-bswup-reload-status" role="status"> next to it. The built-in handling no longer reveals the overlay for updates, so a button left inside it would never become visible. The BswupProgress component ships this layout already.

+
+ +

Behavior changes

+
    +
  • Cache buckets are scope-qualified: bit-bswup:<scope-path> - <version> (previously bit-bswup - <version>). Multiple Bswup apps on one origin no longer evict each other's caches; the migration from legacy-named buckets is automatic and does not re-download.
  • +
  • Under the default lax tolerance, the asset download now runs inside the install event (waitUntil): the browser keeps the worker alive for the whole download, and updateReady is only raised once the new version is fully staged - previously it could fire while the download had barely started.
  • +
  • assetsUrl defaults to a path resolved against the service worker script's own location (previously root-absolute), so apps mounted on a sub-path work without configuration.
  • +
  • The cleanup callback only prunes when no update is staged or staging, and BitBswup.forceRefresh()'s default filter clears only this app's own, legacy, and Blazor caches. The same staged-or-staging guard protects every cache-pruning path, so a prune can never race a newer install's freshly written bucket.
  • +
  • Requests carrying a Range header are answered with real 206 Partial Content slices from cached bodies (cached media now plays in Safari), and partial responses are never written to the cache.
  • +
  • Updates found later in the same session as the first install are treated as real updates: updateReady is raised, downloadFinished carries firstInstall: false, and accepting them runs the normal SKIP_WAITING flow. Previously a long-lived tab kept classifying every later update as another first install.
  • +
  • Navigations whose URL is itself a managed asset (e.g. opening /manifest.json directly) are served that asset instead of the SPA default document.
  • +
  • An update's re-download of the default document and of hash-less assets no longer deletes the existing cache entry first: if the refresh fails (offline mid-update), the previous copy keeps serving - including offline navigation.
  • +
  • The built-in progress UI stays out of the way during background updates: the full-screen splash is first-install only (downloadStarted/downloadProgress payloads carry firstInstall so custom handlers can do the same), the reload button lives outside the overlay, it is announced via a role="status" region, and the overlay no longer swallows clicks outside its content.
  • +
  • A first install completes even when no handler function is registered at all (previously the app waited out the full stallTimeout behind the splash).
  • +
  • The cleanup worker unregisters its own registration during teardown and no longer claims clients; the page reloads on UNREGISTER only while actually controlled, removing a reload-loop hazard.
  • +
  • The passive-mode background top-up after first boot is deterministic: it fills every asset still missing from the cache.
  • +
  • The default asset excludes cover all shipped worker-script variants (bit-bswup.sw.min.js, bit-bswup.sw-cleanup.js, bit-bswup.sw-cleanup.min.js).
  • +
  • A manifest or externalAssets entry whose URL cannot be parsed is skipped with a non-fatal request error instead of killing the whole service worker at startup.
  • +
  • A navigation to a URL that only a RegExp externalAssets pattern matches is served the app shell, never the pattern asset - and a shell cache miss during navigation is refilled from the shell's own URL, never from the navigated route's URL.
  • +
  • WAITING_SKIPPED and UNREGISTER never reload an uncontrolled page anymore; they make sure the app is booted instead. Activating a first install through BitBswup.skipWaiting() completes the seamless claim-and-start flow instead of reloading.
  • +
  • A page that loads while an update is already mid-install now observes it: updateReady / stateChanged fire in that tab when the update finishes staging.
  • +
  • An exception thrown by the app's bitBswupHandler no longer breaks the update pipeline (it is logged, and the remaining messages still dispatch).
  • +
+ +

New capabilities

+
    +
  • Update polling: the updateInterval / updateOnVisibility script attributes, a registration-aware BitBswup.checkForUpdate(), and the updateNotFound / updateCheckFailed events.
  • +
  • Install robustness: errorTolerance (lax/strict), transient-failure retries (maxRetries, retryDelay), the stallTimeout first-install watchdog, and structured error payloads (reason, fatal, firstInstall - including the terminal install-infra reason).
  • +
  • Storage: persistStorage / BitBswup.persistStorage() for eviction-resistant storage, and cacheVersion for manual control of cache-bucket rotation.
  • +
  • Registration: automatic retry with the default scope when the browser rejects the configured scope, and fingerprint-tolerant Blazor entry-script detection for .NET 9+ @@Assets[...] references.
  • +
+ + diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/PlaygroundPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/PlaygroundPage.razor new file mode 100644 index 0000000000..f81331e11b --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/Pages/PlaygroundPage.razor @@ -0,0 +1,93 @@ +@page "/playground" + +Live Playground - bit Bswup + +
+

Live Playground

+

+ This documentation site is itself a Blazor WebAssembly app powered by Bswup. Inspect its service + worker, drive the JavaScript API, and watch every lifecycle event + arrive in real time. +

+ + +

+ Open dev tools → Network and switch to Offline, then reload - the app keeps working from + cache. Or open this site in a second tab, accept an update there, and watch this tab reload + itself onto the new version. +

+
+ +
+
+

Service worker status

+
+
Service worker
+
This page
+
Scope
+
Active worker
+
Update staged
+
Storage
+
+
+ +
+
+ +
+

Cache buckets

+

+ Bswup's buckets are scope-qualified: + bit-bswup:<scope-path> - <version>. +

+
    +
    + +
    +

    Update lifecycle

    +

    + checkForUpdate() re-fetches the service worker script; if nothing changed you + will see UPDATE_NOT_FOUND in the log below. skipWaiting() + activates a staged update immediately. +

    +
    + + +
    +
    + +
    +

    Storage & reset

    +

    + persistStorage() requests eviction-resistant storage (browsers may prompt or + apply engagement heuristics). forceRefresh() clears this app's caches, + unregisters its worker, and reloads - the last-resort client reset. +

    +
    + + +
    +
    +
    + +

    Event log

    +

    + Every BswupMessage raised on this page - fed by a custom handler chained after the + built-in progress UI via the Handler/data-bit-bswup-handler option + (newest first, capped at 200 entries). +

    + @* aria-live pairs with demo.js rendering incrementally: new entries are prepended rather + than the list being rebuilt, so screen readers announce only what is new. *@ +
      +
      + + diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/ProgressUIPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/ProgressUIPage.razor new file mode 100644 index 0000000000..861f5d67ab --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/Pages/ProgressUIPage.razor @@ -0,0 +1,150 @@ +@page "/progress-ui" + +Progress UI - bit Bswup + +

      The Built-in Progress UI

      +

      + Instead of writing a handler yourself, use the built-in splash/progress UI: the + BswupProgress Razor component (the markup) plus bit-bswup.progress.js + (the behavior). It registered the splash you saw when this site first installed. +

      + +

      Reference both in your host document:

      + +

      Then render the component:

      + + +

      Component parameters

      +

      + Each parameter maps to a data-bit-bswup-* attribute that the script reads at load time. + The component emits no inline <script>, so it works under a strict + Content-Security-Policy and when rendered by an interactive Blazor renderer. +

      +
      + + + + + + + + + + + + + + +
      ParameterDefaultDescription
      AutoReloadfalseReload automatically when an update finishes instead of showing the reload button. Changed in v-10-6-0 - previously defaulted to true.
      ShowLogsfalseLog lifecycle messages to the console.
      ShowAssetsfalseList each downloaded asset inside the splash.
      AppContainer#appSelector of the element to hide while installing (used with HideApp). An invalid selector is tolerated - only the hiding is skipped.
      HideAppfalseHide the app container while the first install downloads.
      AutoHidefalseHide the splash automatically when the download finishes.
      Handler-Name of an additional custom handler invoked after the built-in handling, so you can layer your own behavior without replacing the UI. (Do not point it at bitBswupHandler itself - self-references are detected and ignored.)
      ChildContent-Replaces the default splash markup with your own - see below.
      +
      + +

      Behavior worth knowing

      +
        +
      • + The full-screen splash is first-install only. A background update downloads + silently behind the running app; when it finishes - or when an update is already staged at page + load - the reload button appears on its own, with a screen-reader announcement through a + role="status" region. +
      • +
      • + Clicks outside the splash content pass through the overlay, so an app force-started under a failure + panel stays usable. +
      • +
      • + A failure panel with retry appears for fatal first-install errors; deterministic failures + (an unparseable manifest, an integrity mismatch) hide the retry button since reloading would fail + identically. +
      • +
      • + Runtime toggles are available via + BitBswupProgress.config({ autoReload, showLogs, showAssets, hideApp, autoHide }). +
      • +
      + +

      Custom splash content

      +

      + Pass ChildContent to replace the default splash markup. The component keeps initializing + automatically, and the built-in behavior drives your markup through the documented element ids - + include whichever you want driven: +

      +
      + + + + + + + + + + + +
      Element idDriven as
      bit-bswup-progress-barWidth + aria-valuenow set to the download percentage; the splash root also gets --bit-bswup-percent / --bit-bswup-percent-text CSS variables.
      bit-bswup-percentText content set to NN%.
      bit-bswup-assetsEach downloaded asset prepended as a list item.
      bit-bswup-error (+ -message, -details, -retry)The failure panel for fatal first-install errors.
      bit-bswup-reload / bit-bswup-reload-statusThe update-ready button and its screen-reader status region.
      +
      + + The update-ready button (#bit-bswup-reload) and its status region are always rendered by + the component itself, outside the overlay, even with custom content - they are the only way a + finished update surfaces under the default AutoReload="false". A custom splash should not + render its own copy of those two; restyle them by id instead. + + +

      Standalone WebAssembly apps (like this site)

      +

      + In a standalone Blazor WebAssembly app the splash must exist before Blazor starts - on a first + install, Blazor only boots after the download completes - so the BswupProgress component + (which renders as part of the app) cannot paint the first-install splash. Hand-write the same markup in + index.html instead; bit-bswup.progress.js self-initializes from the + data-bit-bswup-* attributes exactly as it does for the component: +

      + + + Keep #bit-bswup-reload and its status region outside the #bit-bswup + overlay (a finished background update must surface the button without revealing the splash), and give + the button its own z-index if your app has fixed chrome in the top-right corner. View the + source of this very page for a complete working example. + + + + +@code { + private const string HostReferencesCode = @" +... + +"; + + private const string ComponentCode = @""; + + private const string HandWrittenCode = @"
      +
      +

      Installing the app

      +
      +
      +
      +

      0%

      +
      +

      Update failed to install

      +

      +
      
      +            
      +        
      +
      +
      + + +"; +} diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/ScriptOptionsPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/ScriptOptionsPage.razor new file mode 100644 index 0000000000..5876bbdaf0 --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/Pages/ScriptOptionsPage.razor @@ -0,0 +1,175 @@ +@page "/script-options" + +Script Options - bit Bswup + +

      Script Options

      +

      + Every option of the bit-bswup.js script tag. All of them are optional - remove any + attribute to fall back to its default. +

      + + + +

      Quick reference

      +
      + + + + + + + + + + + + + + + + +
      AttributeDefaultSummary
      scope/Service worker scope; also namespaces the cache buckets.
      logwarnLog level: none, error, warn, info, verbose, debug.
      swservice-worker.jsPath of the service worker file.
      handlerbitBswupHandlerName of the global function receiving lifecycle events.
      blazorScriptauto-detectedPath of the Blazor entry script, when it lives at a non-default path.
      updateInterval0 (off)Seconds between automatic update checks.
      updateOnVisibilityfalseCheck for an update whenever the tab returns to the foreground.
      stallTimeout60Seconds of total service worker silence before a first install falls back to a network boot.
      persistStoragefalseRequest eviction-resistant storage at startup.
      optionsbitBswupName of a global object to read all of these settings from.
      +
      + +

      scope

      +

      + The scope of the service worker. Defaults to /. A service worker can only control URLs + beneath its own folder unless the server sends a Service-Worker-Allowed header, so if + your app is mounted on a sub-path (e.g. https://host/myapp/) set this to that sub-path. +

      +

      + If the browser refuses the configured scope, Bswup automatically retries with the default scope - + the folder containing the service worker script - so the app keeps offline support rather than losing + the service worker entirely; the fallback is reported as a console warning. +

      + + The scope also namespaces the caches: buckets are named bit-bswup:<scope-path> - <version>, + so several Bswup apps mounted under different scopes on one origin keep fully independent caches. + The previous scope-less name made sibling apps evict each other's caches on every update. On upgrade, + entries from a legacy-named bucket are migrated without re-downloading. + + +

      log

      +

      + The log level of the Bswup logger. Available options: none, error, + warn, info, verbose, and debug. Each level + includes everything above it (e.g. info also shows warn and + error). Defaults to warn; use none to silence all output. +

      + +

      sw

      +

      The file path of the service worker file. Defaults to service-worker.js.

      + +

      handler

      +

      + The name of the global handler function for service worker events. Defaults to + bitBswupHandler - which is also the name the built-in progress script registers, so the + two wire up without configuration. The handler is re-resolved until found, so it may be registered + after bit-bswup.js loads. +

      + + If no handler is ever registered, Bswup still completes a first install on its own - it drives the + finish handshake itself so the app boots instead of waiting for the stall watchdog. Updates are simply + left staged until the next full restart. + + +

      blazorScript

      +

      + The path of the Blazor entry-point script (the one you added autostart="false" to). + When omitted, Bswup auto-detects both the Blazor Web App script (_framework/blazor.web.js) + and the standalone Blazor WebAssembly script (_framework/blazor.webassembly.js), so you + only need to set this if your script lives at a non-default path. +

      +

      + Matching is fingerprint-tolerant: the fingerprinted names .NET 9+ emits when the script is referenced + through @@Assets["..."] / the ImportMap (e.g. _framework/blazor.web.<fingerprint>.js) + are recognized automatically, both for the auto-detected defaults and for an explicitly configured value. +

      + +

      updateInterval

      +

      + Number of seconds between automatic update checks. By default the browser only re-checks the service + worker on navigation and roughly every 24 hours, so a long-lived SPA tab can run a stale version for a + long time. Set a positive number (e.g. 3600 for hourly) to have Bswup call + reg.update() on a timer. Checks are skipped while the tab is in the background and resume + when it becomes visible again. Omit or set 0 to disable (the default). +

      + +

      updateOnVisibility

      +

      + When true, Bswup checks for an update every time the tab returns to the foreground (the + visibilitychange event) - a lightweight way to catch updates right when a user comes back + to a tab they left open. Disabled by default. +

      + +

      stallTimeout

      +

      + Number of seconds of complete service worker silence (no message, no lifecycle event) after + which, on a first install only, Bswup stops waiting and starts Blazor directly from + the network. This is the last line of defense against install failures that report nothing - most + notably the browser terminating the service worker mid-install (Chromium kills installs after + ~5 minutes) - which would otherwise leave the app frozen behind the splash forever. +

      +

      + The page is uncontrolled at that point, so it behaves exactly as if no service worker existed, and the + install is retried on the next load. Every progress message resets the timer, so a slow-but-healthy + download never triggers it - only true silence does. Defaults to 60; set 0 + to disable. Updates are unaffected: the app is already running when an update stalls. +

      + +

      persistStorage

      +

      + When true, Bswup asks the browser to make the origin's storage persistent + (navigator.storage.persist()) at startup. By default everything Bswup caches lives in + best-effort storage: browsers silently reclaim it under disk pressure, and Safari deletes + all storage for a site that has not been interacted with for seven days - the user + comes back offline to an app that no longer boots. Persistent storage exempts the origin from that + eviction. +

      + + This is disabled by default because the request can show a permission prompt (Firefox) and grant odds + are engagement-based elsewhere. For the best odds, leave it off and call + BitBswup.persistStorage() yourself from a user + gesture - after login, or from an "install app" button. + + +

      options

      +

      + The name of a global configuration object to read settings from. Defaults to bitBswup. + Every option above can also be supplied as a property of that object; the object is merged over the + built-in defaults first, and any script-tag attribute then overrides the matching property. This is + the way to configure Bswup when the script is injected dynamically, where attribute-based + configuration may not be readable. +

      + + + + +@code { + private const string ScriptTagCode = @""; + + private const string OptionsObjectCode = @" + +"; +} diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/ServiceWorkerPage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/ServiceWorkerPage.razor new file mode 100644 index 0000000000..4c5f7f2bc5 --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/Pages/ServiceWorkerPage.razor @@ -0,0 +1,336 @@ +@page "/service-worker" + +Service Worker Settings - bit Bswup + +

      Service Worker Settings

      +

      + Everything you can configure in service-worker.js before importing the Bswup engine. +

      + + + +

      + The most important line is the last one - the only mandatory config in this file: +

      + + + +

      + Unlike the assets in service-worker-assets.js (which Bswup verifies with Subresource + Integrity), the service worker script itself cannot be integrity-pinned: browsers support neither an + integrity option on register() nor SRI for importScripts(). + A tampered service-worker.js or bit-bswup.sw.js is effectively persistent, + fully-privileged XSS. Serve both over HTTPS from an origin you control and apply a strict + Content-Security-Policy. +

      +

      + Bswup registers with updateViaCache: 'none' so update checks bypass the HTTP cache for + the whole service-worker.jsbit-bswup.sw.jsservice-worker-assets.js + import chain. As defense-in-depth (support is uneven on older Safari/iOS, and proxies are not bound + by it), also send Cache-Control: no-cache for these two files so every fetch + revalidates against the origin. +

      +
      + +

      How the URL-matching lists are matched

      +

      + assetsInclude, assetsExclude, prohibitedUrls, + serverHandledUrls and serverRenderedUrls all accept the same two kinds of entry: +

      +
        +
      • a RegExp (e.g. /\/admin\//) is used as the pattern it is - what you want for anything non-trivial;
      • +
      • a string (e.g. '/admin/') is matched literally as a substring of the URL. It is regex-escaped, so 'v1.0' matches only v1.0 and never v1X0.
      • +
      + + 'app.css' matches /css/app.css anywhere in the URL; anchor with a + RegExp such as /\/app\.css$/ if that is too broad. Prefer a RegExp + whenever you need anchoring, alternation or wildcards. Also note: before v-10-6-0 string entries were + silently ignored - see the migration guide. + + +

      Asset selection

      + +

      assetsInclude

      +

      The list of file names from the assets list to include when Bswup stores them in cache storage (regex supported).

      + +

      assetsExclude

      +

      The list of file names from the assets list to exclude when Bswup stores them in cache storage (regex supported).

      + +

      ignoreDefaultInclude / ignoreDefaultExclude

      +

      Ignore the default include / exclude arrays Bswup provides. The defaults are:

      + +

      + /\.wasm/, /\.pdb/ and /\.html/ are deliberately unanchored + (mirroring the standard Blazor template), so variants such as foo.wasm.br also match. + The default excludes keep the service worker's own scripts out of the cache: +

      + + + Caching service-worker-related files corrupts the update cycle of the service worker. Only the browser + should handle these files. + + +

      externalAssets

      +

      + The list of external assets to cache that are not included in the auto-generated assets file. For + example, if you're not using index.html (like _host.cshtml), add + { "url": "/" }. +

      + +

      Accepted entry shapes and behaviors:

      +
        +
      • An object with a url (a concrete string, or a RegExp for server-generated names unknown ahead of time), a bare string, or a bare RegExp; a single value also works without the array.
      • +
      • An entry may carry a hash (an SRI digest, sha256-...) that participates in ?v= cache busting and - when enableIntegrityCheck is on - in integrity verification, exactly like a manifest asset.
      • +
      • Entries whose url cannot be parsed are skipped with a non-fatal request error instead of breaking the worker.
      • +
      • Cross-origin entries are fetched in CORS mode first; when the host sends no CORS headers, Bswup retries with no-cors and caches the resulting opaque response so the asset still works offline. This fallback is skipped for integrity-checked assets. Browsers pad opaque responses in quota accounting (Chromium reserves several megabytes per entry), so prefer CORS-enabled hosts when you control them.
      • +
      • Media works too: requests carrying a Range header are answered with a real 206 Partial Content sliced from the cached full body (Safari refuses media served as 200 for a ranged request); uncached ranged requests pass through with their Range intact, and partial responses are never written to the cache.
      • +
      • Entries cached for RegExp patterns are kept across updates so the app still boots offline, but only the newest three generations per pattern survive each update.
      • +
      + +

      Navigation & URLs

      + +

      defaultUrl

      +

      + The default page URL, served from cache for navigation requests (the SPA fallback). Defaults to + index.html; use / when using _Host.cshtml. The value must match + an entry that actually exists in service-worker-assets.js or externalAssets; + the comparison uses resolved URLs, so equivalent spellings match. When nothing matches, + offline navigation cannot work and Bswup logs a defaultUrl ... matches no asset warning + at startup. +

      +
        +
      • Navigations whose URL is itself a managed asset are served that asset instead of the default document (changed in v-10-6-0): opening /manifest.json directly shows that file, while route URLs (/counter, ...) still get the app shell.
      • +
      • If your host answers the shell URL with a redirect (e.g. //index.html, common on Cloudflare Pages and Netlify), Bswup rebuilds that response so offline deep-link navigation keeps working instead of failing with a redirect-mode error.
      • +
      + +

      assetsUrl

      +

      + The path of the compile-time assets manifest (default file name service-worker-assets.js). + The default is resolved relative to the service worker script's own location - which is also where + Blazor publishes the file - so it works unchanged for apps mounted on a sub-path. Set it explicitly + only when the file lives somewhere else; a leading / makes the path origin-absolute. +

      + +

      prohibitedUrls

      +

      + URLs that should not be accessed (regex supported). Matching requests are answered by the service + worker with 403 Forbidden for every HTTP method (changed in v-10-6-0 - previously + 405). +

      + + Enforcement happens only inside the service worker, which is bypassed whenever the page is not + controlled (the very first visit, a hard reload, browsers without service worker support) and by any + client that talks to the server directly. Access control must be enforced on the server. + + +

      serverHandledUrls

      +

      URLs that skip the service worker offline pipeline entirely and are handled only by the server (regex supported) - e.g. /api, /swagger.

      + +

      serverRenderedUrls

      +

      URLs that should be rendered by the server rather than the client while navigating (regex supported) - e.g. /about.html, /privacy.

      + +

      caseInsensitiveUrl

      +

      + Enables case-insensitive URL checking - for asset cache matching and every URL-matching regex + list: when enabled, patterns are compiled with the i flag so e.g. + prohibitedUrls: [/\/admin\//] also blocks /ADMIN/. +

      + +

      noPrerenderQuery

      +

      + The query string attached to the default document request to disable server prerendering, so an + unwanted prerendered result is not cached - e.g. no-prerender=true. +

      + +

      forcePrerender

      +

      + Forces prerendering of the default document for every navigation request to ensure the server always + has the latest version of the app. Useful for server-rendered apps. +

      + +

      Install behavior & resiliency

      + +

      isPassive

      +

      + Enables passive mode: assets are not cached in advance but upon first request. Passive mode does not + skip the full download entirely - on a first install, once Blazor has started, the service + worker still tops up the cache in the background with every asset not yet fetched, so the app ends up + fully offline-capable. What passive mode buys is that the first paint is never blocked behind a full + precache. Assets lazily fetched by the app while the top-up runs can be downloaded twice in that + window - a bandwidth cost, not a correctness issue. +

      + +

      errorTolerance

      +

      Controls how the service worker reacts to asset download / cache failures during install:

      +
      + + + + + + + + + + + + + + +
      ValueBehavior
      lax (default) + Best-effort install. Asset failures never fail the install; missing assets are filled in + lazily on first fetch. Failures are reported through the error event with + fatal: false and still count toward progress so the bar reaches 100%. + Tolerates optional externalAssets that may legitimately 404, and avoids + leaving a first visit with no service worker to complete the startup handshake. The + download runs under the install event's waitUntil, so + updateReady is only announced once the background fill has settled. +
      strict + Mirrors the standard Microsoft template / Workbox behavior. If any required asset fails, + the install rejects, the partial cache is discarded, and the previous service worker (if + any) keeps serving. Failed assets are not counted toward progress, so 100% means + every asset succeeded; the abort is reported with reason: 'install-aborted' + and fatal: true. On a first install (no previous version to fall back to) + Bswup starts the app without a service worker so it still boots, and the install is + retried on the next load. +
      +
      + +

      maxRetries / retryDelay

      +

      + maxRetries (default 2) is the number of additional download attempts + after the first when an asset fails transiently during install (a rejected fetch, or HTTP 408/429/5xx). + Deterministic failures - 404/403 and other permanent statuses, and SRI mismatches - are never retried, + since identical bytes would fail identically. +

      +

      + retryDelay (default 300) is the base backoff in milliseconds: attempt + n waits retryDelay * 2^(n-1) plus random jitter, so a mass failure doesn't re-hit + the origin in one synchronized burst. +

      + +

      enableIntegrityCheck

      +

      + Enables the browsers' built-in integrity verification by setting the integrity attribute + on the requests the service worker creates to fetch assets. +

      + +

      Caching & updates

      + +

      cacheVersion

      +

      + Overrides the value used to name the cache bucket (bit-bswup:<scope-path> - <version>). + By default this tracks Blazor's assetsManifest.version (a hash over the published assets), + so the cache rotates automatically whenever any asset hash changes - and only then. Set it to take + manual control: +

      +
        +
      • Pin it to a stable string so noisy dev rebuilds don't needlessly evict the whole cache.
      • +
      • Bump it to force a refresh when a meaningful change lives outside Blazor's asset manifest.
      • +
      • Feed it a build-stamped value (commit SHA, build timestamp) so it bumps automatically per publish.
      • +
      +

      + Only the cache bucket name is affected: per-asset ?v= cache busting and Subresource + Integrity keep using each asset's own hash. +

      + +

      disableHashlessAssetsUpdate

      +

      + Disables the automatic re-download of hash-less assets (e.g. external assets) that Bswup performs every + time an update is found. +

      + +

      enableCacheControl

      +

      + Adds cache-busting to each asset request (cache: 'no-store' plus a + cache-control: no-cache header). The header is only attached to same-origin requests: it + is not CORS-safelisted, so on a cross-origin asset it would force a preflight most third-party hosts + reject; cross-origin requests rely on the cache: no-store option alone. +

      + +

      Diagnostics

      +
        +
      • enableDiagnostics: pushes service worker logs to the browser console.
      • +
      • enableFetchDiagnostics: additionally logs every fetch event the worker handles.
      • +
      + +

      Modes

      +

      + A mode is a preset bundle of defaults for the individual settings above + (isPassive, defaultUrl, forcePrerender, + errorTolerance, caseInsensitiveUrl, noPrerenderQuery): it only + fills settings you have not assigned yourself, so any explicit assignment always wins over the preset - + including explicit falsy values such as caseInsensitiveUrl = false. +

      +
      + + + + + + + + + + +
      ModeBehavior
      NoPrerenderDisables prerendering of the default document for every navigation request.
      InitialPrerenderEnables prerendering of the default document only for the initial navigation request.
      AlwaysPrerenderEnables prerendering of the default document for every navigation request.
      FullOfflineFull offline mode: all assets are cached and served from cache from the first time the app loads.
      +
      + + + +@code { + private const string FullConfigCode = @"self.assetsInclude = [/\/data\.db$/]; +self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; +self.defaultUrl = '/'; +self.prohibitedUrls = [/\/admin\//]; +self.serverHandledUrls = [/\/api\//]; +self.serverRenderedUrls = [/\/privacy$/]; +self.externalAssets = [ + { + ""url"": ""/"" + }, + { + ""url"": ""https://www.googletagmanager.com/gtag/js?id=G-G123456789"" + } +]; +self.assetsUrl = '/service-worker-assets.js'; +self.noPrerenderQuery = 'no-prerender=true'; +self.cacheVersion = '2026.05.31-abc1234'; + +self.caseInsensitiveUrl = true; +self.ignoreDefaultInclude = true; +self.ignoreDefaultExclude = true; +self.isPassive = true; +self.enableIntegrityCheck = true; +self.enableDiagnostics = true; +self.enableFetchDiagnostics = true; + +self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js');"; + + private const string DefaultIncludeCode = @"[/\.dll$/, /\.wasm/, /\.pdb/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, + /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/, /\.svg$/, /\.woff2$/, /\.ttf$/, /\.webp$/]"; + + private const string DefaultExcludeCode = @"[ + /^_content\/Bit\.Bswup\/bit-bswup\.sw\.js$/, + /^_content\/Bit\.Bswup\/bit-bswup\.sw\.min\.js$/, + /^_content\/Bit\.Bswup\/bit-bswup\.sw-cleanup\.js$/, + /^_content\/Bit\.Bswup\/bit-bswup\.sw-cleanup\.min\.js$/, + /^service-worker\.js$/, +]"; + + private const string ExternalAssetsCode = @"self.externalAssets = [ + { ""url"": ""/"" }, // the host page itself (e.g. _host.cshtml) + ""https://fonts.example.com/app-font.woff2"", // bare string shorthand + { ""url"": /_framework\/resource-collection\..*\.js/ }, // RegExp for server-generated names + { ""url"": ""/lib/chart.js"", ""hash"": ""sha256-..."" } // with SRI hash (cache busting + integrity) +];"; +} diff --git a/src/Bswup/Bit.Bswup.Demo/Properties/launchSettings.json b/src/Bswup/Bit.Bswup.Demo/Properties/launchSettings.json index 8093b9bbbe..e4eff42357 100644 --- a/src/Bswup/Bit.Bswup.Demo/Properties/launchSettings.json +++ b/src/Bswup/Bit.Bswup.Demo/Properties/launchSettings.json @@ -1,4 +1,4 @@ -{ +{ "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "Bit.Bswup.Demo": { @@ -6,10 +6,10 @@ "dotnetRunMessages": true, "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "http://localhost:5020;https://localhost:5021", + "applicationUrl": "http://localhost:5024;https://localhost:5025", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } -} \ No newline at end of file +} diff --git a/src/Bswup/Bit.Bswup.Demo/Shared/Callout.razor b/src/Bswup/Bit.Bswup.Demo/Shared/Callout.razor new file mode 100644 index 0000000000..e1f393e632 --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/Shared/Callout.razor @@ -0,0 +1,19 @@ +
      +
      @(Title ?? DefaultTitle)
      +
      @ChildContent
      +
      + +@code { + /// note | tip | warning | danger + [Parameter] public string Type { get; set; } = "note"; + [Parameter] public string? Title { get; set; } + [Parameter] public RenderFragment? ChildContent { get; set; } + + private string DefaultTitle => Type switch + { + "tip" => "Tip", + "warning" => "Warning", + "danger" => "Caution", + _ => "Note" + }; +} diff --git a/src/Bswup/Bit.Bswup.Demo/Shared/CodeBlock.razor b/src/Bswup/Bit.Bswup.Demo/Shared/CodeBlock.razor new file mode 100644 index 0000000000..27100dc669 --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/Shared/CodeBlock.razor @@ -0,0 +1,12 @@ +
      +
      + @Language + +
      +
      @Code.Trim('\r', '\n')
      +
      + +@code { + [Parameter] public string Code { get; set; } = string.Empty; + [Parameter] public string Language { get; set; } = "code"; +} diff --git a/src/Bswup/Bit.Bswup.Demo/Shared/MainLayout.razor b/src/Bswup/Bit.Bswup.Demo/Shared/MainLayout.razor new file mode 100644 index 0000000000..a9aff959b2 --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/Shared/MainLayout.razor @@ -0,0 +1,34 @@ +@inherits LayoutComponentBase + +
      + + + bit platform logo + bit Bswup + + v10.5.0 +
      + NuGet + GitHub + +
      + + +
      + +
      +
      + @Body +
      + +
      diff --git a/src/Bswup/Bit.Bswup.Demo/Shared/NavMenu.razor b/src/Bswup/Bit.Bswup.Demo/Shared/NavMenu.razor new file mode 100644 index 0000000000..a02408b3ba --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/Shared/NavMenu.razor @@ -0,0 +1,22 @@ +@* Clicking any link also closes the mobile sidebar; on desktop the class toggle is a no-op. + Wired via data-demo-action (see the delegated listener in demo.js) - no inline onclick, + so the site works under a strict Content-Security-Policy. *@ + diff --git a/src/Bswup/Bit.Bswup.Demo/_Imports.razor b/src/Bswup/Bit.Bswup.Demo/_Imports.razor index 772684f47c..c24caf5fb9 100644 --- a/src/Bswup/Bit.Bswup.Demo/_Imports.razor +++ b/src/Bswup/Bit.Bswup.Demo/_Imports.razor @@ -7,3 +7,4 @@ @using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.JSInterop @using Bit.Bswup.Demo +@using Bit.Bswup.Demo.Shared diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/app.css b/src/Bswup/Bit.Bswup.Demo/wwwroot/app.css new file mode 100644 index 0000000000..d559eea45f --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/app.css @@ -0,0 +1,842 @@ +/* ============================================================================ + bit Bswup docs - design system + Light/dark theming via CSS custom properties; the html[data-theme="dark"] + attribute is set before first paint by the inline script in index.html. + ============================================================================ */ + +:root { + --brand: #0065ef; + --brand-strong: #004fc0; + --brand-soft: rgba(0, 101, 239, 0.10); + --navy: #03173d; + + --bg: #ffffff; + --bg-soft: #f6f8fb; + --bg-header: rgba(255, 255, 255, 0.85); + --surface: #ffffff; + --border: #e3e8f0; + --text: #1c2433; + --text-soft: #55627a; + --text-faint: #8b95a9; + --code-bg: #0e1726; + --code-text: #dce4f2; + --inline-code-bg: #eef2f8; + --inline-code-text: #b02a63; + --shadow: 0 1px 3px rgba(16, 24, 40, 0.07), 0 8px 24px rgba(16, 24, 40, 0.05); + + --note: #0065ef; + --tip: #0e9f6e; + --warning: #c27803; + --danger: #d92d20; + + --header-height: 3.75rem; + --sidebar-width: 17rem; + --content-width: 52rem; +} + +html[data-theme="dark"] { + --brand: #4d94ff; + --brand-strong: #77adff; + --brand-soft: rgba(77, 148, 255, 0.14); + + --bg: #0b1220; + --bg-soft: #101a2c; + --bg-header: rgba(11, 18, 32, 0.85); + --surface: #121d31; + --border: #223047; + --text: #e6ecf5; + --text-soft: #a7b4c9; + --text-faint: #6d7c94; + --code-bg: #0a111e; + --code-text: #d7e1f0; + --inline-code-bg: #1b2940; + --inline-code-text: #ff8fb8; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.3); +} + +* { + box-sizing: border-box; +} + +html { + scroll-padding-top: calc(var(--header-height) + 1rem); +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: "Segoe UI", "Segoe UI Web (West European)", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; + font-size: 1rem; + line-height: 1.65; + -webkit-text-size-adjust: 100%; +} + +/* ---------------------------------------------------------------- boot screen */ + +.app-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1rem; + min-height: 100vh; + color: var(--text-soft); +} + +/* ---------------------------------------------------------------- Bswup splash + Branded overrides on top of _content/Bit.Bswup/bit-bswup.progress.css. */ + +#bit-bswup { + background: var(--navy); + background: linear-gradient(160deg, #03173d 0%, #0a2a66 60%, #0065ef 160%); + color: #eaf1ff; +} + +.splash-container { + max-width: 26rem; + margin-top: 22vh; +} + +.splash-logo { + border-radius: 1.25rem; + margin-bottom: 0.75rem; +} + +#bit-bswup .bit-bswup-title { + font-size: 1.5rem; + font-weight: 600; + margin: 0.25rem 0; +} + +#bit-bswup .bit-bswup-description { + color: #b9cdf3; +} + +#bit-bswup .bit-bswup-progress { + background-color: rgba(255, 255, 255, 0.18); + border-radius: 999px; + height: 0.375rem; + overflow: hidden; +} + +#bit-bswup #bit-bswup-progress-bar { + background: #ffffff; + height: 0.375rem; + border-radius: 999px; + transition: width 0.2s ease; +} + +#bit-bswup #bit-bswup-percent { + color: #ffffff; + font-variant-numeric: tabular-nums; +} + +#bit-bswup-reload { + background: var(--brand); + color: #ffffff; + border: none; + border-radius: 999px; + padding: 0.625rem 1.25rem; + font-size: 0.9375rem; + font-weight: 600; + cursor: pointer; + box-shadow: var(--shadow); +} + +#bit-bswup-reload:hover { + background: var(--brand-strong); +} + +/* ---------------------------------------------------------------- shell */ + +.docs-header { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 100; + display: flex; + align-items: center; + gap: 0.75rem; + height: var(--header-height); + padding: 0 1rem; + background: var(--bg-header); + backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border); +} + +.brand { + display: flex; + align-items: center; + gap: 0.625rem; + text-decoration: none; + color: var(--text); + font-weight: 700; + font-size: 1.125rem; +} + +.brand img { + width: 2rem; + height: 2rem; + border-radius: 0.5rem; +} + +.brand-product { + color: var(--text-faint); + font-weight: 400; +} + +.version-badge { + font-size: 0.75rem; + font-weight: 600; + color: var(--brand); + background: var(--brand-soft); + border-radius: 999px; + padding: 0.125rem 0.625rem; +} + +.header-spacer { + flex: 1; +} + +.header-link { + color: var(--text-soft); + text-decoration: none; + font-size: 0.875rem; + padding: 0.375rem 0.625rem; + border-radius: 0.5rem; +} + +.header-link:hover { + color: var(--text); + background: var(--bg-soft); +} + +.icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.25rem; + height: 2.25rem; + border: 1px solid var(--border); + border-radius: 0.5rem; + background: transparent; + color: var(--text-soft); + cursor: pointer; + font-size: 1rem; +} + +.icon-button:hover { + color: var(--text); + background: var(--bg-soft); +} + +.menu-button { + display: none; +} + +.theme-icon-dark, +html[data-theme="dark"] .theme-icon-light { + display: none; +} + +html[data-theme="dark"] .theme-icon-dark { + display: inline; +} + +/* ---------------------------------------------------------------- sidebar */ + +.docs-sidebar { + position: fixed; + top: var(--header-height); + bottom: 0; + left: 0; + width: var(--sidebar-width); + overflow-y: auto; + padding: 1.25rem 0.875rem 2rem; + border-right: 1px solid var(--border); + background: var(--bg); +} + +.docs-nav { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.nav-section { + margin: 1.25rem 0.625rem 0.375rem; + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-faint); +} + +.nav-section:first-child { + margin-top: 0; +} + +.docs-nav a { + display: block; + padding: 0.4375rem 0.625rem; + border-radius: 0.5rem; + color: var(--text-soft); + text-decoration: none; + font-size: 0.9063rem; +} + +.docs-nav a:hover { + color: var(--text); + background: var(--bg-soft); +} + +.docs-nav a.active { + color: var(--brand); + background: var(--brand-soft); + font-weight: 600; +} + +.docs-sidebar-backdrop { + display: none; +} + +/* ---------------------------------------------------------------- main */ + +.docs-main { + padding: calc(var(--header-height) + 2rem) 2rem 2rem; + margin-left: var(--sidebar-width); +} + +.docs-content { + max-width: var(--content-width); + margin: 0 auto; +} + +.docs-footer { + max-width: var(--content-width); + margin: 4rem auto 0; + padding-top: 1.25rem; + border-top: 1px solid var(--border); + display: flex; + flex-wrap: wrap; + gap: 0.5rem 1.5rem; + color: var(--text-faint); + font-size: 0.8438rem; +} + +.docs-footer a { + color: var(--text-soft); +} + +/* ---------------------------------------------------------------- typography */ + +.docs-content h1 { + font-size: 2.125rem; + line-height: 1.2; + letter-spacing: -0.02em; + margin: 0 0 0.75rem; +} + +.docs-content h2 { + font-size: 1.5rem; + letter-spacing: -0.01em; + margin: 2.5rem 0 0.75rem; + padding-top: 1.25rem; + border-top: 1px solid var(--border); +} + +.docs-content h3 { + font-size: 1.1875rem; + margin: 1.75rem 0 0.5rem; +} + +.docs-content a { + color: var(--brand); + text-decoration: none; +} + +.docs-content a:hover { + text-decoration: underline; +} + +.docs-lead { + font-size: 1.125rem; + color: var(--text-soft); + margin: 0 0 1.5rem; +} + +.docs-content code:not(pre code) { + font-family: Consolas, "Cascadia Code", Menlo, monospace; + font-size: 0.85em; + background: var(--inline-code-bg); + color: var(--inline-code-text); + border-radius: 0.3125rem; + padding: 0.125rem 0.375rem; +} + +/* ---------------------------------------------------------------- tables */ + +.docs-table-wrapper { + overflow-x: auto; + margin: 1rem 0; + border: 1px solid var(--border); + border-radius: 0.75rem; +} + +.docs-content table { + width: 100%; + border-collapse: collapse; + font-size: 0.9063rem; +} + +.docs-content th { + text-align: left; + font-size: 0.8125rem; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-faint); + background: var(--bg-soft); + padding: 0.625rem 0.875rem; + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +.docs-content td { + padding: 0.625rem 0.875rem; + border-bottom: 1px solid var(--border); + vertical-align: top; +} + +.docs-content tr:last-child td { + border-bottom: none; +} + +.docs-content td:first-child { + white-space: nowrap; +} + +/* ---------------------------------------------------------------- code blocks */ + +.code-block { + margin: 1rem 0; + border-radius: 0.75rem; + overflow: hidden; + border: 1px solid var(--border); + box-shadow: var(--shadow); +} + +.code-block-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.375rem 0.5rem 0.375rem 1rem; + background: var(--code-bg); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.code-block-lang { + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: #7d8db0; +} + +.code-block-copy { + border: 1px solid rgba(255, 255, 255, 0.15); + background: transparent; + color: #aebad2; + font-size: 0.75rem; + border-radius: 0.375rem; + padding: 0.25rem 0.625rem; + cursor: pointer; +} + +.code-block-copy:hover { + background: rgba(255, 255, 255, 0.08); + color: #ffffff; +} + +.code-block pre { + margin: 0; + padding: 1rem; + overflow-x: auto; + background: var(--code-bg); +} + +.code-block code { + font-family: Consolas, "Cascadia Code", Menlo, monospace; + font-size: 0.8438rem; + line-height: 1.6; + color: var(--code-text); + white-space: pre; +} + +/* ---------------------------------------------------------------- callouts */ + +.callout { + display: grid; + gap: 0.25rem; + margin: 1.25rem 0; + padding: 0.875rem 1rem; + border-radius: 0.75rem; + border: 1px solid var(--border); + border-left: 0.25rem solid var(--note); + background: var(--bg-soft); + font-size: 0.9375rem; +} + +.callout-title { + font-weight: 700; + font-size: 0.875rem; +} + +.callout-note { border-left-color: var(--note); } +.callout-note .callout-title { color: var(--note); } +.callout-tip { border-left-color: var(--tip); } +.callout-tip .callout-title { color: var(--tip); } +.callout-warning { border-left-color: var(--warning); } +.callout-warning .callout-title { color: var(--warning); } +.callout-danger { border-left-color: var(--danger); } +.callout-danger .callout-title { color: var(--danger); } + +.callout p { + margin: 0.25rem 0; +} + +/* ---------------------------------------------------------------- buttons */ + +.button-primary, +.button-secondary { + display: inline-flex; + align-items: center; + gap: 0.5rem; + border-radius: 0.625rem; + padding: 0.625rem 1.25rem; + font-size: 0.9375rem; + font-weight: 600; + cursor: pointer; + text-decoration: none; + border: 1px solid transparent; +} + +.button-primary { + background: var(--brand); + color: #ffffff !important; +} + +.button-primary:hover { + background: var(--brand-strong); + text-decoration: none !important; +} + +.button-secondary { + background: var(--surface); + color: var(--text); + border-color: var(--border); +} + +.button-secondary:hover { + background: var(--bg-soft); + text-decoration: none !important; +} + +.button-danger { + border-color: var(--danger); + color: var(--danger); +} + +/* ---------------------------------------------------------------- home page */ + +.hero { + padding: 2.5rem 0 1rem; + text-align: center; +} + +.hero-badge { + display: inline-block; + font-size: 0.8125rem; + font-weight: 600; + color: var(--brand); + background: var(--brand-soft); + border-radius: 999px; + padding: 0.25rem 0.875rem; + margin-bottom: 1.25rem; +} + +.hero h1 { + font-size: 2.875rem; + line-height: 1.1; + letter-spacing: -0.03em; + margin: 0 0 1rem; +} + +.hero .accent { + color: var(--brand); +} + +.hero p { + max-width: 38rem; + margin: 0 auto 1.75rem; + font-size: 1.1875rem; + color: var(--text-soft); +} + +.hero-actions { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.hero-install { + display: inline-flex; + align-items: center; + gap: 0.75rem; + margin-top: 0.75rem; + font-family: Consolas, "Cascadia Code", Menlo, monospace; + font-size: 0.9063rem; + background: var(--code-bg); + color: var(--code-text); + border-radius: 0.625rem; + padding: 0.625rem 1.125rem; +} + +.feature-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); + gap: 1rem; + margin: 2.5rem 0; +} + +.feature-card { + display: block; + padding: 1.125rem 1.25rem; + border: 1px solid var(--border); + border-radius: 0.875rem; + background: var(--surface); + box-shadow: var(--shadow); + color: var(--text) !important; + text-decoration: none !important; +} + +.feature-card:hover { + border-color: var(--brand); + transform: translateY(-2px); + transition: transform 0.15s ease, border-color 0.15s ease; +} + +.feature-icon { + font-size: 1.5rem; + line-height: 1; + margin-bottom: 0.625rem; +} + +.feature-card h3 { + margin: 0 0 0.375rem; + font-size: 1.0313rem; +} + +.feature-card p { + margin: 0; + font-size: 0.875rem; + color: var(--text-soft); +} + +/* ---------------------------------------------------------------- playground */ + +.playground-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr)); + gap: 1rem; + margin: 1.5rem 0; +} + +.playground-card { + border: 1px solid var(--border); + border-radius: 0.875rem; + background: var(--surface); + box-shadow: var(--shadow); + padding: 1.125rem 1.25rem; +} + +.playground-card h3 { + margin: 0 0 0.75rem; + font-size: 1.0313rem; +} + +.playground-card .actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.75rem; +} + +.status-list { + margin: 0; + display: grid; + gap: 0.375rem; + font-size: 0.9063rem; +} + +.status-list > div { + display: flex; + justify-content: space-between; + gap: 1rem; +} + +.status-list dt { + color: var(--text-soft); +} + +.status-list dd { + margin: 0; + font-weight: 600; + text-align: right; + overflow-wrap: anywhere; +} + +.cache-list { + margin: 0.5rem 0 0; + padding-left: 1.25rem; + font-family: Consolas, "Cascadia Code", Menlo, monospace; + font-size: 0.8125rem; + color: var(--text-soft); +} + +.event-log { + list-style: none; + margin: 0.75rem 0 0; + padding: 0.5rem; + max-height: 22rem; + overflow-y: auto; + border: 1px solid var(--border); + border-radius: 0.75rem; + background: var(--bg-soft); + font-family: Consolas, "Cascadia Code", Menlo, monospace; + font-size: 0.8125rem; +} + +.event-log li { + display: flex; + gap: 0.625rem; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + align-items: baseline; +} + +.event-log li:nth-child(odd) { + background: var(--bg); +} + +.event-time { + color: var(--text-faint); + flex-shrink: 0; +} + +.event-type { + color: var(--brand); + flex-shrink: 0; +} + +.event-detail { + color: var(--text-soft); + overflow-wrap: anywhere; +} + +.event-empty { + color: var(--text-faint); +} + +/* ---------------------------------------------------------------- misc */ + +.docs-not-found { + text-align: center; + padding: 4rem 0; +} + +.docs-not-found h1 { + font-size: 4rem; + margin-bottom: 0; +} + +.pager { + display: flex; + justify-content: space-between; + gap: 1rem; + margin-top: 3rem; +} + +.pager a { + display: inline-flex; + flex-direction: column; + gap: 0.125rem; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + border-radius: 0.75rem; + text-decoration: none !important; + color: var(--text) !important; + min-width: 10rem; +} + +.pager a:hover { + border-color: var(--brand); +} + +.pager .pager-label { + font-size: 0.75rem; + color: var(--text-faint); +} + +.pager .pager-next { + text-align: right; + margin-left: auto; +} + +.pager b { + color: var(--brand); +} + +/* ---------------------------------------------------------------- responsive */ + +@media (max-width: 60rem) { + .menu-button { + display: inline-flex; + } + + .header-link { + display: none; + } + + .docs-sidebar { + transform: translateX(-100%); + transition: transform 0.2s ease; + z-index: 90; + box-shadow: none; + } + + body.sidebar-open .docs-sidebar { + transform: translateX(0); + box-shadow: var(--shadow); + } + + body.sidebar-open .docs-sidebar-backdrop { + display: block; + position: fixed; + inset: var(--header-height) 0 0 0; + z-index: 80; + background: rgba(3, 10, 24, 0.45); + } + + .docs-main { + margin-left: 0; + padding: calc(var(--header-height) + 1.25rem) 1rem 1.5rem; + } + + .hero h1 { + font-size: 2.125rem; + } +} diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/demo.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/demo.js new file mode 100644 index 0000000000..5e38fc199f --- /dev/null +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/demo.js @@ -0,0 +1,266 @@ +// UI helpers for the Bswup docs site: theme, sidebar, code copy, and the Live Playground. +// Everything DOM-writing uses textContent / createElement - never innerHTML with data. +(function () { + 'use strict'; + + const MAX_EVENTS = 200; + const events = []; + // Total events ever recorded. renderEvents keys off this, not events.length: once the + // log reaches MAX_EVENTS its length stays pinned at the cap, and a length-based guard + // would treat every later event as "nothing changed" and freeze the UI. + let revision = 0; + + // ---------------------------------------------------------------- event log + + // Chained after the built-in bitBswupHandler via the data-bit-bswup-handler attribute + // in index.html, so every Bswup lifecycle message lands here too. + window.bswupDemoHandler = function (message, data) { + record(message, describe(message, data)); + }; + + function record(type, detail) { + events.unshift({ time: new Date(), type: type, detail: detail }); + if (events.length > MAX_EVENTS) events.length = MAX_EVENTS; + revision++; + renderEvents(); + } + + function describe(message, data) { + try { + switch (message) { + case 'DOWNLOAD_STARTED': + return `version: ${data && data.version ? data.version : '?'}${data && data.firstInstall === false ? ' (background update)' : ''}`; + case 'DOWNLOAD_PROGRESS': + return `${Math.round(Number(data.percent) || 0)}% - ${data.asset ? data.asset.url : ''}`; + case 'DOWNLOAD_FINISHED': + return data && data.firstInstall ? 'first install complete' : 'update staged, ready to activate'; + case 'STATE_CHANGED': + return `state: ${data && data.currentTarget ? data.currentTarget.state : '?'}`; + case 'ACTIVATE': + return `version: ${data && data.version ? data.version : '?'}`; + case 'ERROR': + return `[${data && data.reason ? data.reason : 'unknown'}] fatal: ${data ? data.fatal : '?'} - ${data && data.message ? data.message : ''}`; + case 'UPDATE_CHECK_FAILED': + return `[${data && data.reason ? data.reason : 'unknown'}] ${data && data.message ? data.message : ''}`; + default: + return ''; + } + } catch (err) { + return ''; + } + } + + function renderEvent(evt) { + const li = document.createElement('li'); + const time = document.createElement('span'); + time.className = 'event-time'; + time.textContent = evt.time.toLocaleTimeString(); + const type = document.createElement('b'); + type.className = 'event-type'; + type.textContent = evt.type; + const detail = document.createElement('span'); + detail.className = 'event-detail'; + detail.textContent = evt.detail; + li.append(time, type, detail); + return li; + } + + function renderEvents() { + const el = document.getElementById('bswup-demo-events'); + if (!el) return; + // Guard for the MutationObserver below: re-render only when something new was + // recorded, otherwise our own DOM writes would re-trigger the observer forever. + // A missing dataset.rendered means a fresh element (Blazor replaced the subtree) + // that still needs a full render even when the revision itself is unchanged. + if (el.dataset.rendered === String(revision)) return; + const prior = el.dataset.rendered === undefined ? -1 : Number(el.dataset.rendered); + el.dataset.rendered = String(revision); + + if (events.length === 0) { + el.textContent = ''; + const empty = document.createElement('li'); + empty.className = 'event-empty'; + empty.textContent = 'No events yet - Bswup raises events on install, update checks, downloads, and activation.'; + el.append(empty); + return; + } + + // Prepend only the entries recorded since this element was last rendered (they are + // the head of `events` - newest first). The list is an aria-live region, and a full + // clear-and-rebuild would make screen readers re-announce the entire log on every + // event; prepending keeps the announcement to just what is new. A fresh element or + // a gap wider than the cap falls back to rendering everything. + const fresh = prior < 0 || revision - prior >= events.length ? events.length : revision - prior; + if (fresh === events.length) el.textContent = ''; + for (let i = fresh - 1; i >= 0; i--) el.prepend(renderEvent(events[i])); + while (el.children.length > MAX_EVENTS) el.lastElementChild.remove(); + } + + // ---------------------------------------------------------------- playground + + function setText(id, value) { + const el = document.getElementById(id); + if (el) el.textContent = value; + } + + async function refreshStatus() { + if (!('serviceWorker' in navigator)) { + setText('pg-sw-supported', 'not supported'); + return; + } + setText('pg-sw-supported', 'supported'); + setText('pg-sw-controller', navigator.serviceWorker.controller ? 'this page is controlled' : 'this page is NOT controlled'); + try { + const reg = await navigator.serviceWorker.getRegistration(); + setText('pg-sw-scope', reg ? reg.scope : 'no registration'); + setText('pg-sw-state', reg && reg.active ? reg.active.state : 'none'); + setText('pg-sw-waiting', reg && reg.waiting ? 'yes - an update is staged' : 'no'); + } catch (err) { + setText('pg-sw-scope', String(err)); + } + try { + const persisted = navigator.storage && navigator.storage.persisted ? await navigator.storage.persisted() : undefined; + setText('pg-storage-persisted', persisted === undefined ? 'unknown' : (persisted ? 'persistent' : 'best-effort')); + } catch (err) { + setText('pg-storage-persisted', 'unknown'); + } + try { + const keys = await caches.keys(); + const list = document.getElementById('pg-cache-list'); + if (list) { + list.textContent = ''; + if (keys.length === 0) { + const li = document.createElement('li'); + li.textContent = '(no caches yet)'; + list.append(li); + } + for (const key of keys) { + const li = document.createElement('li'); + li.textContent = key; + list.append(li); + } + } + } catch (err) { /* CacheStorage unavailable (e.g. some private modes) */ } + } + + async function checkForUpdate() { + record('(playground)', 'BitBswup.checkForUpdate() called'); + if (window.BitBswup && BitBswup.checkForUpdate) { + try { + await BitBswup.checkForUpdate(); + } catch (err) { + // The registration-aware implementation reports failures through the + // UPDATE_CHECK_FAILED event (logged above by bswupDemoHandler), but the + // pre-registration fallback rejects directly when reg.update() fails + // (e.g. offline) - record it instead of an unhandled rejection. + record('(playground)', `BitBswup.checkForUpdate() rejected: ${err}`); + } + } + } + + async function skipWaiting() { + if (window.BitBswup && BitBswup.skipWaiting) { + const result = await BitBswup.skipWaiting(); + record('(playground)', `BitBswup.skipWaiting() returned ${result} (${result ? 'a staged update is being activated' : 'no update was waiting'})`); + } + } + + async function persistStorage() { + if (window.BitBswup && BitBswup.persistStorage) { + const result = await BitBswup.persistStorage(); + record('(playground)', `BitBswup.persistStorage() resolved ${result}`); + refreshStatus(); + } + } + + function forceRefresh() { + if (!confirm('Force refresh clears this app\'s caches, unregisters its service worker, and reloads the page. Continue?')) return; + if (window.BitBswup && BitBswup.forceRefresh) BitBswup.forceRefresh(); + } + + // ---------------------------------------------------------------- theme & layout + + function toggleTheme() { + const root = document.documentElement; + const dark = root.getAttribute('data-theme') === 'dark'; + if (dark) { + root.removeAttribute('data-theme'); + } else { + root.setAttribute('data-theme', 'dark'); + } + try { localStorage.setItem('bswup-demo-theme', dark ? 'light' : 'dark'); } catch (err) { } + } + + function toggleSidebar() { + document.body.classList.toggle('sidebar-open'); + } + + function closeSidebar() { + document.body.classList.remove('sidebar-open'); + } + + function copyCode(button) { + const block = button.closest('.code-block'); + const code = block ? block.querySelector('pre code') : null; + if (!code) return; + const original = button.textContent; + const flash = text => { + button.textContent = text; + button.disabled = true; + setTimeout(() => { button.textContent = original; button.disabled = false; }, 1500); + }; + if (!navigator.clipboard || !navigator.clipboard.writeText) return flash('Copy failed'); + navigator.clipboard.writeText(code.textContent || '').then(() => flash('Copied!'), () => flash('Copy failed')); + } + + // Blazor renders pages after this script runs (and on every navigation), so watch for the + // playground elements appearing and (re)hydrate them. The dataset guards keep this cheap: + // the callback exits immediately when nothing relevant changed. + if (typeof MutationObserver !== 'undefined') { + new MutationObserver(() => { + renderEvents(); + const playground = document.getElementById('bswup-playground'); + if (playground && playground.dataset.init !== 'true') { + playground.dataset.init = 'true'; + refreshStatus(); + } + }).observe(document.documentElement, { childList: true, subtree: true }); + } + + // ---------------------------------------------------------------- CSP-safe wiring + + // The markup wires its controls through data-demo-action attributes and this single + // delegated listener instead of inline onclick="..." attributes: inline handlers require + // script-src 'unsafe-inline' (or per-page hashes), and this docs site keeps itself + // compatible with the same strict Content-Security-Policy that Bswup's own scripts are + // designed for. Delegation on document also survives Blazor re-renders replacing the + // elements, with no per-element wiring to redo. + const actions = { + 'copy-code': copyCode, + 'toggle-theme': toggleTheme, + 'toggle-sidebar': toggleSidebar, + 'close-sidebar': closeSidebar, + 'refresh-status': refreshStatus, + 'check-for-update': checkForUpdate, + 'skip-waiting': skipWaiting, + 'persist-storage': persistStorage, + 'force-refresh': forceRefresh + }; + document.addEventListener('click', function (e) { + const trigger = e.target instanceof Element ? e.target.closest('[data-demo-action]') : null; + const action = trigger && actions[trigger.getAttribute('data-demo-action')]; + if (action) action(trigger); + }); + + window.BswupDemo = { + toggleTheme: toggleTheme, + toggleSidebar: toggleSidebar, + closeSidebar: closeSidebar, + copyCode: copyCode, + refreshStatus: refreshStatus, + checkForUpdate: checkForUpdate, + skipWaiting: skipWaiting, + persistStorage: persistStorage, + forceRefresh: forceRefresh + }; +}()); diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/index.html b/src/Bswup/Bit.Bswup.Demo/wwwroot/index.html index 951dd57acf..7323fb4b21 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/index.html +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/index.html @@ -1,152 +1,98 @@ - + - bit Bswup Demo + bit Bswup - Blazor Service Worker Update Progress + + + - + + + -
      -
      -

      New version is available

      -

      Downloading updates, please wait...

      + +
      +
      + +

      bit Bswup

      +

      Preparing the app for offline use, please wait...

      -
      +
      +
      +

      0%

      + + -

      0 %

      -
        - - -
        Loading...
        - - - + + + + updateInterval="3600" + updateOnVisibility="true"> + diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/manifest.json b/src/Bswup/Bit.Bswup.Demo/wwwroot/manifest.json index 4c9c96590a..91f455e630 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/manifest.json +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/manifest.json @@ -1,6 +1,7 @@ -{ - "name": "Bit.Bswup.Demo", - "short_name": "Bit.Bswup.Demo", +{ + "name": "bit Bswup", + "short_name": "Bswup", + "description": "Documentation and live showcase of bit Bswup - install progress, background updates, and offline support for Blazor WebAssembly apps.", "start_url": "./", "display": "standalone", "background_color": "#ffffff", diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js index 234b154b45..97ab20a5a6 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js @@ -1,14 +1,8 @@ -// bit version: 10.5.0 +// bit version: 10.5.0 -self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; +// Development service worker of the Bswup docs site. The site dogfoods Bswup even during +// development so the full first-install / update experience is visible on plain F5. +self.assetsExclude = [/\.scp\.css$/]; self.caseInsensitiveUrl = true; -self.precachedAssetsInclude = [/favicon\.ico$/, /icon-512\.png$/, /bit-bw-64\.png$/]; - -self.externalAssets = [ - { - "url": "not-found/script.file.js" - } -]; -self.errorTolerance = 'lax'; self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js'); diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js index 98df52a75d..fbb1eaf7c1 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js @@ -1,63 +1,13 @@ -// bit version: 10.5.0 +// bit version: 10.5.0 -self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; +self.assetsExclude = [/\.scp\.css$/]; self.caseInsensitiveUrl = true; -self.precachedAssetsInclude = [/favicon\.ico$/, /icon-512\.png$/, /bit-bw-64\.png$/]; -//self.externalAssets = [ -// { -// "url": "not-found/script.file.js" -// } -//]; -//self.errorTolerance = 'lax'; +//// Resiliency knobs (see the Bswup README for details): +//self.errorTolerance = 'strict'; // abort the install if any asset fails ('lax' = best-effort lazy-fill, the default) +//self.maxRetries = 2; // extra download attempts on transient failures (408/429/5xx, dropped connections) +//self.retryDelay = 300; // base backoff in ms between those retries (exponential, with jitter) +//self.enableIntegrityCheck = true; // attach SRI hashes so tampered assets are rejected (requires byte-identical serving) +//self.cacheVersion = '2026.07.28-abc1234'; // pin/bump the cache bucket independently of the asset manifest self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js'); - -//// Caution! Be sure you understand the caveats before publishing an application with -//// offline support. See https://aka.ms/blazor-offline-considerations - -//self.importScripts('./service-worker-assets.js'); -//self.addEventListener('install', event => event.waitUntil(onInstall(event))); -//self.addEventListener('activate', event => event.waitUntil(onActivate(event))); -//self.addEventListener('fetch', event => event.respondWith(onFetch(event))); - -//const cacheNamePrefix = 'offline-cache-'; -//const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; -//const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; -//const offlineAssetsExclude = [ /^service-worker\.js$/ ]; - -//async function onInstall(event) { -// console.info('Service worker: Install'); - -// // Fetch and cache all matching items from the assets manifest -// const assetsRequests = self.assetsManifest.assets -// .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) -// .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) -// .map(asset => new Request(asset.url, { integrity: asset.hash })); -// await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); -//} - -//async function onActivate(event) { -// console.info('Service worker: Activate'); - -// // Delete unused caches -// const cacheKeys = await caches.keys(); -// await Promise.all(cacheKeys -// .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) -// .map(key => caches.delete(key))); -//} - -//async function onFetch(event) { -// let cachedResponse = null; -// if (event.request.method === 'GET') { -// // For all navigation requests, try to serve index.html from cache -// // If you need some URLs to be server-rendered, edit the following check to exclude those URLs -// const shouldServeIndexHtml = event.request.mode === 'navigate'; - -// const request = shouldServeIndexHtml ? 'index.html' : event.request; -// const cache = await caches.open(cacheName); -// cachedResponse = await cache.match(request); -// } - -// return cachedResponse || fetch(event.request); -//} diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor deleted file mode 100644 index 19a3f2febc..0000000000 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor +++ /dev/null @@ -1,27 +0,0 @@ - - -@*
        - -
        -
        Loading...
        -
        -
        -
        -
        -
        -
        *@ - -
        - -
        - - - - - - -
        diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor.css b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor.css deleted file mode 100644 index df8c10ff29..0000000000 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor.css +++ /dev/null @@ -1,18 +0,0 @@ -#blazor-error-ui { - background: lightyellow; - bottom: 0; - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; - width: 100%; - z-index: 1000; -} - - #blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; - } diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Program.cs b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Program.cs deleted file mode 100644 index 519269f21b..0000000000 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Program.cs +++ /dev/null @@ -1,5 +0,0 @@ -using Microsoft.AspNetCore.Components.WebAssembly.Hosting; - -var builder = WebAssemblyHostBuilder.CreateDefault(args); - -await builder.Build().RunAsync(); diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Routes.razor b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Routes.razor deleted file mode 100644 index d0df781615..0000000000 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Routes.razor +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/app.css b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/app.css deleted file mode 100644 index e398853b8e..0000000000 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/app.css +++ /dev/null @@ -1,29 +0,0 @@ -h1:focus { - outline: none; -} - -.valid.modified:not([type=checkbox]) { - outline: 1px solid #26b050; -} - -.invalid { - outline: 1px solid #e50000; -} - -.validation-message { - color: #e50000; -} - -.blazor-error-boundary { - background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; - padding: 1rem 1rem 1rem 3.7rem; - color: white; -} - - .blazor-error-boundary::after { - content: "An error has occurred." - } - -.darker-border-checkbox.form-check-input { - border-color: #929292; -} diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js deleted file mode 100644 index 476267b680..0000000000 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js +++ /dev/null @@ -1,49 +0,0 @@ -// bit version: 10.5.0 - -// In development, always fetch from the network and do not enable offline support. -// This is because caching would make development more difficult (changes would not -// be reflected on the first load after each change). -//self.addEventListener('fetch', () => { }); - -self.assetsInclude = []; -self.assetsExclude = [ - /Bit\.Bswup\.NewDemo\.Client\.styles\.css$/ -]; -self.defaultUrl = '/'; -self.prohibitedUrls = []; -self.assetsUrl = '/service-worker-assets.js'; - -// more about SRI (Subresource Integrity) here: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity -// online tool to generate integrity hash: https://www.srihash.org/ or https://laysent.github.io/sri-hash-generator/ -// using only js to generate hash: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest -self.externalAssets = [ - { - "url": "/" - }, - { - "url": "app.css" - }, - { - "url": "_framework/blazor.web.js?v=10.0.0" - }, - { - "url": "Bit.Bswup.NewDemo.styles.css" - }, - { - "url": "Bit.Bswup.NewDemo.Client.bundle.scp.css" - } -]; - -self.caseInsensitiveUrl = true; - -self.serverHandledUrls = [/\/api\//]; -self.serverRenderedUrls = [/\/privacy$/]; - -self.noPrerenderQuery = 'no-prerender=true'; - -self.isPassive = true; - -//self.enableDiagnostics = true; -//self.enableFetchDiagnostics = true; - -self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js'); diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js deleted file mode 100644 index 7468555824..0000000000 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js +++ /dev/null @@ -1,44 +0,0 @@ -// bit version: 10.5.0 - -self.assetsInclude = []; -self.assetsExclude = [ - /Bit\.Bswup\.NewDemo\.Client\.styles\.css$/ -]; -self.defaultUrl = '/'; -self.prohibitedUrls = []; -self.assetsUrl = '/service-worker-assets.js'; - -// more about SRI (Subresource Integrity) here: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity -// online tool to generate integrity hash: https://www.srihash.org/ or https://laysent.github.io/sri-hash-generator/ -// using only js to generate hash: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest -self.externalAssets = [ - { - "url": "/" - }, - { - "url": "app.css" - }, - { - "url": "_framework/blazor.web.js?v=10.0.0" - }, - { - "url": "Bit.Bswup.NewDemo.styles.css" - }, - { - "url": "Bit.Bswup.NewDemo.Client.bundle.scp.css" - } -]; - -self.caseInsensitiveUrl = true; - -self.serverHandledUrls = [/\/api\//]; -self.serverRenderedUrls = [/\/privacy$/]; - -self.noPrerenderQuery = 'no-prerender=true'; - -self.isPassive = true; - -//self.enableDiagnostics = true; -//self.enableFetchDiagnostics = true; - -self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js'); diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.csproj b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.csproj deleted file mode 100644 index e14bdfa8ce..0000000000 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net10.0 - enable - enable - - - - - - - - diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/App.razor b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/App.razor deleted file mode 100644 index 3110bb5054..0000000000 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/App.razor +++ /dev/null @@ -1,38 +0,0 @@ -@code { - [CascadingParameter] HttpContext HttpContext { get; set; } = default!; -} - -@{ - var noPrerender = HttpContext.Request.Query["no-prerender"].Count > 0; - var renderMode = new InteractiveWebAssemblyRenderMode(!noPrerender); -} - - - - - - - - - - - - - - - -

        111

        - - - - - - - - - diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/Pages/Error.razor b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/Pages/Error.razor deleted file mode 100644 index 576cc2d2f4..0000000000 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/Pages/Error.razor +++ /dev/null @@ -1,36 +0,0 @@ -@page "/Error" -@using System.Diagnostics - -Error - -

        Error.

        -

        An error occurred while processing your request.

        - -@if (ShowRequestId) -{ -

        - Request ID: @RequestId -

        -} - -

        Development Mode

        -

        - Swapping to Development environment will display more detailed information about the error that occurred. -

        -

        - The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

        - -@code{ - [CascadingParameter] - private HttpContext? HttpContext { get; set; } - - private string? RequestId { get; set; } - private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - - protected override void OnInitialized() => - RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; -} diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Program.cs b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Program.cs deleted file mode 100644 index 668876cc8a..0000000000 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Program.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Bit.Bswup.NewDemo.Components; - -var builder = WebApplication.CreateBuilder(args); - -// Add services to the container. -builder.Services.AddRazorComponents() - .AddInteractiveServerComponents() - .AddInteractiveWebAssemblyComponents(); - -var app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseWebAssemblyDebugging(); -} -else -{ - app.UseExceptionHandler("/Error", createScopeForErrors: true); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); -} - -app.UseHttpsRedirection(); - -app.MapStaticAssets(); -app.UseAntiforgery(); - -app.MapRazorComponents() - .AddInteractiveServerRenderMode() - .AddInteractiveWebAssemblyRenderMode() - .AddAdditionalAssemblies(typeof(Bit.Bswup.NewDemo.Client._Imports).Assembly); - -app.Run(); diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.Development.json b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.Development.json deleted file mode 100644 index 0c208ae918..0000000000 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.json b/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.json deleted file mode 100644 index 10f68b8c8b..0000000000 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} diff --git a/src/Bswup/Bit.Bswup.slnx b/src/Bswup/Bit.Bswup.slnx index 2b41b652c7..85af14d2f4 100644 --- a/src/Bswup/Bit.Bswup.slnx +++ b/src/Bswup/Bit.Bswup.slnx @@ -6,14 +6,13 @@ - - - + + - - - + + + - + diff --git a/src/Bswup/Bit.Bswup/Bit.Bswup.csproj b/src/Bswup/Bit.Bswup/Bit.Bswup.csproj index 0335bfd2f5..e355026245 100644 --- a/src/Bswup/Bit.Bswup/Bit.Bswup.csproj +++ b/src/Bswup/Bit.Bswup/Bit.Bswup.csproj @@ -5,10 +5,15 @@ net10.0;net9.0;net8.0 true - - BeforeBuildTasks; - $(ResolveStaticWebAssetsInputsDependsOn) - + + es2019 @@ -21,41 +26,142 @@ + - - + + - - - + + + + + + + + + + + + + + + + + + - - - - + + + + - + - + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + - - - - - - - - \ No newline at end of file + diff --git a/src/Bswup/Bit.Bswup/BswupProgress.razor b/src/Bswup/Bit.Bswup/BswupProgress.razor index 3d6344e5a2..35c432f0b0 100644 --- a/src/Bswup/Bit.Bswup/BswupProgress.razor +++ b/src/Bswup/Bit.Bswup/BswupProgress.razor @@ -1,7 +1,16 @@ @code { [Parameter] public RenderFragment ChildContent { get; set; } = default!; - [Parameter] public bool AutoReload { get; set; } = true; + // Whether a finished update is activated (and every open tab reloaded) automatically, or + // announced through the reload button for the user to accept. Defaults to false: an + // unprompted reload discards whatever in-page state the user has mid-session (a + // half-filled form has no other copy), which is a worse failure than running one version + // behind for a while - and it matches the prompt-then-reload pattern of the standard + // Blazor template and this package's own README sample. First installs are unaffected: + // they always complete the claim-and-start flow (no reload) regardless of this setting. + // CHANGED in v-10-6-0 - previous versions defaulted to true; set AutoReload="true" + // explicitly to keep the old behavior. + [Parameter] public bool AutoReload { get; set; } = false; [Parameter] public bool ShowLogs { get; set; } = false; [Parameter] public bool ShowAssets { get; set; } = false; [Parameter] public string AppContainer { get; set; } = "#app"; @@ -10,24 +19,83 @@ [Parameter] public string? Handler { get; set; } } -
        - @if (ChildContent is not null) +@* Configuration is published as data-* attributes and read by bit-bswup.progress.js when it + loads (it self-initializes from these attributes). This deliberately avoids emitting an + inline + + + + + + diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/manifest.json b/src/Bswup/Demos/BasicDemo/wwwroot/manifest.json similarity index 77% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/manifest.json rename to src/Bswup/Demos/BasicDemo/wwwroot/manifest.json index 33796c575a..a70e09bceb 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/manifest.json +++ b/src/Bswup/Demos/BasicDemo/wwwroot/manifest.json @@ -1,6 +1,6 @@ { - "name": "Bit.Bswup.NewDemo", - "short_name": "Bit.Bswup.NewDemo", + "name": "Bit.Bswup.BasicDemo", + "short_name": "Bit.Bswup.BasicDemo", "start_url": "./", "display": "standalone", "background_color": "#ffffff", diff --git a/src/Bswup/Demos/BasicDemo/wwwroot/service-worker.js b/src/Bswup/Demos/BasicDemo/wwwroot/service-worker.js new file mode 100644 index 0000000000..713c3d4128 --- /dev/null +++ b/src/Bswup/Demos/BasicDemo/wwwroot/service-worker.js @@ -0,0 +1,16 @@ +// bit version: 10.5.0 + +self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; +self.caseInsensitiveUrl = true; + +self.externalAssets = [ + { + "url": "not-found/script.file.js" + } +]; +// 'lax' is already the default; it is spelled out here because the demo intentionally +// references a non-existent asset to exercise the progress / error reporting UI, and under +// 'strict' that would abort the install. See README.md > errorTolerance. +self.errorTolerance = 'lax'; + +self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js'); diff --git a/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js b/src/Bswup/Demos/BasicDemo/wwwroot/service-worker.published.js similarity index 76% rename from src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js rename to src/Bswup/Demos/BasicDemo/wwwroot/service-worker.published.js index 8b9547729d..9039228117 100644 --- a/src/Bswup/FullDemo/Client/wwwroot/service-worker.published.js +++ b/src/Bswup/Demos/BasicDemo/wwwroot/service-worker.published.js @@ -1,31 +1,20 @@ // bit version: 10.5.0 -self.assetsInclude = []; self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; -self.defaultUrl = "/"; -self.prohibitedUrls = []; -self.assetsUrl = '/service-worker-assets.js'; -self.externalAssets = [ - //{ - // "hash": "sha256-lDAEEaul32OkTANWkZgjgs4sFCsMdLsR5NJxrjVcXdo=", - // "url": "css/app.css" - //}, - { - "url": "/" - }, - { - "url": "https://www.googletagmanager.com/gtag/js?id=G-G1ET5L69QF" - } -]; - self.caseInsensitiveUrl = true; -self.serverHandledUrls = [/\/api\//]; -self.serverRenderedUrls = [/\/privacy$/]; - -self.noPrerenderQuery = 'no-prerender=true'; - -self.isPassive = true; +//self.externalAssets = [ +// { +// "url": "not-found/script.file.js" +// } +//]; + +//// Resiliency knobs (see the Bswup README for details): +//self.errorTolerance = 'strict'; // abort the install if any asset fails ('lax' = best-effort lazy-fill, the default) +//self.maxRetries = 2; // extra download attempts on transient failures (408/429/5xx, dropped connections) +//self.retryDelay = 300; // base backoff in ms between those retries (exponential, with jitter) +//self.enableIntegrityCheck = true; // attach SRI hashes so tampered assets are rejected (requires byte-identical serving) +//self.cacheVersion = '2026.07.24-abc1234'; // pin/bump the cache bucket independently of the asset manifest self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js'); diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Bit.Bswup.NewDemo.Client.csproj b/src/Bswup/Demos/FullDemo/Client/Bit.Bswup.FullDemo.Client.csproj similarity index 72% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Bit.Bswup.NewDemo.Client.csproj rename to src/Bswup/Demos/FullDemo/Client/Bit.Bswup.FullDemo.Client.csproj index cff2c86f7f..8cb9a178da 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Bit.Bswup.NewDemo.Client.csproj +++ b/src/Bswup/Demos/FullDemo/Client/Bit.Bswup.FullDemo.Client.csproj @@ -1,23 +1,26 @@ - + net10.0 - enable enable + enable true Default - service-worker-assets.js false - + + + + + diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Counter.razor b/src/Bswup/Demos/FullDemo/Client/Pages/Counter.razor similarity index 95% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Counter.razor rename to src/Bswup/Demos/FullDemo/Client/Pages/Counter.razor index e1f857fb6c..6526cae88a 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Counter.razor +++ b/src/Bswup/Demos/FullDemo/Client/Pages/Counter.razor @@ -12,7 +12,7 @@ } -
        Counter
        +

        Counter


        diff --git a/src/Bswup/Demos/FullDemo/Client/Pages/Error.razor b/src/Bswup/Demos/FullDemo/Client/Pages/Error.razor new file mode 100644 index 0000000000..a661b47c66 --- /dev/null +++ b/src/Bswup/Demos/FullDemo/Client/Pages/Error.razor @@ -0,0 +1,13 @@ +@page "/Error" + +@* Target of app.UseExceptionHandler("/Error") in the host's Program.cs. It lives in the client + project on purpose: Routes.razor binds the router to this assembly only, so a page placed in + the host project would never be matched and the re-executed request would end in a 404. *@ + +Error + +

        Error

        + +

        An error occurred while processing your request.

        + +Home diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Home.razor b/src/Bswup/Demos/FullDemo/Client/Pages/Home.razor similarity index 91% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Home.razor rename to src/Bswup/Demos/FullDemo/Client/Pages/Home.razor index 1594fe7a5e..d0d3530273 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Home.razor +++ b/src/Bswup/Demos/FullDemo/Client/Pages/Home.razor @@ -1,4 +1,4 @@ -@page "/" +@page "/" Home @@ -14,4 +14,4 @@ - \ No newline at end of file + diff --git a/src/Bswup/FullDemo/Client/Program.cs b/src/Bswup/Demos/FullDemo/Client/Program.cs similarity index 100% rename from src/Bswup/FullDemo/Client/Program.cs rename to src/Bswup/Demos/FullDemo/Client/Program.cs diff --git a/src/Bswup/Demos/FullDemo/Client/Routes.razor b/src/Bswup/Demos/FullDemo/Client/Routes.razor new file mode 100644 index 0000000000..aae7e56222 --- /dev/null +++ b/src/Bswup/Demos/FullDemo/Client/Routes.razor @@ -0,0 +1,12 @@ + + + + + + + Not found + +

        Sorry, there's nothing at this address.

        +
        +
        +
        diff --git a/src/Bswup/FullDemo/Client/Shared/DemoBswupProgressBar.razor b/src/Bswup/Demos/FullDemo/Client/Shared/DemoBswupProgressBar.razor similarity index 70% rename from src/Bswup/FullDemo/Client/Shared/DemoBswupProgressBar.razor rename to src/Bswup/Demos/FullDemo/Client/Shared/DemoBswupProgressBar.razor index 40ce8b0052..3190246cf6 100644 --- a/src/Bswup/FullDemo/Client/Shared/DemoBswupProgressBar.razor +++ b/src/Bswup/Demos/FullDemo/Client/Shared/DemoBswupProgressBar.razor @@ -25,5 +25,8 @@ - + @* No hand-written #bit-bswup-reload here: since v-10-6-0 BswupProgress always renders + the update-ready button (and its screen-reader status region) itself, outside the + overlay - even with custom ChildContent. A second copy would be a duplicate id that + shadows the component's working button. Restyle it via ::deep #bit-bswup-reload. *@
        diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor.css b/src/Bswup/Demos/FullDemo/Client/Shared/DemoBswupProgressBar.razor.css similarity index 81% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor.css rename to src/Bswup/Demos/FullDemo/Client/Shared/DemoBswupProgressBar.razor.css index 218b51c948..0b023e5224 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor.css +++ b/src/Bswup/Demos/FullDemo/Client/Shared/DemoBswupProgressBar.razor.css @@ -5,7 +5,7 @@ bottom: 50px; text-align: center; width: 200px; - font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; + font-family: "Segoe UI", "Segoe UI Web (West European)", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; } ::deep #bit-bswup .bswup-container { @@ -30,21 +30,21 @@ }*/ ::deep #bit-bswup { - top: 2px; - left: 50%; display: none; - z-index: 999999; position: fixed; - text-align: center; + left: 50%; + top: 2px; transform: translateX(-50%); - font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; + text-align: center; + z-index: 999999; + font-family: "Segoe UI", "Segoe UI Web (West European)", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; } ::deep #bit-bswup .bswup-container { + position: relative; + display: block; width: 3rem; height: 3rem; - display: block; - position: relative; } ::deep #bit-bswup .bswup-container circle { @@ -57,17 +57,17 @@ ::deep #bit-bswup .bswup-container circle:last-child { stroke: #1b6ec2; - transition: stroke-dasharray 0.05s ease-in-out; stroke-dasharray: calc(3.141 * var(--bit-bswup-percent, 0%) * 0.8), 500%; + transition: stroke-dasharray 0.05s ease-in-out; } ::deep #bit-bswup .bswup-progress-text { - top: 50%; - left: 50%; - font-size: 12px; position: absolute; text-align: center; font-weight: normal; + font-size: 12px; + top: 50%; + left: 50%; transform: translate(-50%, -50%); } @@ -76,8 +76,11 @@ } ::deep #bit-bswup-reload { - top: 10px; - right: 10px; display: none; position: fixed; + top: 10px; + right: 10px; + /* Above any fixed app chrome - the button sits outside the splash overlay and does not + inherit its stacking. */ + z-index: 999999; } \ No newline at end of file diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor b/src/Bswup/Demos/FullDemo/Client/Shared/MainLayout.razor similarity index 82% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor rename to src/Bswup/Demos/FullDemo/Client/Shared/MainLayout.razor index 0fd1b20ecf..4231bc9974 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor +++ b/src/Bswup/Demos/FullDemo/Client/Shared/MainLayout.razor @@ -1,4 +1,4 @@ -@inherits LayoutComponentBase +@inherits LayoutComponentBase @Body diff --git a/src/Bswup/Demos/FullDemo/Client/Shared/MainLayout.razor.css b/src/Bswup/Demos/FullDemo/Client/Shared/MainLayout.razor.css new file mode 100644 index 0000000000..7ef85f8dbf --- /dev/null +++ b/src/Bswup/Demos/FullDemo/Client/Shared/MainLayout.razor.css @@ -0,0 +1,28 @@ +/* The #blazor-error-ui banner in MainLayout.razor is the standard Blazor error placeholder: + the framework flips it to 'display: block' when an unhandled exception reaches the client. + The stock templates hide it from wwwroot/css/app.css, which this demo deliberately does not + have - so without this rule the banner renders on every page from the first paint, looking + like a permanent error. Scoped CSS reaches it because the element lives in this layout's + own markup. */ +#blazor-error-ui { + color-scheme: light only; + /* The demo's body sets 'color: white', which would be unreadable on lightyellow. */ + color: black; + background: lightyellow; + bottom: 0; + box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + display: none; + left: 0; + padding: 0.6rem 1.25rem 0.7rem 1.25rem; + position: fixed; + width: 100%; + z-index: 1000; +} + + #blazor-error-ui .dismiss { + cursor: pointer; + position: absolute; + right: 0.75rem; + top: 0.5rem; + } diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/_Imports.razor b/src/Bswup/Demos/FullDemo/Client/_Imports.razor similarity index 78% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/_Imports.razor rename to src/Bswup/Demos/FullDemo/Client/_Imports.razor index 7d6a36f7aa..67c122e5d9 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/_Imports.razor +++ b/src/Bswup/Demos/FullDemo/Client/_Imports.razor @@ -6,4 +6,6 @@ @using static Microsoft.AspNetCore.Components.Web.RenderMode @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop -@using Bit.Bswup.NewDemo.Client +@using Bit.Bswup +@using Bit.Bswup.FullDemo.Client +@using Bit.Bswup.FullDemo.Client.Shared diff --git a/src/Bswup/FullDemo/Client/wwwroot/bit-bw-64.png b/src/Bswup/Demos/FullDemo/Client/wwwroot/bit-bw-64.png similarity index 100% rename from src/Bswup/FullDemo/Client/wwwroot/bit-bw-64.png rename to src/Bswup/Demos/FullDemo/Client/wwwroot/bit-bw-64.png diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/favicon.ico b/src/Bswup/Demos/FullDemo/Client/wwwroot/favicon.ico similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/favicon.ico rename to src/Bswup/Demos/FullDemo/Client/wwwroot/favicon.ico diff --git a/src/Bswup/FullDemo/Client/wwwroot/icon-512.png b/src/Bswup/Demos/FullDemo/Client/wwwroot/icon-512.png similarity index 100% rename from src/Bswup/FullDemo/Client/wwwroot/icon-512.png rename to src/Bswup/Demos/FullDemo/Client/wwwroot/icon-512.png diff --git a/src/Bswup/FullDemo/Client/wwwroot/manifest.json b/src/Bswup/Demos/FullDemo/Client/wwwroot/manifest.json similarity index 77% rename from src/Bswup/FullDemo/Client/wwwroot/manifest.json rename to src/Bswup/Demos/FullDemo/Client/wwwroot/manifest.json index 4c9c96590a..9d0997fb58 100644 --- a/src/Bswup/FullDemo/Client/wwwroot/manifest.json +++ b/src/Bswup/Demos/FullDemo/Client/wwwroot/manifest.json @@ -1,6 +1,6 @@ { - "name": "Bit.Bswup.Demo", - "short_name": "Bit.Bswup.Demo", + "name": "Bit.Bswup.FullDemo", + "short_name": "Bit.Bswup.FullDemo", "start_url": "./", "display": "standalone", "background_color": "#ffffff", diff --git a/src/Bswup/Demos/FullDemo/Client/wwwroot/service-worker.js b/src/Bswup/Demos/FullDemo/Client/wwwroot/service-worker.js new file mode 100644 index 0000000000..eb6c07a78b --- /dev/null +++ b/src/Bswup/Demos/FullDemo/Client/wwwroot/service-worker.js @@ -0,0 +1,69 @@ +// bit version: 10.5.0 + +// Development service worker of the FullDemo. Unlike the standard Blazor template - whose +// dev worker is a no-op so caching never hides source changes - this demo runs the full +// Bswup engine even in development (see the importScripts at the bottom), so the complete +// first-install / update experience is exercised on plain F5, matching the other demos. + +self.assetsInclude = []; +// The client's scoped-css bundle is in this app's asset manifest but is never served: in a +// Blazor Web App the host project merges the client's scoped styles into its own +// .styles.css. Precaching it would fail with a 404, so it's excluded here and +// the two files the page actually loads are precached through externalAssets instead. +self.assetsExclude = [/^Bit\.Bswup\.FullDemo\.Client\.styles\.css$/, /weather\.json$/]; +self.defaultUrl = '/'; +self.prohibitedUrls = []; +// self.assetsUrl is deliberately NOT set: since v-10-6-0 it defaults to a relative +// 'service-worker-assets.js', resolved next to this service-worker file - which keeps +// sub-path-mounted apps working. Set it explicitly only when the file lives elsewhere. + +// more about SRI (Subresource Integrity) here: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity +// online tool to generate integrity hash: https://www.srihash.org/ or https://laysent.github.io/sri-hash-generator/ +// using only js to generate hash: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest +self.externalAssets = [ + //{ + // "hash": "sha256-lDAEEaul32OkTANWkZgjgs4sFCsMdLsR5NJxrjVcXdo=", + // "url": "css/app.css" + //}, + { + "url": "/" + }, + // The host's scoped-css bundle (linked from App.razor) and the client bundle it @imports. + // Both are served by the host project, so they're absent from the client asset manifest. + { + "url": "Bit.Bswup.FullDemo.Server.styles.css" + }, + { + "url": "Bit.Bswup.FullDemo.Client.bundle.scp.css" + }, + // A Blazor Web App boots through blazor.web.js, not the blazor.webassembly.js that the + // client's asset manifest lists - it belongs to the host project, so the manifest never + // sees it and the app cannot start offline without this entry. + { + "url": "_framework/blazor.web.js" + }, + // blazor.web.js then loads the WASM resource list from a fingerprinted + // resource-collection..js, named by the host at build time. The concrete name is + // unknown here and changes every build, so it is matched with a RegExp and cached lazily + // on first fetch (see the externalAssets notes in the Bswup README). + { + "url": /\/_framework\/resource-collection\.[^\/]*\.js$/ + }, + { + "url": "https://www.googletagmanager.com/gtag/js?id=G-G1ET5L69QF" + } +]; + +self.caseInsensitiveUrl = true; + +self.serverHandledUrls = [/\/api\//]; +self.serverRenderedUrls = [/\/privacy$/]; + +self.noPrerenderQuery = 'no-prerender=true'; + +self.isPassive = true; + +//self.enableDiagnostics = true; +//self.enableFetchDiagnostics = true; + +self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js'); diff --git a/src/Bswup/Demos/FullDemo/Client/wwwroot/service-worker.published.js b/src/Bswup/Demos/FullDemo/Client/wwwroot/service-worker.published.js new file mode 100644 index 0000000000..37488c3140 --- /dev/null +++ b/src/Bswup/Demos/FullDemo/Client/wwwroot/service-worker.published.js @@ -0,0 +1,117 @@ +// bit version: 10.5.0 + +self.assetsInclude = []; +// The client's scoped-css bundle is in this app's asset manifest but is never served: in a +// Blazor Web App the host project merges the client's scoped styles into its own +// .styles.css. Precaching it would fail with a 404, so it's excluded here and +// the two files the page actually loads are precached through externalAssets instead. +self.assetsExclude = [/^Bit\.Bswup\.FullDemo\.Client\.styles\.css$/, /weather\.json$/]; +self.defaultUrl = "/"; +self.prohibitedUrls = []; +// self.assetsUrl is deliberately NOT set: since v-10-6-0 it defaults to a relative +// 'service-worker-assets.js', resolved next to this service-worker file - which keeps +// sub-path-mounted apps working. Set it explicitly only when the file lives elsewhere. +self.externalAssets = [ + //{ + // "hash": "sha256-lDAEEaul32OkTANWkZgjgs4sFCsMdLsR5NJxrjVcXdo=", + // "url": "css/app.css" + //}, + { + "url": "/" + }, + // The host's scoped-css bundle (linked from App.razor) and the client bundle it @imports. + // Both are served by the host project, so they're absent from the client asset manifest. + { + "url": "Bit.Bswup.FullDemo.Server.styles.css" + }, + { + "url": "Bit.Bswup.FullDemo.Client.bundle.scp.css" + }, + // A Blazor Web App boots through blazor.web.js, not the blazor.webassembly.js that the + // client's asset manifest lists - it belongs to the host project, so the manifest never + // sees it and the app cannot start offline without this entry. + { + "url": "_framework/blazor.web.js" + }, + // blazor.web.js then loads the WASM resource list from a fingerprinted + // resource-collection..js, named by the host at build time. The concrete name is + // unknown here and changes every build, so it is matched with a RegExp and cached lazily + // on first fetch (see the externalAssets notes in the Bswup README). + { + "url": /\/_framework\/resource-collection\.[^\/]*\.js$/ + }, + { + "url": "https://www.googletagmanager.com/gtag/js?id=G-G1ET5L69QF" + } +]; + +self.caseInsensitiveUrl = true; + +self.serverHandledUrls = [/\/api\//]; +self.serverRenderedUrls = [/\/privacy$/]; + +self.noPrerenderQuery = 'no-prerender=true'; + +self.isPassive = true; + +//// Diagnostics knobs (see the Bswup README for details): +//self.enableDiagnostics = true; // log install/activate/cache decisions to the console +//self.enableFetchDiagnostics = true; // additionally log every intercepted fetch (very verbose) + +//// Resiliency knobs (see the Bswup README for details): +//self.errorTolerance = 'strict'; // abort the install if any asset fails ('lax' = best-effort lazy-fill, the default) +//self.maxRetries = 2; // extra download attempts on transient failures (408/429/5xx, dropped connections) +//self.retryDelay = 300; // base backoff in ms between those retries (exponential, with jitter) +//self.enableIntegrityCheck = true; // attach SRI hashes so tampered assets are rejected (requires byte-identical serving) +//self.cacheVersion = '2026.07.24-abc1234'; // pin/bump the cache bucket independently of the asset manifest + +self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js'); + +//// Caution! Be sure you understand the caveats before publishing an application with +//// offline support. See https://aka.ms/blazor-offline-considerations + +//self.importScripts('./service-worker-assets.js'); +//self.addEventListener('install', event => event.waitUntil(onInstall(event))); +//self.addEventListener('activate', event => event.waitUntil(onActivate(event))); +//self.addEventListener('fetch', event => event.respondWith(onFetch(event))); + +//const cacheNamePrefix = 'offline-cache-'; +//const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; +//const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ]; +//const offlineAssetsExclude = [ /^service-worker\.js$/ ]; + +//async function onInstall(event) { +// console.info('Service worker: Install'); + +// // Fetch and cache all matching items from the assets manifest +// const assetsRequests = self.assetsManifest.assets +// .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) +// .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) +// .map(asset => new Request(asset.url, { integrity: asset.hash })); +// await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); +//} + +//async function onActivate(event) { +// console.info('Service worker: Activate'); + +// // Delete unused caches +// const cacheKeys = await caches.keys(); +// await Promise.all(cacheKeys +// .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) +// .map(key => caches.delete(key))); +//} + +//async function onFetch(event) { +// let cachedResponse = null; +// if (event.request.method === 'GET') { +// // For all navigation requests, try to serve index.html from cache +// // If you need some URLs to be server-rendered, edit the following check to exclude those URLs +// const shouldServeIndexHtml = event.request.mode === 'navigate'; + +// const request = shouldServeIndexHtml ? 'index.html' : event.request; +// const cache = await caches.open(cacheName); +// cachedResponse = await cache.match(request); +// } + +// return cachedResponse || fetch(event.request); +//} diff --git a/src/Bswup/FullDemo/Server/Bit.Bswup.Demo.Server.csproj b/src/Bswup/Demos/FullDemo/Server/Bit.Bswup.FullDemo.Server.csproj similarity index 63% rename from src/Bswup/FullDemo/Server/Bit.Bswup.Demo.Server.csproj rename to src/Bswup/Demos/FullDemo/Server/Bit.Bswup.FullDemo.Server.csproj index 3cdd9c5447..5a01411f62 100644 --- a/src/Bswup/FullDemo/Server/Bit.Bswup.Demo.Server.csproj +++ b/src/Bswup/Demos/FullDemo/Server/Bit.Bswup.FullDemo.Server.csproj @@ -2,12 +2,13 @@ net10.0 + enable enable - - + + diff --git a/src/Bswup/Demos/FullDemo/Server/Components/App.razor b/src/Bswup/Demos/FullDemo/Server/Components/App.razor new file mode 100644 index 0000000000..286fbb281a --- /dev/null +++ b/src/Bswup/Demos/FullDemo/Server/Components/App.razor @@ -0,0 +1,49 @@ +@code { + [CascadingParameter] HttpContext HttpContext { get; set; } = default!; +} + +@{ + // Honors the Bswup service-worker "no-prerender" escape hatch: when the query string is + // present the page is rendered as interactive WebAssembly without a prerendered pass. + var noPrerender = HttpContext.Request.Query["no-prerender"].Count > 0; + var renderMode = new InteractiveWebAssemblyRenderMode(prerender: !noPrerender); +} + + + + + + + + bit Bswup Full Demo + + + + + + + + +
        + +
        + + + + @* blazorScript is omitted on purpose: both default entry scripts are auto-detected + (fingerprints and query strings included) - the attribute is only needed for + non-default script paths. *@ + + + @* The component renders first so its data-bit-bswup-* configuration is already in the + document when the script initializes. Rendered after the script it still works - the + script's MutationObserver picks up a late element - but that is the fallback path. *@ + + + + + diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/_Imports.razor b/src/Bswup/Demos/FullDemo/Server/Components/_Imports.razor similarity index 61% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/_Imports.razor rename to src/Bswup/Demos/FullDemo/Server/Components/_Imports.razor index fee2256aae..23aacbed1f 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/_Imports.razor +++ b/src/Bswup/Demos/FullDemo/Server/Components/_Imports.razor @@ -1,4 +1,4 @@ -@using System.Net.Http +@using System.Net.Http @using System.Net.Http.Json @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @@ -6,7 +6,8 @@ @using static Microsoft.AspNetCore.Components.Web.RenderMode @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop -@using Bit.Bswup.NewDemo -@using Bit.Bswup.NewDemo.Client -@using Bit.Bswup.NewDemo.Components -@using Bit.Bswup.NewDemo.Client.Layout +@using Bit.Bswup +@using Bit.Bswup.FullDemo.Server +@using Bit.Bswup.FullDemo.Server.Components +@using Bit.Bswup.FullDemo.Client +@using Bit.Bswup.FullDemo.Client.Shared diff --git a/src/Bswup/Demos/FullDemo/Server/Program.cs b/src/Bswup/Demos/FullDemo/Server/Program.cs new file mode 100644 index 0000000000..37bf9cc289 --- /dev/null +++ b/src/Bswup/Demos/FullDemo/Server/Program.cs @@ -0,0 +1,50 @@ +using System.IO.Compression; +using Bit.Bswup.FullDemo.Server.Components; +using Microsoft.AspNetCore.ResponseCompression; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveWebAssemblyComponents(); + +builder.Services.AddControllers(); +builder.Services.AddHttpContextAccessor(); + +builder.Services.AddResponseCompression(opts => +{ + opts.EnableForHttps = true; + opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/octet-stream"]); + opts.Providers.Add(); + opts.Providers.Add(); +}) + .Configure(opt => opt.Level = CompressionLevel.Fastest) + .Configure(opt => opt.Level = CompressionLevel.Fastest); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseWebAssemblyDebugging(); +} +else +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + app.UseResponseCompression(); +} + +app.UseHttpsRedirection(); + +app.MapStaticAssets(); +app.UseAntiforgery(); + +app.MapControllers(); + +app.MapRazorComponents() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(Bit.Bswup.FullDemo.Client._Imports).Assembly); + +app.Run(); diff --git a/src/Bswup/FullDemo/Server/Properties/launchSettings.json b/src/Bswup/Demos/FullDemo/Server/Properties/launchSettings.json similarity index 79% rename from src/Bswup/FullDemo/Server/Properties/launchSettings.json rename to src/Bswup/Demos/FullDemo/Server/Properties/launchSettings.json index c60dbf41d8..458c81ce1c 100644 --- a/src/Bswup/FullDemo/Server/Properties/launchSettings.json +++ b/src/Bswup/Demos/FullDemo/Server/Properties/launchSettings.json @@ -1,12 +1,12 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "Bit.Bswup.Demo.Server": { + "Bit.Bswup.FullDemo.Server": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "applicationUrl": "http://localhost:5020;https://localhost:5021", + "applicationUrl": "http://localhost:5022;https://localhost:5023", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/Bswup/FullDemo/Server/appsettings.json b/src/Bswup/Demos/FullDemo/Server/appsettings.json similarity index 100% rename from src/Bswup/FullDemo/Server/appsettings.json rename to src/Bswup/Demos/FullDemo/Server/appsettings.json diff --git a/src/Bswup/FullDemo/Client/Pages/Counter.razor b/src/Bswup/FullDemo/Client/Pages/Counter.razor deleted file mode 100644 index e1f857fb6c..0000000000 --- a/src/Bswup/FullDemo/Client/Pages/Counter.razor +++ /dev/null @@ -1,32 +0,0 @@ -@page "/counter" - -Counter - - - -
        Counter
        - -
        - -Home - -
        -
        - -
        - -
        - - - -@code { - int count; -} \ No newline at end of file diff --git a/src/Bswup/FullDemo/Client/Properties/launchSettings.json b/src/Bswup/FullDemo/Client/Properties/launchSettings.json deleted file mode 100644 index 6ea3a80281..0000000000 --- a/src/Bswup/FullDemo/Client/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "Bit.Bswup.Demo.Client": { - "commandName": "Project", - "dotnetRunMessages": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} \ No newline at end of file diff --git a/src/Bswup/FullDemo/Client/Shared/DemoBswupProgressBar.razor.css b/src/Bswup/FullDemo/Client/Shared/DemoBswupProgressBar.razor.css deleted file mode 100644 index a969e544ed..0000000000 --- a/src/Bswup/FullDemo/Client/Shared/DemoBswupProgressBar.razor.css +++ /dev/null @@ -1,83 +0,0 @@ -/*::deep #bit-bswup { - display: none; - position: fixed; - left: 0; - bottom: 50px; - text-align: center; - width: 200px; - font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; -} - - ::deep #bit-bswup .bswup-container { - width: 80%; - margin: 0 auto; - } - - ::deep #bit-bswup .bswup-title { - color: white; - font-size: 14px; - margin-bottom: 1px; - } - - ::deep #bit-bswup .bswup-progress { - background-color: rgb(237, 235, 233); - } - - ::deep #bit-bswup #bit-bswup-progress-bar { - background-color: rgb(0, 120, 212); - border-radius: 10px; - height: 3px; - }*/ - -::deep #bit-bswup { - display: none; - position: fixed; - left: 50%; - top: 2px; - transform: translateX(-50%); - text-align: center; - z-index: 999999; - font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; -} - - ::deep #bit-bswup .bswup-container { - position: relative; - display: block; - width: 3rem; - height: 3rem; - } - - ::deep #bit-bswup .bswup-container circle { - fill: none; - stroke: #e0e0e0; - stroke-width: 0.2rem; - transform-origin: 50% 50%; - transform: rotate(-90deg); - } - - ::deep #bit-bswup .bswup-container circle:last-child { - stroke: #1b6ec2; - stroke-dasharray: calc(3.141 * var(--bit-bswup-percent, 0%) * 0.8), 500%; - transition: stroke-dasharray 0.05s ease-in-out; - } - - ::deep #bit-bswup .bswup-progress-text { - position: absolute; - text-align: center; - font-weight: normal; - font-size: 12px; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - } - - ::deep #bit-bswup .bswup-progress-text::after { - content: var(--bit-bswup-percent-text, ""); - } - -::deep #bit-bswup-reload { - display: none; - position: fixed; - top: 10px; - right: 10px; -} \ No newline at end of file diff --git a/src/Bswup/FullDemo/Client/_Imports.razor b/src/Bswup/FullDemo/Client/_Imports.razor deleted file mode 100644 index c4b950bae2..0000000000 --- a/src/Bswup/FullDemo/Client/_Imports.razor +++ /dev/null @@ -1,9 +0,0 @@ -@using System; -@using System.Net.Http; -@using System.Reflection; -@using System.Net.Http.Json; -@using Microsoft.JSInterop; -@using Microsoft.AspNetCore.Components.Web; -@using Microsoft.AspNetCore.Components.Forms; -@using Microsoft.AspNetCore.Components.Routing; -@using Bit.Bswup.Demo.Client; \ No newline at end of file diff --git a/src/Bswup/FullDemo/Client/wwwroot/favicon.ico b/src/Bswup/FullDemo/Client/wwwroot/favicon.ico deleted file mode 100644 index d310b1b80b..0000000000 Binary files a/src/Bswup/FullDemo/Client/wwwroot/favicon.ico and /dev/null differ diff --git a/src/Bswup/FullDemo/Client/wwwroot/service-worker.js b/src/Bswup/FullDemo/Client/wwwroot/service-worker.js deleted file mode 100644 index fe30d7444d..0000000000 --- a/src/Bswup/FullDemo/Client/wwwroot/service-worker.js +++ /dev/null @@ -1,42 +0,0 @@ -// bit version: 10.5.0 - -// In development, always fetch from the network and do not enable offline support. -// This is because caching would make development more difficult (changes would not -// be reflected on the first load after each change). -//self.addEventListener('fetch', () => { }); - -self.assetsInclude = []; -self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; -self.defaultUrl = '/'; -self.prohibitedUrls = []; -self.assetsUrl = '/service-worker-assets.js'; - -// more about SRI (Subresource Integrity) here: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity -// online tool to generate integrity hash: https://www.srihash.org/ or https://laysent.github.io/sri-hash-generator/ -// using only js to generate hash: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest -self.externalAssets = [ - //{ - // "hash": "sha256-lDAEEaul32OkTANWkZgjgs4sFCsMdLsR5NJxrjVcXdo=", - // "url": "css/app.css" - //}, - { - "url": "/" - }, - { - "url": "https://www.googletagmanager.com/gtag/js?id=G-G1ET5L69QF" - } -]; - -self.caseInsensitiveUrl = true; - -self.serverHandledUrls = [/\/api\//]; -self.serverRenderedUrls = [/\/privacy$/]; - -self.noPrerenderQuery = 'no-prerender=true'; - -self.isPassive = true; - -//self.enableDiagnostics = true; -//self.enableFetchDiagnostics = true; - -self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js'); diff --git a/src/Bswup/FullDemo/Server/Pages/_Host.cshtml b/src/Bswup/FullDemo/Server/Pages/_Host.cshtml deleted file mode 100644 index 9f671f70ec..0000000000 --- a/src/Bswup/FullDemo/Server/Pages/_Host.cshtml +++ /dev/null @@ -1,14 +0,0 @@ -@page "/" -@namespace Bit.Bswup.Demo.Web.Pages -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers - -@using RenderMode = Microsoft.AspNetCore.Mvc.Rendering.RenderMode - -@{ - Layout = "_Layout"; - - var noPrerender = Request.Query["no-prerender"].Count > 0; - var renderMode = noPrerender ? RenderMode.WebAssembly : RenderMode.WebAssemblyPrerendered; -} - - \ No newline at end of file diff --git a/src/Bswup/FullDemo/Server/Pages/_Layout.cshtml b/src/Bswup/FullDemo/Server/Pages/_Layout.cshtml deleted file mode 100644 index 34dacd5b93..0000000000 --- a/src/Bswup/FullDemo/Server/Pages/_Layout.cshtml +++ /dev/null @@ -1,44 +0,0 @@ -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@namespace Bit.Bswup.Demo.Web.Pages - -@using Bit.Bswup.Demo.Web -@using Microsoft.AspNetCore.Http -@using Microsoft.AspNetCore.Components.Web -@using RenderMode = Microsoft.AspNetCore.Mvc.Rendering.RenderMode - - - - - - - bit Bswup Full Demo - - - - - - -
        - @RenderBody() -
        - - - - - - - - @* - *@ - - \ No newline at end of file diff --git a/src/Bswup/FullDemo/Server/Program.cs b/src/Bswup/FullDemo/Server/Program.cs deleted file mode 100644 index 49c224c338..0000000000 --- a/src/Bswup/FullDemo/Server/Program.cs +++ /dev/null @@ -1,20 +0,0 @@ -var builder = WebApplication.CreateBuilder(args); - -#if DEBUG -if (OperatingSystem.IsWindows()) -{ - builder.WebHost.UseUrls("https://localhost:5021", "http://localhost:5020", "https://*:5021", "http://*:5020"); -} -else -{ - builder.WebHost.UseUrls("https://localhost:5021", "http://localhost:5020"); -} -#endif - -Bit.Bswup.Demo.Server.Startup.Services.Add(builder.Services); - -var app = builder.Build(); - -Bit.Bswup.Demo.Server.Startup.Middlewares.Use(app, builder.Environment); - -app.Run(); diff --git a/src/Bswup/FullDemo/Server/Startup/Middlewares.cs b/src/Bswup/FullDemo/Server/Startup/Middlewares.cs deleted file mode 100644 index 2cadf34289..0000000000 --- a/src/Bswup/FullDemo/Server/Startup/Middlewares.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.Net.Http.Headers; - -namespace Bit.Bswup.Demo.Server.Startup; - -public static class Middlewares -{ - public static void Use(IApplicationBuilder app, IHostEnvironment env) - { - //app.Use(async (context, next) => - //{ - // await Task.Delay(new Random().Next(500, 800)); - // await next.Invoke(context); - //}); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseWebAssemblyDebugging(); - } - - app.UseBlazorFrameworkFiles(); - - if (env.IsDevelopment() is false) - { - app.UseResponseCompression(); - } - app.UseStaticFiles(new StaticFileOptions - { - OnPrepareResponse = ctx => - { - ctx.Context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromDays(7), - Public = true - }; - } - }); - - app.UseRouting(); - app.UseCors(options => options.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()); - - app.UseEndpoints(endpoints => - { - endpoints.MapDefaultControllerRoute(); - - endpoints.MapFallbackToPage("/_Host"); - }); - } -} diff --git a/src/Bswup/FullDemo/Server/Startup/Services.cs b/src/Bswup/FullDemo/Server/Startup/Services.cs deleted file mode 100644 index 7137f85770..0000000000 --- a/src/Bswup/FullDemo/Server/Startup/Services.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.IO.Compression; -using Microsoft.AspNetCore.ResponseCompression; - -namespace Bit.Bswup.Demo.Server.Startup; - -public static class Services -{ - public static void Add(IServiceCollection services) - { - services.AddRazorPages(); - - services.AddCors(); - - services.AddControllers(); - - services.AddHttpContextAccessor(); - - services.AddResponseCompression(opts => - { - opts.EnableForHttps = true; - opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "application/octet-stream" }).ToArray(); - opts.Providers.Add(); - opts.Providers.Add(); - }) - .Configure(opt => opt.Level = CompressionLevel.Fastest) - .Configure(opt => opt.Level = CompressionLevel.Fastest); - } -} diff --git a/src/Bswup/README.md b/src/Bswup/README.md index 7ab1845ca7..53a12cbf06 100644 --- a/src/Bswup/README.md +++ b/src/Bswup/README.md @@ -40,13 +40,22 @@ app.UseStaticFiles(new StaticFileOptions scope="/" log="verbose" sw="service-worker.js" - handler="bitBswupHandler"> + handler="bitBswupHandler" + updateInterval="3600" + updateOnVisibility="true"> ``` -- `scope`: The scope of the service-worker ([read more](https://developer.chrome.com/docs/workbox/service-worker-lifecycle/#scope)). -- `log`: The log level of the Bswup logger. available options are: `info`, `verbose`, `debug`, and `error`. (not implemented yet) -- `sw`: The file path of the service-worker file. -- `handler`: The name of the handler function for the service-worker events. +- `scope`: The scope of the service-worker ([read more](https://developer.chrome.com/docs/workbox/service-worker-lifecycle/#scope)). Defaults to `/`. A service-worker can only control URLs beneath its own folder unless the server sends a `Service-Worker-Allowed` header, so if your app is mounted on a sub-path (e.g. `https://host/myapp/`) set this to that sub-path. If the browser refuses the configured scope, Bswup automatically retries with the default scope - the folder containing the service-worker script - so the app keeps working with offline support rather than losing the service-worker entirely; the fallback is reported as a warning in the console. The scope also namespaces the caches: buckets are named `bit-bswup: - `, so several Bswup apps mounted under different scopes on one origin keep fully independent caches (each app only ever migrates and prunes its own; **changed in v-10-6-0** - the previous scope-less name `bit-bswup - ` made sibling apps evict each other's caches on every update). On upgrade, entries from a legacy-named bucket are migrated into the scoped bucket without re-downloading, and the legacy bucket is then cleaned up; on a multi-app origin a not-yet-upgraded sibling may lose its legacy bucket once during that transition (the old behavior did this on every update) and refills it on its next load. +- `log`: The log level of the Bswup logger. Available options are: `none`, `error`, `warn`, `info`, `verbose`, and `debug`. Each level includes everything above it (e.g. `info` also shows `warn` and `error`). Defaults to `warn`. Use `none` to silence all output. +- `sw`: The file path of the service-worker file. Defaults to `service-worker.js`. +- `handler`: The name of the global handler function for the service-worker events. Defaults to `bitBswupHandler` - which is also the name the built-in progress script (`bit-bswup.progress.js`, see the `BswupProgress` section below) registers, so the two wire up without configuration. The handler is re-resolved until found, so it may be registered after `bit-bswup.js` loads. **If no handler is ever registered, Bswup still completes a first install on its own** (it drives the finish handshake itself so the app boots instead of waiting for the stall watchdog); updates are simply left staged until the next full restart. +- `blazorScript`: The path of the Blazor entry-point script (the one you added `autostart="false"` to in step 3). When omitted, Bswup auto-detects both the Blazor Web App script (`_framework/blazor.web.js`) and the standalone Blazor WebAssembly script (`_framework/blazor.webassembly.js`), so you only need to set this if your script lives at a non-default path. Matching is fingerprint-tolerant: the fingerprinted names that .NET 9+ emits when the script is referenced through `@Assets["..."]` / the ImportMap (e.g. `_framework/blazor.web..js`) are recognized automatically, both for the auto-detected defaults and for an explicitly configured `blazorScript` value. +- `updateInterval`: Number of seconds between automatic update checks. By default the browser only re-checks the service worker on navigation and roughly every 24 hours, so a long-lived SPA tab can run a stale version for a long time. Set this to a positive number (e.g. `3600` for hourly) to have Bswup call `reg.update()` on a timer. Checks are skipped while the tab is in the background (the browser throttles those timers anyway) and resume when it becomes visible again. Omit or set to `0` to disable (the default). +- `updateOnVisibility`: When set to `true`, Bswup checks for an update every time the tab returns to the foreground (the `visibilitychange` event). This is a lightweight way to catch updates right when a user comes back to a tab they left open. Disabled by default. +- `stallTimeout`: Number of seconds of complete service-worker *silence* (no message, no lifecycle event) after which, on a **first install** only, Bswup stops waiting and starts Blazor directly from the network. This is the last line of defense against install failures that report nothing - most notably the browser terminating the service worker mid-install (browsers cap how long an install may run; Chromium kills it after ~5 minutes) - which would otherwise leave the app frozen behind the splash forever, since a first install only starts Blazor once the install completes. The page is uncontrolled at that point, so it behaves exactly as if no service worker existed, and the install is retried on the next load. Every progress message resets the timer, so a slow-but-healthy download never triggers it - only true silence does. Defaults to `60`; set `0` to disable. Updates are unaffected: the app is already running when an update stalls. +- `persistStorage`: When set to `true`, Bswup asks the browser to make the origin's storage persistent (`navigator.storage.persist()`) at startup. By default everything Bswup caches lives in *best-effort* storage: browsers silently reclaim it under disk pressure, and Safari deletes **all** storage for a site that has not been interacted with for seven days - the user comes back offline to an app that no longer boots. Persistent storage exempts the origin from that eviction. Disabled by default because the request can show a permission prompt (Firefox) and grant odds are engagement-based elsewhere; for the best odds, leave this off and call `BitBswup.persistStorage()` yourself at a high-signal moment (see the JavaScript API below). + +- `options`: The name of a global configuration object to read settings from. Defaults to `bitBswup`. Every option above can also be supplied as a property of that object (e.g. `window.bitBswup = { sw: 'my-sw.js', updateInterval: 3600 }` before the script loads); the object is merged over the built-in defaults first, and any script-tag attribute then overrides the matching property. This is the way to configure Bswup when the script is injected dynamically, where attribute-based configuration may not be readable. > You can remove any of these attributes, and use the default values mentioned above. @@ -73,10 +82,22 @@ function bitBswupHandler(type, data) { return console.log('downloading assets started:', data?.version); case BswupMessage.downloadProgress: + // data.percent is 0-100, data.index is the 1-based count of assets handled so far, + // and data.asset describes the asset that just finished: `url` (the path as declared + // in service-worker-assets.js / externalAssets), `reqUrl` (the absolute URL it was + // fetched from) and `hash` when the asset has one. + const percent = Math.round(data.percent); progressBar.style.width = `${percent}%`; - return console.log('asset downloaded:', data); + return console.log('asset downloaded:', data.asset.url, data); case BswupMessage.downloadFinished: + // data.reload activates the staged version (first install: claims + starts Blazor + // with no reload; update: SKIP_WAITING + reload). data.cleanup (optional) asks the + // active service worker to prune this app's stale cache buckets right away; it is + // safe to call at any time - the worker declines while an update is staged or + // staging (pruning then happens automatically on activation), and it never touches + // another app's caches. Most apps never need it: the same pruning already runs on + // activation and after every accepted update. if (data.firstInstall) { data.reload().then(() => { appEl.style.display = 'block'; @@ -92,14 +113,66 @@ function bitBswupHandler(type, data) { reloadButton.style.display = 'block'; reloadButton.onclick = data.reload; return console.log('new update ready.'); + + case BswupMessage.updateNotFound: + return console.log('checked for an update, already on the latest version.'); + + case BswupMessage.error: + // Structured install failure. data.reason is one of 'manifest' | 'integrity' | + // 'fetch' | 'cache' | 'request' | 'install-incomplete' | 'install-aborted' | + // 'install-infra' (the install died before/while touching CacheStorage - storage + // pressure, a broken private mode - always fatal); data.message is human readable, + // and data.url / data.hash point at the offending asset when known. + // + // data.fatal says whether the install actually stopped. Under the default 'lax' + // tolerance a failed asset is reported with `fatal: false` - the install still + // succeeds and that asset is fetched from the network on first use - so treat it + // as a warning, not a dead app. `fatal: true` (an invalid manifest, an abort under + // errorTolerance 'strict', or an 'install-infra' failure) means no usable staged + // version is available to this page. Note that a worker may still have been + // installed: a lax 'install-infra' failure resolves the install so the worker can + // keep serving as a network pass-through. + // + // data.firstInstall distinguishes where a fatal failure landed. `true`: it happened + // before the app ever booted - Bswup starts the app without a service worker so it + // still boots, and the built-in progress UI shows its failure panel. `false`: a + // background *update* failed - the app keeps running on the current version (the + // previous service worker keeps serving), so the built-in UI just clears any + // in-progress download splash and stays out of the way. + if (data.fatal === false) { + console.warn('Bswup asset skipped:', data.reason, data.message, data); + return; + } + console.error('Bswup install error:', data.reason, data.message, + ...(data.url ? [`url: ${data.url}`] : []), + ...(data.hash ? [`hash: ${data.hash}`] : []), + data); + return; } } ``` +> **Breaking Change - updates no longer auto-reload by default.** The built-in `BswupProgress` +> component's `AutoReload` parameter now defaults to `false`: when an update finishes +> downloading, the reload button is shown and the new version activates when the user accepts +> it, instead of every open tab reloading immediately - an unprompted reload discards whatever +> in-page state the user has mid-session. Set `AutoReload="true"` on the component to restore +> the old behavior. First installs are unaffected: they always complete the seamless +> claim-and-start flow (no reload) regardless of this setting. + +> **Multi-tab updates:** Service workers are single-instance per origin, so accepting an +> update in one tab activates the new version for every open tab. When that happens, Bswup +> has the new worker claim all clients and each *other* tab reloads itself automatically +> (via the `controllerchange` event) onto the new version. This keeps every tab consistent +> and avoids the classic failure where an old tab keeps running old app code while its +> asset requests are served from the new version's cache (mismatched boot config / DLL +> hashes). The first install is exempt: claiming a client for the first time starts Blazor +> and does not trigger a reload. + 6. Configure additional settings in the service-worker file like the following code: ```js -self.assetsInclude = [/\data.db$/]; +self.assetsInclude = [/\/data\.db$/]; self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; self.defaultUrl = '/'; self.prohibitedUrls = [/\/admin\//]; @@ -115,6 +188,7 @@ self.externalAssets = [ ]; self.assetsUrl = '/service-worker-assets.js'; self.noPrerenderQuery = 'no-prerender=true'; +self.cacheVersion = '2026.05.31-abc1234'; self.caseInsensitiveUrl = true; self.ignoreDefaultInclude = true; @@ -133,37 +207,211 @@ The most important line here is the last line which is the only mandatory config self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js'); ``` +> **Security note - the service worker is part of your trusted base.** Unlike the assets in +> `service-worker-assets.js` (which Bswup can verify with Subresource Integrity when the +> opt-in `enableIntegrityCheck` setting is enabled), the service-worker script itself cannot +> be integrity-pinned: browsers do not support an +> `integrity` option on `navigator.serviceWorker.register()`, and `importScripts()` has no +> SRI mechanism either. This is not Bswup-specific - Workbox and every other SW library share +> the limitation - but it matters because a service worker can intercept every request, so a +> tampered `service-worker.js` or `bit-bswup.sw.js` is effectively persistent, fully-privileged +> XSS. Treat the origin/CDN that serves these two files as part of your trusted computing base: +> serve them over HTTPS from an origin you control, and apply a strict Content-Security-Policy. +> +> To keep clients from getting stuck on a stale worker, Bswup registers with +> `updateViaCache: 'none'`, which tells the browser to bypass the HTTP cache for the +> service-worker script **and** the scripts it pulls in via `importScripts()` during update +> checks (the browser default, `'imports'`, would still serve imported scripts from the HTTP +> cache). That covers the whole `service-worker.js` -> `bit-bswup.sw.js` -> `service-worker-assets.js` +> import chain. As defense-in-depth - and because `updateViaCache` support is uneven (older +> Safari/iOS in particular) and intermediary proxies are not bound by it - also configure your +> server to send `Cache-Control: no-cache` (or `no-store`) for `service-worker.js` and +> `_content/Bit.Bswup/bit-bswup.sw.js` so every fetch revalidates against the origin. + The other settings are: +> **How the URL-matching lists are matched.** `assetsInclude`, `assetsExclude`, `prohibitedUrls`, +> `serverHandledUrls` and `serverRenderedUrls` all accept the same two kinds of entry: +> - a **`RegExp`** (e.g. `/\/admin\//`) is used as the pattern it is - this is what the +> examples below use, and what you want for anything non-trivial; +> - a **string** (e.g. `'/admin/'`) is matched **literally** as a substring of the URL. It is +> regex-escaped, so `'v1.0'` matches only `v1.0` and never `v1X0`. +> +> Prefer a `RegExp` whenever you need anchoring, alternation or wildcards - a literal string +> cannot express "ends with" or "starts with". Note that a string is a *substring* match, so +> `'app.css'` matches `/css/app.css` anywhere in the URL; anchor with a `RegExp` such as +> `/\/app\.css$/` if that is too broad. + - `assetsInclude`: The list of file names from the assets list to **include** when the Bswup tries to store them in the cache storage (regex supported). - `assetsExclude`: The list of file names from the assets list to **exclude** when the Bswup tries to store them in the cache storage (regex supported). -- `externalAssets`: The list of external assets to cache that are not included in the auto-generated assets file. For example, if you're not using `index.html` (like `_host.cshtml`), then you should add `{ "url": "/" }`. -- `defaultUrl`: The default page URL. Use `/` when using `_Host.cshtml`. -- `assetsUrl`: The file path of the service-worker assets file generated at compile time (the default file name is `service-worker-assets.js`). -- `prohibitedUrls`: The list of file names that should not be accessed (regex supported). -- `caseInsensitiveUrl`: Enables the case insensitivity in the URL checking of the cache process. +- `externalAssets`: The list of external assets to cache that are not included in the auto-generated assets file. For example, if you're not using `index.html` (like `_host.cshtml`), then you should add `{ "url": "/" }`. Accepted entry shapes: an object with a `url` (a concrete string, or a `RegExp` for server-generated names unknown ahead of time), a bare string (shorthand for `{ "url": "..." }`), or a bare `RegExp`; a single value also works without the array. An entry may carry a `hash` alongside its `url` - an SRI digest (`sha256-...`) that participates in the `?v=` cache busting and, when `enableIntegrityCheck` is on, in Subresource Integrity verification, exactly like a manifest asset. Entries whose `url` cannot be parsed as a request URL are skipped with a non-fatal `request` error instead of breaking the worker. Cross-origin entries (like the Google Tag Manager example above) are fetched in CORS mode first; when the host does not send CORS headers, Bswup retries with a `no-cors` request and caches the resulting *opaque* response so the asset still works offline (script and img tags consume opaque responses normally). This fallback is skipped for assets with an integrity check enabled, since an opaque body cannot be verified. Note that browsers deliberately pad opaque responses in storage-quota accounting (Chromium reserves several megabytes per entry), so prefer CORS-enabled hosts when you control them. Media assets work too: requests carrying a `Range` header (audio/video elements) are answered with a real `206 Partial Content` sliced from the cached full body when the asset is cached (Safari refuses to play media served as a `200` in response to a ranged request); when it is not cached yet, the ranged request goes to the network with its `Range` header intact so the server can answer `206` itself, and partial responses are never written to the cache - only full bodies are. Entries cached for `RegExp` patterns (server-generated file names unknown ahead of time, e.g. `_framework/resource-collection..js`) are kept across updates so the app still boots offline, but only the newest three generations per pattern survive each update - older fingerprints are evicted so the cache cannot grow without bound. +- `defaultUrl`: The default page URL, served from cache for navigation requests (the SPA fallback). Defaults to `index.html`; use `/` when using `_Host.cshtml`. The value must match an entry that actually exists in `service-worker-assets.js` or `externalAssets`; the comparison uses *resolved* URLs, so equivalent spellings match (`'index.html'` and `'/index.html'` are the same resource for a root-mounted app - they differ, correctly, for an app mounted on a sub-path). When nothing matches, offline navigation cannot work (navigations silently pass through to the network) and Bswup logs a `defaultUrl ... matches no asset` warning to the console at startup. Navigations whose URL is itself a managed asset are served that asset instead of the default document (**changed in v-10-6-0**): opening `/manifest.json` or an image directly in a tab shows that file, while route URLs (`/counter`, ...) match no asset and still get the app shell. If your host answers the shell URL with a redirect (for example `/` → `/index.html`, common on Cloudflare Pages, Netlify, and some reverse proxies), Bswup rebuilds that response before serving it to a navigation so the browser does not reject the followed redirect with *"a redirected response was used for a request whose redirect mode is not follow"* - offline deep-link navigation keeps working regardless. +- `assetsUrl`: The file path of the service-worker assets file generated at compile time (the default file name is `service-worker-assets.js`). The default is resolved relative to the service-worker script's own location - which is also where Blazor publishes the file - so it works unchanged for apps mounted on a sub-path (`https://host/myapp/`). Set it explicitly only when the file lives somewhere else; a leading `/` makes the path origin-absolute. +- `prohibitedUrls`: The list of file names that should not be accessed (regex supported). Matching requests are answered by the service-worker with `403 Forbidden` and a short `text/plain` body, for every HTTP method. **Changed in v-10-6-0:** previous versions answered `405 Method Not Allowed`; if your code detects a blocked URL by checking the status, look for `403`. **This is a client-side convenience, not a security boundary:** enforcement happens only inside the service worker, which is bypassed whenever the page is not controlled (the very first visit, a hard reload / Shift+Reload, browsers without service-worker support) and by any client that talks to the server directly. Access control for these URLs must be enforced on the server. +- `caseInsensitiveUrl`: Enables case-insensitive URL checking. This applies both to the asset cache matching and to every URL-matching regex list (`prohibitedUrls`, `serverHandledUrls`, `serverRenderedUrls`, `assetsInclude`, `assetsExclude`): when enabled, those patterns are compiled with the `i` flag so e.g. `prohibitedUrls: [/\/admin\//]` also blocks `/ADMIN/`. Patterns that already specify the `i` flag are left unchanged. - `serverHandledUrls`: The list of URLs that do not enter the service-worker offline process and will be handled only by server (regex supported). such as `/api`, `/swagger`, ... - `serverRenderedUrls`: The list of URLs that should be rendered by the server and not client while navigating (regex supported). such as `/about.html`, `/privacy`, ... - `noPrerenderQuery`: The query string attached to the default document request to disable the prerendering from the server so an unwanted prerendered result not be cached. - `ignoreDefaultInclude`: Ignores the default asset **includes** array which is provided by the Bswup itself which is like this: ```js - [/\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/, /\.svg$/, /\.woff2$/, /\.ttf$/, /\.webp$/] + [/\.dll$/, /\.wasm/, /\.pdb/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/, /\.svg$/, /\.woff2$/, /\.ttf$/, /\.webp$/] ``` + Note that `/\.wasm/`, `/\.pdb/` and `/\.html/` are deliberately unanchored (mirroring the standard Blazor template), so related variants such as `foo.wasm.br` also match when they appear in the manifest. - `ignoreDefaultExclude`: Ignores the default asset **excludes** array which is provided by the Bswup itself which is like this: ```js - [/^_content\/Bit\.Bswup\/bit-bswup\.sw\.js$/, /^service-worker\.js$/] + [ + /^_content\/Bit\.Bswup\/bit-bswup\.sw\.js$/, + /^_content\/Bit\.Bswup\/bit-bswup\.sw\.min\.js$/, + /^_content\/Bit\.Bswup\/bit-bswup\.sw-cleanup\.js$/, + /^_content\/Bit\.Bswup\/bit-bswup\.sw-cleanup\.min\.js$/, + /^service-worker\.js$/, + ] ``` - #### Keep in mind that caching service-worker related files will corrupt the update cycle of the service-worker. Only the browser should handle these files. -- `isPassive`: Enables the Bswup's passive mode. In this mode, the assets won't be cached in advance but rather upon initial request. + **Keep in mind** that caching service-worker related files will corrupt the update cycle of the service-worker. Only the browser should handle these files. +- `isPassive`: Enables the Bswup's passive mode. In this mode, the assets won't be cached in advance but rather upon initial request. Note that passive mode does not skip the full download entirely: on a *first* install, once Blazor has started, the service worker still tops up the cache in the background with every asset not yet fetched, so the app ends up fully offline-capable - what passive mode buys is that the first paint is never blocked behind a full precache. Assets being lazily fetched by the app while that top-up runs can be downloaded twice in that window (both writes land on the same cache keys, so this is a bandwidth cost, not a correctness issue). - `enableIntegrityCheck`: Enables the default integrity check available in browsers by setting the `integrity` attribute of the request object created in the service-worker to fetch the assets. -- `errorTolerance`: Determines how the Bswup should handle the errors while downloading assets. Possible values are: `strict`, `lax`, `config`. +- `errorTolerance`: Controls how the service worker reacts to asset download / cache failures during install. Possible values: + - `lax` (default): best-effort install. Asset failures never fail the install; missing assets are filled in lazily on the first fetch (in both passive and non-passive modes). Failed assets are reported through the `error` message with `fatal: false` and are counted toward the progress so the bar can still reach 100%. This is the default because it tolerates optional `externalAssets` that may legitimately 404, and because a failed install on a *first* visit would otherwise leave the app with no service-worker to complete the startup handshake. The download runs under the install event's `waitUntil`, so the browser keeps the service worker alive for the whole download and the worker only reaches the *waiting* state - and `updateReady` is only announced - once the background fill has settled. (Browsers cap how long an install may run - Chromium at roughly 5 minutes; a download outlasting the cap fails the install cleanly and is retried on the next load, and on a first install the page's `stallTimeout` watchdog still boots the app from the network.) + - `strict`: mirrors the standard Microsoft template / Workbox behavior. If any required asset fails to fetch or store during install, the install promise rejects, the partially populated cache is discarded, and the previous service-worker (if any) keeps serving the app. Failed assets are reported via the `error` message and are *not* counted toward the progress percentage, so 100% means every asset succeeded; the abort itself is reported once more with `reason: 'install-aborted'` and `fatal: true`. Choose this when a partial cache is unacceptable and you would rather stay on the previous version. On a first install (where there is no previous version to fall back to) Bswup starts the app without a service worker so it still boots from the network, and the install is retried on the next load. +- `maxRetries`: The number of *additional* download attempts after the first one when an asset fails transiently during install (a rejected fetch, or HTTP 408/429/5xx). Defaults to `2` (up to 3 total attempts). Deterministic failures - 404/403 and other permanent statuses, and Subresource Integrity mismatches - are never retried, since identical bytes would fail identically. +- `retryDelay`: The base backoff in milliseconds between those retries. Attempt *n* waits `retryDelay * 2^(n-1)` plus a random jitter (so a mass failure doesn't re-hit the origin in one synchronized burst). Defaults to `300`. - `enableDiagnostics`: Enables diagnostics by pushing service-worker logs to the browser console. - `enableFetchDiagnostics`: Enables fetch event diagnostics by pushing service-worker fetch event logs to the browser console. -- `disableHashlessAssetsUpdate`: Disables the update of the hash-less assets. By default, the Bswup tries to automatically update all of the hash-less assets (e.g. the external assets) every time an update found for the app. +- `disableHashlessAssetsUpdate`: Disables the update of hash-less assets. By default, Bswup automatically updates all hash-less assets (e.g. the external assets) every time an update is found for the app. - `forcePrerender`: Forces the prerendering of the default document for every navigation request to ensure that the server always has the latest version of the app. This is useful when you have a server-rendered app and you want to make sure that the client always has the latest version of the app. -- `enableCacheControl`: Enables the cache-control mechanism by providing cache busting setting and header to each request (`cache:no-store` settings and `cache-control:no-cache` header). -- `mode`: Determines the mode of the Bswup. Possible values are: +- `enableCacheControl`: Enables the cache-control mechanism by providing cache busting setting and header to each request (`cache:no-store` settings and `cache-control:no-cache` header). The `cache-control` header is only attached to same-origin requests: it is not a CORS-safelisted request header, so on a cross-origin asset it would force a preflight that most third-party hosts reject. Cross-origin requests rely on the `cache: no-store` option alone, which the CORS protocol never sees. +- `cacheVersion`: Overrides the value used to name the cache storage bucket (`bit-bswup: - `; the scope-path qualifier is what keeps multiple Bswup apps on one origin from evicting each other's caches - see the `scope` option above). By default this tracks Blazor's `assetsManifest.version` (a hash over the published assets), which means the cache is rotated automatically whenever any asset hash changes - and *only* then. Set `cacheVersion` to take manual control: pin it to a stable string so noisy dev rebuilds that perturb asset hashes don't needlessly evict the whole cache (runtime `.dll`/`.wasm` included), or bump it to force a refresh when a meaningful change lives outside Blazor's asset manifest. Only the cache bucket name (`CACHE_NAME`) is affected. Per-asset cache busting (`?v=`) is set in `createNewAssetRequest()` from each asset's `asset.hash` (falling back to `assetsManifest.version`), and Subresource Integrity uses `asset.hash` when integrity checking is enabled. When unset (or not a non-empty string) it falls back to the manifest version. Tip: feed it a build-stamped value (commit SHA, build timestamp, or your app's informational version) so it bumps automatically per publish. +- `mode`: Determines the mode of the Bswup. A mode is a preset bundle of defaults for the individual settings above (`isPassive`, `defaultUrl`, `forcePrerender`, `errorTolerance`, `caseInsensitiveUrl`, `noPrerenderQuery`): it only fills settings you have not assigned yourself, so any explicit assignment in the service-worker file always wins over the preset - including explicit falsy values such as `caseInsensitiveUrl = false` or `noPrerenderQuery = ''`. Possible values are: - `NoPrerender`: Disables the prerendering of the default document for every navigation request. - `InitialPrerender`: Enables the prerendering of the default document only for the initial navigation request. - `AlwaysPrerender`: Enables the prerendering of the default document for every navigation request. - - `FullOffline`: Enables the full offline mode where all assets are cached and served from the cache from first time the app is loaded. \ No newline at end of file + - `FullOffline`: Enables the full offline mode where all assets are cached and served from the cache from the first time the app is loaded. + +## The built-in progress UI (`BswupProgress`) + +Instead of writing the step-5 handler yourself, you can use the built-in splash/progress UI. It consists of the `BswupProgress` Razor component (the markup: progress bar, percentage, asset log, reload button, failure panel) and the `bit-bswup.progress.js` script, which registers the default `bitBswupHandler` and drives that markup. Reference both in your host document: + +```html + +... + + +``` + +```razor + +``` + +Component parameters (each maps to a `data-bit-bswup-*` attribute the script reads at load - the component emits no inline `