Build tool for localized, offline-first, search-engine-optimized progressive web applications.
.d.ts.npm install --save-dev @sovereignbase/pwa
# or
pnpm add --save-dev @sovereignbase/pwa
# or
yarn add --dev @sovereignbase/pwa
# or
bun add --dev @sovereignbase/pwa
import { pwaize } from '@sovereignbase/pwa'
await pwaize({
_headersFile: true,
alternateLanguages: ['fi'],
applicationName: 'Example',
assetsDir: './src/client/assets',
canonicalLanguage: 'en',
contentSecurityPolicy: {
'connect-src': ['https://api.stripe.com', 'https://maps.googleapis.com'],
'frame-src': ['https://checkout.stripe.com', 'https://*.js.stripe.com'],
'img-src': ['https://*.stripe.com'],
'script-src': ['https://js.stripe.com', 'https://maps.googleapis.com'],
},
defaultLanguage: 'en',
description: {
en: 'An offline-first example.',
fi: 'Offline-first-esimerkki.',
},
distribution: {
android: {},
build: 1,
id: 'com.example.pwa',
version: '1.0.0',
},
entrypoint: './src/client/index.ts',
i18nDir: './src/client/i18n',
icons: {
icon192: '/assets/icon-192.png',
icon512: '/assets/icon-512.png',
maskableIcon512: '/assets/icon-maskable-512.png',
},
openGraphLocale: { en: 'en_US', fi: 'fi_FI' },
origin: 'https://example.com',
outDir: './dist',
serviceWorker: {
bypass: ['/api/**', 'https://cdn.example.com/private/**'],
},
socialImage: {
alt: { en: 'Example', fi: 'Esimerkki' },
url: 'https://example.com/assets/social.png',
},
stylesheet: './src/client/style.css',
themeColor: '#000000',
title: { en: 'Example', fi: 'Esimerkki' },
twitter: { creator: '@example', site: '@example' },
})
pwaize(config)The package's only runtime export. It bundles, tree-shakes, repeatedly
minifies, and writes the complete site to <outDir>/web.
Values such as titles, descriptions, colors, icons, and metadata are declared
once. Localizable fields accept either one value or a language record. Lookup
order is the requested language, defaultLanguage, and finally the field's
empty or default value.
serviceWorker.bypass accepts regular expressions and glob strings. *
matches within one path segment, ** crosses path segments, and an absolute
pattern matches the complete URL.
TypeScript and JavaScript files under i18nDir are individually bundled,
tree-shaken, and minified to .js. Relative imports are bundled, declaration
files are ignored, and other files are copied.
// src/client/i18n/en.ts
export default {
description: 'Available offline.',
title: 'Example',
} as const
The application entrypoint can load the generated module dynamically:
const language = document.documentElement.lang
const content = (await import(`/i18n/${language}.js`)).default
document.querySelector('h1')!.textContent = content.title
The non-literal import remains dynamic. Generated language modules are included in the Service Worker precache.
Android support is opt-in. When distribution.android is absent, Bubblewrap is
not loaded and no Android files are generated. An empty object enables a
deterministic Trusted Web Activity project at <outDir>/android/project:
npm install --save-dev @bubblewrap/core
distribution: {
android: {
package: 'com.example.pwa',
sha256CertFingerprints: ['AA:BB:CC:...'],
},
build: 1,
id: 'com.example.pwa',
version: '1.0.0',
}
The generated default-language web manifest supplies the Android name, launch
URL, colors, display mode, and icons. Its public origin and icon URLs must be
reachable while Bubblewrap generates the project. Certificate fingerprints
also produce web/.well-known/assetlinks.json, which is included in the web
precache.
Set both android.androidSdkPath and android.jdkPath to additionally run the
Gradle release builds. Project generation, compilation, and signing are
separate steps. Without a keystore, the results are
app-release-unsigned.apk and app-release-unsigned.aab. To sign both files,
set android.keystore, optionally set android.keyAlias, and provide
ANDROID_KEYSTORE_PASSWORD and ANDROID_KEY_PASSWORD in the build environment.
Passwords are never accepted in config or written to output. With Play App
Signing, this keystore should contain the replaceable upload key; Google keeps
the long-lived app signing key.
web/
├── ServiceWorker
├── index.html
├── manifest.webmanifest
├── en/index.html
├── en/manifest.webmanifest
├── fi/index.html
├── fi/manifest.webmanifest
├── assets/...
├── i18n/*.js
└── .sovereignbase/pwa/pwaize-build-id.txt
android/ # only with distribution.android
├── project/ # generated Bubblewrap project
├── app-release-unsigned.apk # with Android SDK and JDK
└── app-release-unsigned.aab
The root installer and manifest use defaultLanguage. Each language also gets
an indexed installer and manifest. Installers contain only the Service Worker
loader and are never precached. The Service Worker renders the application
document with its localized SEO metadata, bundled stylesheet, and bundled
entrypoint.
hreflang, JSON-LD, Open Graph, and Twitter metadata._headers keeps the Service Worker and build ID uncached and gives
the assets directory a one-year immutable browser cache.Generated installers and Service Worker application responses include a
Content-Security-Policy. script-src and style-src contain SHA-256 hashes
calculated from the final inline content. They also contain 'unsafe-inline'
as a CSP1 fallback; browsers implementing CSP2 or newer ignore that fallback
when a hash source is present. Modern browsers additionally receive
strict-dynamic, blocked inline script and style attributes, and required
Trusted Types for script sinks.
Use contentSecurityPolicy to append source expressions to generated source
directives. Existing defaults and generated inline hashes are retained, and
duplicate sources are removed. A configured source also replaces a conflicting
'none' default for that directive. For example:
contentSecurityPolicy: {
'connect-src': ['https://api.example.com'],
'frame-src': ['https://checkout.example.com', 'https://*.example.com'],
'img-src': ['https://*.example.com'],
'script-src': ['https://js.example.com'],
}
These additions apply to both generated installers and application documents
served by the Service Worker. Source expressions are written in CSP syntax;
quote keywords such as 'self' and 'report-sample'. script-src additions
are also appended to script-src-elem, so configured external scripts are not
blocked by the generated element-specific directive.
The policy also restricts base URLs, framing, forms, objects, workers, fonts,
media, images, and network connections. Generated _headers adds strong HSTS,
same-origin COOP and CORP, X-Frame-Options: DENY,
X-Content-Type-Options: nosniff, and a strict-origin referrer policy. The same
document security headers are attached to application responses generated by
the Service Worker.
Apache-2.0