sw 0.1.5
sw: ^0.1.5 copied to clipboard
A command-line tool to generate service worker files for web applications. It simplifies the process of creating service workers.
Service Worker Generator #
A complete Flutter Web bootstrap replacement that generates an optimized Service Worker and Bootstrap pipeline. Replaces Flutter's default flutter_bootstrap.js with a professional loading experience featuring progress tracking, intelligent caching, CanvasKit CDN loading, and a customizable loading widget.
Features #
- Full Bootstrap Replacement — Replaces Flutter's
flutter_bootstrap.jswith a controlled initialization pipeline - Loading Widget — Responsive circular progress indicator with status text, stall detection, and error handling
- CanvasKit CDN Loading — Loads CanvasKit from Google CDN with automatic local fallback
- Smart Resource Categorization — Core, Required, Optional, and Ignore categories with configurable caching strategies
- Automatic File Scanning — Analyzes build directory and creates resource manifest with MD5 hashes
- Cache Busting — Hash-based query params prevent stale cached resources
- Retry with Backoff — Exponential backoff retry (3 attempts, 1s/2s/4s delays) with 10s timeout per request
- Progress Notifications — Real-time
sw-progressmessages from Service Worker to loading widget - Version Management — Cache versioning with atomic updates for safe deployments
- Stall Detection — Shows "Reset Cache" button after 30s without progress
- Console Logging — Styled version banner and progress logging in browser console
- Global API —
window.Bootstrapfor Dart integration (dispose, progress, subscribe) - Update Prompt API — Use
Bootstrap.onUpdateAvailable()to show a "new version available" prompt, then callBootstrap.applyUpdate()to activate the waiting Service Worker and reload into the new build. - Flexible Configuration — CLI args, YAML config file, and environment variables (priority: CLI > YAML > env)
- Cross-Platform — Works on Windows, macOS, and Linux
Requirements #
- Dart SDK: >= 3.11.0
- Flutter: >= 3.41.0
Installation #
dart pub global activate sw
Or add as a dev dependency:
dev_dependencies:
sw: ^0.1.2
Pin the minor version in CI to avoid surprise template changes between builds:
dart pub global activate sw ^0.1.2
Quick Start #
# 1. Build your Flutter web app
flutter build web --release --wasm --base-href=/ -o build/web
# 2. Swap the dev index.html for the prod template (see "Local Development")
# (web/index.prod.html must keep an explicit <base href="/" />)
cp web/index.prod.html build/web/index.html
# 3. Generate service worker and bootstrap
dart run sw:generate \
--input=build/web \
--prefix=my-app \
--version="$(git rev-parse --short=8 HEAD)"
The generator produces two files in your build directory:
sw.js— Service Worker with resource manifest and caching logicbootstrap.js— Initialization pipeline with loading widget
And automatically:
- Extracts
engineRevisionand build config from Flutter's output - Categorizes all files (Core/Required/Optional/Ignore)
- Removes Flutter's deprecated files (
flutter_bootstrap.js,flutter_service_worker.js,flutter.js— its loader is inlined intobootstrap.js) - Removes
.js.mapand.js.symbolsfiles (unless--keep-maps) - Replaces
{{sw_version}}placeholders inindex.html
Server Configuration #
⚠️ Required. Your HTTP server must serve
bootstrap.js,index.html, andsw.jswithCache-Control: no-cache. Without this, users can get stuck on an outdated build.
Why it matters #
bootstrap.js is regenerated on every build with an embedded config snapshot — engine revision, SW version, manifest filename, and renderer variants are injected directly into the file. If a CDN or browser HTTP cache returns a stale bootstrap.js, the client registers an outdated Service Worker and never discovers the new manifest. The Service Worker's own invalidation logic cannot fix this, because it only runs after bootstrap.js has already loaded.
The same applies to index.html (the entry point that loads bootstrap.js) and sw.js (the browser updates the Service Worker by byte-comparing its response — a stale copy defeats the update check).
Required headers #
| File | Cache-Control |
Notes |
|---|---|---|
index.html |
no-cache |
Allows 304 via ETag/Last-Modified — fast but always revalidated |
bootstrap.js |
no-cache |
Content changes every build (embedded config snapshot) |
sw.js |
no-cache |
Browser also enforces ≤ 24h SW freshness, but explicit is better |
| Everything else | public, max-age=31536000, immutable |
Safe: the SW manifest handles invalidation via MD5 hashes |
Use no-cache, not no-store — no-cache lets the browser keep the file on disk but forces a conditional request on every load, so unchanged builds cost only a 304 response.
Example server configurations #
nginx
location = /index.html { add_header Cache-Control "no-cache"; }
location = /bootstrap.js { add_header Cache-Control "no-cache"; }
location = /sw.js { add_header Cache-Control "no-cache"; }
location / {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri $uri/ /index.html;
}
Apache (.htaccess)
<FilesMatch "^(index\.html|bootstrap\.js|sw\.js)$">
Header set Cache-Control "no-cache"
</FilesMatch>
Netlify (_headers)
/index.html
Cache-Control: no-cache
/bootstrap.js
Cache-Control: no-cache
/sw.js
Cache-Control: no-cache
Firebase Hosting (firebase.json)
{
"hosting": {
"headers": [
{
"source": "/(index.html|bootstrap.js|sw.js)",
"headers": [{ "key": "Cache-Control", "value": "no-cache" }]
}
]
}
}
Vercel (vercel.json)
{
"headers": [
{
"source": "/(index.html|bootstrap.js|sw.js)",
"headers": [{ "key": "Cache-Control", "value": "no-cache" }]
}
]
}
index.html Setup #
Replace Flutter's default index.html content with a single script tag:
<!DOCTYPE html>
<html>
<head>
<base href="/" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#25D366" />
<link rel="manifest" href="manifest.json" />
<link rel="icon" type="image/png" href="favicon.png" />
<title>My App</title>
</head>
<body>
<script
defer
data-sw-bootstrap
src="bootstrap.js"
data-config='{
"logo": "icons/Icon-192.png",
"title": "My App",
"theme": "auto",
"color": "#25D366"
}'
></script>
</body>
</html>
The explicit <base href="/" /> is intentional for the production template and should not be replaced with $FLUTTER_BASE_HREF. If you deploy under a subpath, keep the base href explicit there too, and make sure it still ends with /.
The loading widget, progress tracking, service worker registration, and Flutter initialization are all handled by bootstrap.js automatically.
Bootstrap Configuration (data-config) #
| Option | Type | Default | Description |
|---|---|---|---|
logo |
string | — | Path to logo image for the loading widget |
title |
string | — | Title text displayed below the logo |
theme |
string | "auto" |
Widget theme: "light", "dark", or "auto" |
color |
string | "#25D366" |
Accent color for the progress ring |
showPercentage |
boolean | true |
Show numeric percentage below status text |
minProgress |
number | 0 |
Minimum progress value for the bootstrap range |
maxProgress |
number | 90 |
Maximum progress value (Dart manages the rest) |
Local Development #
flutter run -d chrome and flutter run -d web-server do not invoke the generator — they serve Flutter's default bootstrap for hot reload. This means bootstrap.js does not exist during flutter run, and the production index.html (which references it) would break hot reload.
The recommended pattern is to keep two index.html templates side by side in your web/ folder:
| File | Purpose | Used by |
|---|---|---|
web/index.html |
Dev template with Flutter's {{flutter_js}} / {{flutter_build_config}} placeholders |
flutter run (hot reload, dev mode) |
web/index.prod.html |
Prod template that loads bootstrap.js generated by sw |
CI / release builds |
Dev template — web/index.html #
Keep Flutter's default placeholders so flutter run works unchanged:
<!DOCTYPE html>
<html>
<head>
<base href="$FLUTTER_BASE_HREF" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#25D366" />
<link rel="manifest" href="manifest.json" />
<link rel="icon" type="image/png" href="favicon.png" />
<title>My App | Development</title>
</head>
<body>
<!-- Loading progress bridge called by Dart JS interop -->
<script>
window.updateLoadingProgress = function (progress, text) {};
window.removeLoadingIndicator = function () {};
</script>
<!-- Flutter's default bootstrap -->
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
Flutter populates flutter_bootstrap.js on build; during flutter run it is served from memory.
Prod template — web/index.prod.html #
Replace Flutter's loader with the bootstrap.js tag shown in index.html Setup. This file is not touched by flutter build — it is a static asset copied into build/web/ next to the dev index.html.
Important. Keep an explicit
<base href="/" />inweb/index.prod.htmlfor the default root deployment flow. Do not leave$FLUTTER_BASE_HREFin the prod template. If you change the base href for a subpath deployment, it must still end with/(for example,/my-app/).
CI step — swap templates before generating #
In your CI pipeline, after flutter build web, delete the dev index.html and rename index.prod.html to index.html, then run the generator:
# 1. Build with Flutter (uses web/index.html with default placeholders)
flutter build web --release --wasm --base-href=/ -o build/web
# 2. Swap: prod template replaces dev template inside build/web/
rm -f build/web/index.html
mv build/web/index.prod.html build/web/index.html
# 3. Generate sw.js + bootstrap.js, inject {{sw_version}} into index.html,
# and auto-remove flutter_bootstrap.js / flutter_service_worker.js / flutter.js
# (flutter.js is inlined into bootstrap.js, so it's dropped from the output).
dart pub global activate sw ^0.1.2
dart pub global run sw:generate --version="$(git rev-parse --short=8 HEAD)"
After this sequence, build/web/ contains exactly the artifacts that should ship: production index.html, bootstrap.js, sw.js, and hashed assets. The dev template stays in source but never leaves the build directory.
Tip. Passing
--version="$(git rev-parse --short=8 HEAD)"ties each release to a commit SHA, which makes cache versioning deterministic and easy to trace across environments.
Command Line Options #
| Option | Short | Description | Default |
|---|---|---|---|
--help |
-h |
Show help information | — |
--input |
-i |
Path to Flutter build directory | build/web |
--output |
-o |
Output service worker filename | sw.js |
--bootstrap-output |
Output bootstrap filename | bootstrap.js |
|
--prefix |
-p |
Cache name prefix | app-cache |
--version |
-v |
Cache version | current timestamp |
--glob |
-g |
Include glob patterns (; separated) |
** |
--no-glob |
-e |
Exclude glob patterns (; separated) |
— |
--core |
Core category glob overrides | — | |
--required |
Required category glob overrides | — | |
--optional |
Optional category glob overrides | — | |
--ignore |
Ignore category glob overrides | — | |
--keep-maps |
Keep .js.map files |
false |
|
--no-cleanup |
Skip Flutter file cleanup | false |
|
--config |
Path to YAML config file | sw.yaml |
|
--theme |
Loading widget theme | auto |
|
--logo |
Loading widget logo path | — | |
--title |
Loading widget title | — | |
--color |
Loading widget accent color | #25D366 |
|
--min-progress |
Minimum progress value | 0 |
|
--max-progress |
Maximum progress value | 90 |
YAML Configuration #
Create an sw.yaml file in your project root as an alternative to CLI args:
input: build/web
output: sw.js
prefix: my-app
theme: dark
logo: icons/Icon-192.png
title: My App
color: "#25D366"
glob: "**.{html,js,wasm,json}; assets/**; canvaskit/**; icons/**"
no-glob: "sw.js; bootstrap.js; **/*.map; assets/NOTICES"
Priority: CLI arguments > YAML config > environment variables > defaults.
Environment Variables #
| Variable | Maps to |
|---|---|
SW_INPUT |
--input |
SW_OUTPUT |
--output |
SW_PREFIX |
--prefix |
SW_VERSION |
--version |
SW_THEME |
--theme |
SW_LOGO |
--logo |
SW_TITLE |
--title |
SW_COLOR |
--color |
Bootstrap Pipeline #
The bootstrap replaces Flutter's initialization with a controlled 6-stage pipeline:
| Stage | Progress | Description |
|---|---|---|
| Init | 0% → 1% | Environment check, browser capability detection |
| Service Worker | 1% → 2% | Register sw.js, unregister old Flutter SW, timeout fallback |
| CanvasKit | 2% → 20% | Load from Google CDN, fall back to local canvaskit/ |
| Assets | 20% → 80% | Load main.dart.js/.wasm via Flutter's loader |
| Dart Entry | 80% → 90% | Initialize Flutter engine and run app |
| Dart Init | 90% → 100% | Dart application manages remaining progress |
Loading Widget #
The loading widget provides visual feedback during initialization:
- Circular SVG progress ring with configurable accent color
- Logo image and title text (configurable)
- Status text showing current operation
- Percentage display (optional)
- Stall detection — after 30 seconds without progress, shows a "Reset Cache" button
- Error display — shows error message with reset option on failure
- Responsive design — adapts to mobile screens
- Theme support — light, dark, or auto (follows
prefers-color-scheme)
CanvasKit Loading #
The bootstrap automatically determines the correct CanvasKit variant:
- Extracts
engineRevisionfrom Flutter's build config - Detects browser capabilities (ImageDecoder, Intl.Segmenter, WebGL, WasmGC)
- Selects the appropriate variant (canvaskit, chromium, skwasm, skwasm_heavy, wimp)
- Tries Google CDN:
https://www.gstatic.com/flutter-canvaskit/{engineRevision}/{variant}.js - Falls back to local
canvaskit/directory on failure
Global API (window.Bootstrap) #
The bootstrap exposes a global API for Dart integration via JS interop:
// In your Dart code:
import 'dart:js_interop';
@JS('Bootstrap.dispose')
external void bootstrapDispose();
@JS('Bootstrap.progress')
external JSObject get bootstrapProgress;
| Method | Description |
|---|---|
Bootstrap.dispose() |
Remove loading widget, clean up listeners |
Bootstrap.progress |
Current state: { phase, percent, message } |
Bootstrap.subscribe(callback) |
Subscribe to progress changes, returns unsubscribe function |
Bootstrap.onUpdateAvailable(handler) |
Runs when a newer Service Worker has installed and is waiting; notifications stay active for the whole page session, even after the loading widget is gone. |
Bootstrap.applyUpdate(reload = true) |
Activates the waiting Service Worker and reloads the page into the updated build. |
Alternatively, the loading widget auto-disposes on the flutter-first-frame event.
Resource Categories #
Files are automatically categorized based on their path and size:
| Category | Pre-cached | On Fetch | Description |
|---|---|---|---|
| Core | Install | Cache-first | Essential: canvaskit variant, main.dart.js/.wasm/.mjs, *.support.wasm |
| Required | Install | Cache-first | Early-needed: AssetManifest*.json, FontManifest.json |
| Optional | — | Cache on first fetch | Fonts (.ttf, .otf, .woff, .woff2, .eot) at any size; .json, .webp, .png, .jpg, .jpeg, .svg, .gif, .ico under 512KB |
| Ignore | — | Pass-through | Not cached: *.map, *.symbols, NOTICES, large assets |
Override categorization with glob patterns via CLI or YAML config:
dart run sw:generate \
--input=build/web \
--core="assets/critical/**" \
--ignore="assets/video/**"
Never Cached #
These files are always fetched fresh (never stored in the SW cache):
bootstrap.js— must reflect latest build configindex.html— must be fresh for updatessw.js— browser handles SW updates natively
The same three files also require Cache-Control: no-cache at the HTTP layer — see Server Configuration.
Service Worker #
Caching Strategies #
| Resource | Strategy | Details |
|---|---|---|
index.html (/) |
Network-first | Fresh from network, cache fallback for offline |
| Core + Required | Pre-cached | Cached during SW install with cache-busted URLs |
| Optional | Lazy cache | Cached on first fetch for repeat visits |
| Ignore | Pass-through | Not cached, always from network |
Cache Management #
- Atomic updates — New resources cached in temp cache, then swapped atomically on activate
- Manifest diff — Previous manifest compared to detect changed resources (by MD5 hash)
- Stale cleanup — Old versioned caches automatically deleted on activate
- Error recovery — On activate error, all caches cleared (clean slate),
clients.claim()always called
Resilience #
- Fetch timeout: 10s per request via AbortController
- Fetch retry: 3 attempts with exponential backoff (1s → 2s → 4s + jitter)
- SW registration timeout: 4s, continues without SW on timeout
- Error recovery: Always calls
self.clients.claim()even on errors
Client Notifications #
The service worker sends sw-progress messages during resource operations:
{
type: 'sw-progress',
timestamp: 1749123456789,
resourcesSize: 5242880,
resourceName: 'main.dart.js',
resourceUrl: 'https://example.com/main.dart.js',
resourceKey: 'main.dart.js',
resourceSize: 1048576,
loaded: 1048576,
status: 'completed' // 'loading' | 'completed' | 'updated' | 'cached' | 'error'
}
Message Commands #
| Message | Action |
|---|---|
'skipWaiting' |
Immediately activate a waiting service worker |
{ type: 'getVersion' } |
Respond with current SW version |
Architecture #
This is a TypeScript + Dart monorepo:
packages/sw/ # TypeScript source (Vite → minified IIFE)
├── src/sw/ # Service Worker modules
├── src/bootstrap/ # Bootstrap pipeline + loading widget
└── src/shared/ # Shared types, constants, utilities
lib/ # Dart CLI
├── src/assets/ # Compiled JS embedded as Dart string constants
├── src/ # Config, manifest, categorizer, injector, cleanup
└── sw.dart # Barrel export
bin/generate.dart # CLI entry point
Users install only the Dart package — no Node.js required. The TypeScript source is pre-compiled and shipped as Dart string constants.
Contributing #
Prerequisites #
- Dart SDK >= 3.11.0
- Node.js >= 18 (for TypeScript development only)
- Flutter >= 3.41.0 (for example app)
Setup #
# Clone the repository
git clone https://github.com/DoctorinaAI/service-worker-generator.git
cd service-worker-generator
# Install dependencies
dart pub get
npm install
# Build TypeScript
npm run build
# Run tests
dart test
npm test
Development Workflow #
# Watch TypeScript changes
npm run dev
# Build TypeScript + copy to Dart string constants
npm run build:all
# Verify committed assets are up to date
npm run verify
# Run the generator on the example app
dart run sw:generate --input=example/build/web --no-cleanup
# Format Dart code
dart format -l 80 lib/ bin/ test/
Project Structure #
| Directory | Purpose |
|---|---|
packages/sw/ |
TypeScript source for SW and Bootstrap |
lib/src/ |
Dart CLI modules |
bin/ |
CLI entry point |
scripts/ |
Build scripts (copy-assets, verify-assets) |
docs/ |
Architecture documentation |
example/ |
Flutter Web example app |
test/ |
Dart tests |
License #
This project is licensed under the MIT License. See the LICENSE file for details.