sw 0.1.6-dev
sw: ^0.1.6-dev copied to clipboard
A command-line tool to generate service worker files for web applications. It simplifies the process of creating service workers.
0.1.6-dev — 2026-09-02 #
Fixed #
-
Progress counter: the loading overlay could print a count greater than its own total (
Loaded 5 of 4 resources). The numerator and the denominator were computed from two different sets:resourcesCountcovered the pre-cached set (Core+Required), while the client counted every resource the SW reported — includingOptionalfiles served lazily by the fetch handler, which is unbounded. Progress messages now carrycounted, marking membership of the setresourcesCountdescribes, and the bootstrap only adds counted resources to the numerator. (packages/sw/src/sw/progress.ts,packages/sw/src/bootstrap/pipeline.ts) -
Progress counter: a page can be talked to by two workers at once — an old controller still serving fetches while a new build pre-caches in the background — and their manifests need not agree on a total. Messages now carry
swVersion, and the bootstrap ignores any worker other than the one it was built against, so the denominator cannot change mid-load. (packages/sw/src/sw/progress.ts,packages/sw/src/bootstrap/pipeline.ts) -
App shell:
index.htmlwas categorisedignore, so it never reached the manifest. The fetch handler'smanifest['index.html']lookups were consequently always empty: the shell was neither pre-cached nor reported as progress, and an offline navigation had nothing to fall back to on a cold profile. It is nowrequired— pre-cached on install, counted, and stored under one canonicalindex.htmlcache key that both the precache and the navigation handler use. (lib/src/categorizer.dart,packages/sw/src/sw/fetch-handler.ts) -
App shell: navigations to SPA deep links (
/chat/42) were dropped by the fetch handler. The manifest lookup ran first and returned early for any route without an entry of its own, so therequest.mode === 'navigate'branch below it was unreachable for exactly the routes that need it. The navigation check now runs first. (packages/sw/src/sw/fetch-handler.ts) -
App shell: every same-origin navigation wrote its response into the app-shell cache slot. A page the host serves outside the SPA rewrite — Firebase Hosting's
/__/auth/handlerand/__/auth/iframewhenauthDomainis the app's own domain, a static legal page, a download — replaced the shell with its own HTML, after which any navigation that falls back to cache (offline, origin 5xx, a deep link the origin 404s) served that page instead of the app. Only/and/index.htmlmay refresh the shell now,__/*is passed straight to the network (RESERVED_PATH_PREFIXES),, and a response that followed a redirect is stored only as a rebuilt copy with the flag dropped — browsers refuse to replay a redirected response for a navigation. (packages/sw/src/sw/fetch-handler.ts,packages/sw/src/shared/constants.ts) -
App shell: a failed cache write discarded a good page.
cache.putsat inside the sametryas the fetch, so quota pressure — routine for a wasm-sized app — turned a 200 from the origin into the stale cached copy, or into the literal503 Offlinepage on a profile with nothing cached yet. Cache writes and cache reads are now each contained, and nothing after a successful fetch can reject the response handed torespondWith. (packages/sw/src/sw/fetch-handler.ts) -
Service Worker:
progress.reportis best-effort. Its callers sit on the path that produces arespondWithresponse, so a failure to reach clients could surface as a failed navigation or a failedmain.dart.wasm. (packages/sw/src/sw/progress.ts) -
Generator: making the shell a manifest entry exposed an ordering problem that was harmless while it was
ignore—cleanupsubstitutes{{sw_version}}/{{flutter_service_worker_version}}intoindex.htmlafter the manifest is hashed, so the recorded hash and size would have described a file the origin never serves. The generator now substitutes and re-hashes the entry itself. The version is still derived from the pre-substitution manifest, so it stays deterministic; note that the derived value does change relative to 0.1.5, because the shell is now part of the manifest it is derived from. (lib/src/generator.dart,lib/src/cleanup.dart) -
Bootstrap: a
bootstrap.jsand answ.jsfrom different builds (an HTTP-cached bootstrap withoutCache-Control: no-cache) froze the counter silently, since no message ever passes theswVersioncheck. The mismatch is now reported — once, at teardown, and only when every message was foreign, so an ordinary update load (where the outgoing controller talks alongside the installing worker) stays quiet. (packages/sw/src/bootstrap/pipeline.ts) -
Bootstrap:
reloadIfForeignControllerresolved the SW filename againstlocation.hrefinstead of the document base URL thatnavigator.serviceWorker.registeractually uses. On any deep link it expected/chat/sw.js, judged the perfectly good/sw.jscontroller foreign, unregistered it and reloaded the page — once per tab session, on every deep link. (packages/sw/src/bootstrap/sw-registration.ts) -
Service Worker: resource keys are now relative to the worker's
registration.scope. Keyed by absolute pathname, every manifest lookup missed under a non-root<base href>— and, worse,RESERVED_PATH_PREFIXESstopped matching, so an app served from/app/routed Firebase Auth's/app/__/auth/*navigations through the app-shell branch.swapCachesused the same normalisation and so could evict the shell it had just pre-cached. (packages/sw/src/shared/utils.ts,packages/sw/src/sw/cache-manager.ts) -
Service Worker: a same-origin navigation that the origin redirects is no longer answered with the app shell. A navigation carries redirect mode
manual, so a 3xx arrives as an opaque redirect (status0,okfalse); it is now passed back untouched, which is what lets the browser follow it. (packages/sw/src/sw/fetch-handler.ts) -
Service Worker: the install-time pre-cache stores a replayable copy of the shell. It had no redirect guard at all — and on a cold profile it is the only writer of the shell — so a host that redirects
index.html?v=…left a copy the browser refuses to serve for any later navigation. (packages/sw/src/sw/cache-manager.ts) -
Service Worker:
notifyIndex('updated')is sent only when the shell was actually written, instead of announcing a refresh that quota pressure had just discarded. (packages/sw/src/sw/fetch-handler.ts) -
Service Worker: the pre-cache progress callback runs outside the block that attributes failures, so a throwing progress report can no longer be recorded as a failed resource — or, for a
Coreentry, fail the whole install over telemetry. (packages/sw/src/sw/cache-manager.ts) -
Generator:
--no-cleanupleavesindex.htmlalone again, including its version placeholders. Substituting under that flag both broke its contract and made the derived version depend on a file the same run had rewritten, so re-running the documented in-place dev loop shipped a new version for byte-identical content. (lib/src/generator.dart)
Changed #
- Service Worker:
cacheFirstdegrades to a plain network fetch whenCacheStorageis unusable (evicted, blocked by a privacy mode) instead of failing the resource outright. - Protocol:
SWProgressMessagegainsswVersionandcounted.resourcesSizenow describes the counted set rather than every cacheable resource, so it agrees withresourcesCount; consumers reading it as "total bytes on disk" should switch to summing the manifest. See docs/service-worker.md for the two rules a client must follow. - Service Worker: the startup banner reports
counted: N(wasprecache: N) and itssizeis the total cacheable bytes, matchingresources: Nbeside it. - Service Worker: every
sw-progressmessage is emitted through one reporter (createProgressReporter) instead of each handler assembling its own; the pre-cache and the counted set are now driven by the sameCOUNTED_CATEGORIESlist and cannot drift apart.
Tests #
- Playwright: cold, warm and five-consecutive-cold loads assert that the numerator never leaves the counted set, reading the SW messages directly rather than the overlay text (which stops updating when the app takes over). On 0.1.5 the cold load counts 9 resources against a total of 6.
- Playwright: the app shell is pre-cached, answers an offline navigation, and answers a route the origin 404s.
- Playwright: visiting a same-origin page outside the SPA leaves the cached shell intact, and a deep link still gets the app afterwards. Both fail on the first cut of this fix.
- Vitest: host-reserved navigations are passed through; the shell is refreshed only from a request for the shell; a redirected response is not stored; a page the origin served is still returned when the cache write fails; precache and the fetch handler agree on the shell cache key.
- Vitest:
progress.tsset membership and message shape; pipeline counter behaviour for foreign versions, uncounted resources and a fixed denominator. - Vitest: an app served from a subpath keeps host-reserved paths reserved, treats its scope root as the shell request, and serves deep routes from the pre-cached shell.
- Vitest: a redirected shell is stored replayably by both writers; an opaque redirect is passed back rather than answered with the shell.
- Vitest: the counter clamp is reached — two builds reporting under one pinned
--version— and the foreign-version report fires only when every message was foreign. - Dart:
generate()over a synthetic build directory pins the shell entry against the bytes on disk, version stability across in-place re-runs, and that the version still moves for any changed resource including the shell. - Playwright: the counter specs wait for the worker to go quiet instead of sleeping on fixed timers, and the five-load spec installs its recorder once.
0.1.5 — 2026-04-22 #
Changed #
- Example: simplified the platform-specific update-check wiring with a conditional-import entrypoint and clarified the example update API contract for the user-approved refresh flow. (
example/lib/src/update/platform/update_check.dart,example/lib/src/update/platform/update_check_js.dart,example/lib/src/update/platform/update_check_vm.dart,example/lib/src/update/update_check_api.dart)
0.1.4 — 2026-04-22 #
Fixed #
- Bootstrap:
WebAssembly.instantiate(): Import #N "X": module is not an object or functionfailures on first load after a deploy. When an old SW kept controlling the page while a new SW installed in the background, any file missing from the old SW's manifest fell through to the network (fresh) while cached files stayed stale — so Flutter would load a freshmain.dart.mjsagainst a stalemain.dart.wasmand the import schemas would disagree. Bootstrap now force-activates a pre-existing waiting worker at registration time via{type:'skipWaiting'}+controllerchange, so subsequentfetch(main.dart.wasm)/import(main.dart.mjs)go through the new SW with a coherent cache. Bounded bySW_REGISTRATION_TIMEOUT_MS; falls back to the oldwaitForActivationpath on timeout. (packages/sw/src/bootstrap/sw-registration.ts) - Bootstrap: pages controlled by a foreign SW at a different script path (e.g., an old
flutter_service_worker.jsleft from a pre-migration deploy) can serve a similarly incoherent mix of files. Bootstrap now detects this pre-registration and performs a one-shotlocation.reload()after unregistering the offending registration. AsessionStorageguard prevents reload loops if the handoff doesn't take. (packages/sw/src/bootstrap/sw-registration.ts,packages/sw/src/bootstrap/pipeline.ts)
Changed #
- Bootstrap: foreign-controller cleanup is surgical — only the registration whose active worker equals the current
navigator.serviceWorker.controlleris unregistered. Unrelated registrations at other scopes (typicallyfirebase-messaging-sw.jsfor push notifications) are left intact so their subscriptions survive the recovery. (packages/sw/src/bootstrap/sw-registration.ts)
Scope note #
- The pre-existing-waiting auto-activation is intentionally bootstrap-only: once the app is running, updates continue through the existing
sw-update-available→applyUpdateuser-approval flow, so running sessions are never yanked out from under the user.
0.1.3 — 2026-04-20 #
Fixed #
- Bootstrap:
Bootstrap.onUpdateAvailablehandlers were silently dropped onflutter-first-framebecauseBootstrapAPI.dispose()cleared theupdateHandlersSet alongside the (correctly load-phase-scoped) progresssubscribers. Update handlers are now retained for the lifetime of the page, so apps reliably receive the "new SW installed" signal after the loading widget disappears. (packages/sw/src/bootstrap/api.ts) - Bootstrap: the
sw-update-availableDOM bridge listener installed byrunPipelinewas registered with{ once: true }, so a long-lived tab encountering a second deploy never notified handlers. The listener is now permanent for the page lifetime;wireUpdateDetectionalready dispatches at most once per SW install. (packages/sw/src/bootstrap/pipeline.ts)
Changed #
- Bootstrap:
BootstrapAPI.notifyUpdateAvailablenow routes async-handler rejections toconsole.errorinstead of leaving an "Uncaught (in promise)" warning that masks the real error. (packages/sw/src/bootstrap/api.ts)
0.1.2 — 2026-04-20 #
Fixed #
- Bootstrap: production deploys on hosts with SPA rewrites (Firebase Hosting, Netlify with
index.htmlfallback, etc.) crashed at_flutter.loader not available after loading flutter.jsbecause the server returnedindex.htmlfor the bareflutter.jsfetch. The bootstrap now inlines Flutter'sflutter.jsIIFE intobootstrap.jsat generation time, so no runtime fetch offlutter.jsis performed and the loader is available before the pipeline starts.
Changed #
- Cleanup:
version.jsonis no longer deleted — it carries Flutter's app version metadata and may be read by the app or external tooling at runtime. - Cleanup:
flutter.jsis now removed from the deployed output (its loader is inlined intobootstrap.js). - Docs: README "CI step" snippet no longer recommends
rm -f build/web/flutter.jsmanually — the generator handles it.
0.1.1 — 2026-04-20 #
Added #
- Docs: "Local Development" section in README.md covering the monorepo layout, TypeScript ↔ Dart build pipeline, and commands for running the generator against
example/build/web/.
0.1.0 — 2026-04-20 #
Complete rewrite replacing Flutter's default bootstrap with a professional two-artifact system.
Fixed (post-audit hardening) #
- Generator: crash on short or empty
engineRevisionnow surfaces a clear error before artifact generation. - CLI precedence:
--flagwith an ArgParser default no longer shadows YAML / env values (wasParsedgate). Glob options (--core,--required, …) now honourSW_CORE,SW_REQUIRED,SW_OPTIONAL,SW_IGNORE,SW_GLOB,SW_EXCLUDE_GLOBenvironment variables that previously did nothing. - CLI: invalid
--min-progress/--max-progressvalues now fail with exit code 64 instead of silently defaulting to0/90. - Cleanup: canvaskit pruning uses consistent URL-style paths, fixing a Windows case where
\-separated paths never matched the keep-set. - SW
notifyClients: a single dead client no longer aborts iteration — every other client still receives the progress update. - SW
cacheFirst: non-OK responses (4xx/5xx) now emit anerrorprogress event so the bootstrap UI can surface the failure instead of hanging onloading. Fallback 503s carryContent-Type: text/plain. - SW
swapCaches: reordered to copy-then-evict-then-persist-manifest, and eviction now excludes paths that were just re-precached from temp (no more accidental deletion of the fresh bytes). - SW
message-handler: accepts both"skipWaiting"strings and{type: "skipWaiting"|"getVersion", requestId?}objects;getVersionreplies echorequestIdfor correlation. - Loading widget: dispose adds a 600ms safety teardown, so the widget and its stylesheet don't leak when
transitionenddoesn't fire (reduced-motion, background tabs). - CanvasKit loader:
detectWebGLVersionreleases its temporary canvas + GL context viaWEBGL_lose_contextinstead of relying on GC.
Added #
- Deterministic
version: when--version/SW_VERSION/ YAML is absent, the generator derives a stable 12-char sha256 over the manifest contents. Re-running against an unchanged build now yields the same SW version. - Dart UI defaults → bootstrap:
--logo,--title,--theme,--color,--min-progress,--max-progressflags are baked intobootstrap.jsasBuildConfig.uiDefaults.data-configstill overrides at runtime. - Precache concurrency cap (
PRECACHE_CONCURRENCY = 6) so large manifests don't stampede the origin during install. - Manifest size warning: generator emits a stderr warning when
sw.jsexceeds 10 MB. - Update prompt API:
window.Bootstrap.onUpdateAvailable(handler)andwindow.Bootstrap.applyUpdate(reload=true)let apps prompt for and apply a waiting SW upgrade.sw-registration.tsexportsactivateWaitingSW(registration)for lower-level use. - CI: new
e2eGitHub Actions job runsflutter build web+dart run sw:generate+ Playwright on each PR;SW_E2E_BROWSERS=allopts into the Chromium/Firefox/WebKit matrix. - Tests:
test/config_test.dart,test/files_test.dart, and new injector cases coveringuiDefaults. Removed the emptytest/unit_test.dartplaceholder.
Original 0.1.0 feature set #
Breaking Changes #
- CLI arguments updated: new options added, some defaults changed
- Generated output now produces two files (
sw.js+bootstrap.js) instead of one - Requires
<script defer data-sw-bootstrap src="bootstrap.js">inindex.html - Old inline JS/CSS loading UI replaced by the built-in loading widget
Added #
- Bootstrap pipeline — 6-stage initialization replacing
flutter_bootstrap.js- Stages: Init → SW Registration → CanvasKit → Assets → Dart Entry → Dart Init
- Loading widget — Responsive circular progress with SVG ring, stall detection, error display, dark/light/auto theme
- CanvasKit CDN loading — Automatic
engineRevisionextraction, Google CDN with local fallback - Resource categorization — Core, Required, Optional, Ignore with glob-based overrides
- Global API —
window.Bootstrap.dispose(),.progress,.subscribe(cb)for Dart integration - YAML config —
sw.yamlas alternative to CLI args (priority: CLI > YAML > env > defaults) - Exponential backoff — 3 retry attempts with 1s/2s/4s delays and jitter
- Cache busting — Hash-based
?v={hash}query params on all cached resources - Atomic cache updates — Temp cache during install, swapped on activate
- Console logging — Styled version banner with engine revision, SW version, renderer info
- Stall detection — "Reset Cache" button after 30s without progress
- Auto-cleanup — Removes
flutter_bootstrap.js,flutter_service_worker.js,version.json,.js.map,.js.symbols - TypeScript source — SW and Bootstrap written in TypeScript, compiled via Vite to minified IIFE
Changed #
- Monorepo structure:
packages/sw/(TypeScript) + root (Dart CLI) - Service Worker rewritten in TypeScript with modular architecture
- Cache naming:
{prefix}-{version}for content,{prefix}-manifestfor manifest storage bootstrap.js,index.html,sw.jsare never cached by the SW
Removed #
- Inline JS/CSS string templates in Dart (replaced by compiled TypeScript)
downloadOfflinecommand (simplified caching model)- Navigation preload (replaced by simpler network-first for index.html)
- Comment stripping (Vite handles minification)
0.0.7 #
- Simplify service worker by removing retry logic and navigation preload.
- Replace
Promise.racetimeout wrappers with modernAbortController-basedfetchWithTimeout. - Remove unused constants (
MEDIA_EXT,NETWORK_ONLY,RETRY_DELAY). - Remove
INSTALL_TIMEOUT,ACTIVATE_TIMEOUTwrappers from install/activate events. - Streamline activate event handler by removing redundant
Promise.racenesting.
0.0.6 #
- Add timeout protection for install (30s) and activate (30s) events.
- Add fetch timeout (10s) and retry logic (2 retries with 500ms delay).
- Add navigation preload support for online-first strategy.
- Clean up all stale caches with matching prefix on activation.
- Ensure
self.clients.claim()is always called, even on error/timeout. - Emit
sw-version.txtalongsidesw.jsfor CI version injection.
0.0.5 #
- Update index.html example to include more features.
0.0.4 #
- Improved service worker generation.
0.0.3 #
- Service worker generation now based on the flutter's
flutter_service_worker.js.
0.0.2 #
- Proof of concept for service worker generation
0.0.1 #
- Initial release with basic functionality