diff --git a/.github/workflows/bit.ci.Bswup.yml b/.github/workflows/bit.ci.Bswup.yml index 5192c57506..2e74e2aad2 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 install + npm test diff --git a/src/Bswup/Bit.Bswup.Demo/Pages/HomePage.razor b/src/Bswup/Bit.Bswup.Demo/Pages/HomePage.razor index 4ef62a7a95..bb30021363 100644 --- a/src/Bswup/Bit.Bswup.Demo/Pages/HomePage.razor +++ b/src/Bswup/Bit.Bswup.Demo/Pages/HomePage.razor @@ -3,7 +3,7 @@ Home -

222

+

111

Hello, world!

diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/index.html b/src/Bswup/Bit.Bswup.Demo/wwwroot/index.html index 951dd57acf..989930555b 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/index.html +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/index.html @@ -106,17 +106,24 @@ 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 console.log('downloading update assets started:', data?.version); appEl.style.display = 'none'; bswupEl.style.display = 'block'; return console.log('downloading assets started:', data?.version); case BswupMessage.downloadProgress: + // Built with textContent (not innerHTML) so asset names can never be + // parsed as markup - the same hardening as the bundled progress script. const li = document.createElement('li'); - li.innerHTML = `${data.index}: ${data.asset.url}: ${data.asset.hash}` + const urlEl = document.createElement('b'); + urlEl.textContent = data.asset.url; + li.append(`${data.index}: `, urlEl, `: ${data.asset.hash}`); assetsUl.prepend(li); const percent = Math.round(data.percent); progressBar.style.width = `${percent}%`; - percentLabel.innerHTML = `${percent} %`; + percentLabel.textContent = `${percent} %`; return console.log('asset downloaded:', data); case BswupMessage.downloadFinished: @@ -137,16 +144,34 @@ reloadButton.style.display = 'block'; reloadButton.onclick = data.reload; return console.log('new update ready.'); + + case BswupMessage.error: + // This demo deliberately ships a 404ing external asset (see the dev + // service-worker.js), so non-fatal errors are expected here: under the + // default lax tolerance the install continues and the asset lazy-fills. + console.error('installation error:', data); + if (data.fatal === false) return; + // A fatal failure during a background update leaves the running app + // untouched - the previous worker keeps serving it. + if (data.firstInstall === false) return; + // Fatal first-install failure: bit-bswup.js force-starts Blazor from the + // network; reveal the app instead of leaving it booted behind the splash. + descriptionLabel.textContent = 'Install failed; starting without offline support...'; + appEl.style.display = 'block'; + bswupEl.style.display = 'none'; + return; } } + + handler="bitBswupHandler"> diff --git a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js index 234b154b45..713c3d4128 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.js @@ -2,13 +2,15 @@ self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; self.caseInsensitiveUrl = true; -self.precachedAssetsInclude = [/favicon\.ico$/, /icon-512\.png$/, /bit-bw-64\.png$/]; 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/Bit.Bswup.Demo/wwwroot/service-worker.published.js b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js index 98df52a75d..9039228117 100644 --- a/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js +++ b/src/Bswup/Bit.Bswup.Demo/wwwroot/service-worker.published.js @@ -2,14 +2,19 @@ self.assetsExclude = [/\.scp\.css$/, /weather\.json$/]; 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.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/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.slnx b/src/Bswup/Bit.Bswup.slnx index 2b41b652c7..6bf34c2593 100644 --- a/src/Bswup/Bit.Bswup.slnx +++ b/src/Bswup/Bit.Bswup.slnx @@ -7,12 +7,12 @@ - - + + - - + + diff --git a/src/Bswup/Bit.Bswup/Bit.Bswup.csproj b/src/Bswup/Bit.Bswup/Bit.Bswup.csproj index 0335bfd2f5..9969d5d918 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,126 @@ + - - + + - - - + + - - - - + + + + + + + + + + + + + + - + + + + + + + - + + + - - - + + + + + + + + + + + + + + + + + + - - - - - - - - \ No newline at end of file + diff --git a/src/Bswup/Bit.Bswup/BswupProgress.razor b/src/Bswup/Bit.Bswup/BswupProgress.razor index 3d6344e5a2..1907b2d5e8 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-5-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 + + @* 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. *@ + + + + + + + diff --git a/src/Bswup/FullDemo/Server/Components/_Imports.razor b/src/Bswup/FullDemo/Server/Components/_Imports.razor new file mode 100644 index 0000000000..23aacbed1f --- /dev/null +++ b/src/Bswup/FullDemo/Server/Components/_Imports.razor @@ -0,0 +1,13 @@ +@using System.Net.Http +@using System.Net.Http.Json +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.JSInterop +@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/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 index 49c224c338..37bf9cc289 100644 --- a/src/Bswup/FullDemo/Server/Program.cs +++ b/src/Bswup/FullDemo/Server/Program.cs @@ -1,20 +1,50 @@ -var builder = WebApplication.CreateBuilder(args); +using System.IO.Compression; +using Bit.Bswup.FullDemo.Server.Components; +using Microsoft.AspNetCore.ResponseCompression; -#if DEBUG -if (OperatingSystem.IsWindows()) +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddRazorComponents() + .AddInteractiveWebAssemblyComponents(); + +builder.Services.AddControllers(); +builder.Services.AddHttpContextAccessor(); + +builder.Services.AddResponseCompression(opts => { - builder.WebHost.UseUrls("https://localhost:5021", "http://localhost:5020", "https://*:5021", "http://*:5020"); + 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 { - builder.WebHost.UseUrls("https://localhost:5021", "http://localhost:5020"); + 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(); } -#endif -Bit.Bswup.Demo.Server.Startup.Services.Add(builder.Services); +app.UseHttpsRedirection(); -var app = builder.Build(); +app.MapStaticAssets(); +app.UseAntiforgery(); + +app.MapControllers(); -Bit.Bswup.Demo.Server.Startup.Middlewares.Use(app, builder.Environment); +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/FullDemo/Server/Properties/launchSettings.json index c60dbf41d8..a257a7aa19 100644 --- a/src/Bswup/FullDemo/Server/Properties/launchSettings.json +++ b/src/Bswup/FullDemo/Server/Properties/launchSettings.json @@ -1,7 +1,7 @@ { "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "Bit.Bswup.Demo.Server": { + "Bit.Bswup.FullDemo.Server": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, 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/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Bit.Bswup.NewDemo.Client.csproj b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Bit.Bswup.NewDemo.Client.csproj similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Bit.Bswup.NewDemo.Client.csproj rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Bit.Bswup.NewDemo.Client.csproj diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor similarity index 68% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor index 19a3f2febc..af229ba46e 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor +++ b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor @@ -23,5 +23,8 @@ - + @* No hand-written #bit-bswup-reload here: since v-10-5-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/NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor.css similarity index 92% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor.css rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor.css index 218b51c948..60037ea8fb 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor.css +++ b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/DemoBswupProgressBar.razor.css @@ -80,4 +80,7 @@ right: 10px; display: none; position: fixed; + /* Above the fixed page header (z-index: 1000 in MainLayout.razor.css) - 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/NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor.css b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor.css similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor.css rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Layout/MainLayout.razor.css diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Counter.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Pages/Counter.razor similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Counter.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Pages/Counter.razor diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Home.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Pages/Home.razor similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Pages/Home.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Pages/Home.razor diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Program.cs b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Program.cs similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Program.cs rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Program.cs diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Routes.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Routes.razor similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/Routes.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/Routes.razor diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/_Imports.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/_Imports.razor similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/_Imports.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/_Imports.razor diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/app.css b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/app.css similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/app.css rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/app.css diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/bit-bw-64.png b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/bit-bw-64.png similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/bit-bw-64.png rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/bit-bw-64.png diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/favicon.ico b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.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/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/favicon.ico diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/icon-512.png b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/icon-512.png similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/icon-512.png rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/icon-512.png diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/manifest.json b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/manifest.json similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/manifest.json rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/manifest.json diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js similarity index 71% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js index 476267b680..9024137b2d 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js +++ b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.js @@ -11,7 +11,9 @@ self.assetsExclude = [ ]; self.defaultUrl = '/'; self.prohibitedUrls = []; -self.assetsUrl = '/service-worker-assets.js'; +// self.assetsUrl is deliberately NOT set: since v-10-5-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/ @@ -31,6 +33,12 @@ self.externalAssets = [ }, { "url": "Bit.Bswup.NewDemo.Client.bundle.scp.css" + }, + { + // Server-generated Blazor Web boot module with a fingerprint that changes each publish, + // so it can't be listed as an exact asset. This RegExp lets Bswup cache it lazily so the + // app still boots offline. + "url": /\/_framework\/resource-collection\..+\.js$/ } ]; diff --git a/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js new file mode 100644 index 0000000000..12f9b3c525 --- /dev/null +++ b/src/Bswup/NewDemo/Bit.Bswup.NewDemo.Client/wwwroot/service-worker.published.js @@ -0,0 +1,59 @@ +// bit version: 10.5.0 + +self.assetsInclude = []; +self.assetsExclude = [ + /Bit\.Bswup\.NewDemo\.Client\.styles\.css$/ +]; +self.defaultUrl = '/'; +self.prohibitedUrls = []; +// self.assetsUrl is deliberately NOT set: since v-10-5-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 = [ + { + "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" + }, + { + // Server-generated Blazor Web boot module with a fingerprint that changes each publish, + // so it can't be listed as an exact asset. This RegExp lets Bswup cache it lazily so the + // app still boots offline. + "url": /\/_framework\/resource-collection\..+\.js$/ + } +]; + +self.caseInsensitiveUrl = true; + +self.serverHandledUrls = [/\/api\//]; +self.serverRenderedUrls = [/\/privacy$/]; + +self.noPrerenderQuery = 'no-prerender=true'; + +self.isPassive = true; + +//self.enableDiagnostics = true; +//self.enableFetchDiagnostics = true; + +//// 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/Bit.Bswup.NewDemo.csproj b/src/Bswup/NewDemo/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.csproj similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.csproj rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo.csproj diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/App.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo/Components/App.razor similarity index 79% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/App.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo/Components/App.razor index 3110bb5054..d0d3029849 100644 --- a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/App.razor +++ b/src/Bswup/NewDemo/Bit.Bswup.NewDemo/Components/App.razor @@ -21,15 +21,16 @@ -

111

+ @* 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. *@ + handler="bitBswupHandler"> diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/Pages/Error.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo/Components/Pages/Error.razor similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/Pages/Error.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo/Components/Pages/Error.razor diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/_Imports.razor b/src/Bswup/NewDemo/Bit.Bswup.NewDemo/Components/_Imports.razor similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Components/_Imports.razor rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo/Components/_Imports.razor diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Program.cs b/src/Bswup/NewDemo/Bit.Bswup.NewDemo/Program.cs similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Program.cs rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo/Program.cs diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Properties/launchSettings.json b/src/Bswup/NewDemo/Bit.Bswup.NewDemo/Properties/launchSettings.json similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/Properties/launchSettings.json rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo/Properties/launchSettings.json diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.Development.json b/src/Bswup/NewDemo/Bit.Bswup.NewDemo/appsettings.Development.json similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.Development.json rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo/appsettings.Development.json diff --git a/src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.json b/src/Bswup/NewDemo/Bit.Bswup.NewDemo/appsettings.json similarity index 100% rename from src/Bswup/Bit.Bswup.NewDemo/Bit.Bswup.NewDemo/appsettings.json rename to src/Bswup/NewDemo/Bit.Bswup.NewDemo/appsettings.json diff --git a/src/Bswup/README.md b/src/Bswup/README.md index 7ab1845ca7..2c55ebc08f 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-5-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,63 @@ 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. Only `fatal: true` (an invalid manifest, or an + // abort under errorTolerance 'strict') means no new version was installed. + // + // 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 +185,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 +204,204 @@ 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 verifies with Subresource Integrity), 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-5-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-5-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. +- `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 `