├── .github ├── pull_request_template.md └── workflows │ ├── cr-comment.yml │ ├── cr.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.config.ts ├── eslint.config.js ├── examples ├── sveltekit-ts-assets-generator │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── adapter.mjs │ ├── client-test │ │ ├── offline.spec.ts │ │ └── sw.spec.ts │ ├── package.json │ ├── playwright.config.ts │ ├── pnpm-workspace.yaml │ ├── pwa-assets.config.ts │ ├── pwa.mjs │ ├── src │ │ ├── app.css │ │ ├── app.d.ts │ │ ├── app.html │ │ ├── hooks-servers.ts │ │ ├── lib │ │ │ ├── Counter.svelte │ │ │ ├── ReloadPrompt.svelte │ │ │ └── header │ │ │ │ ├── Header.svelte │ │ │ │ └── svelte-logo.svg │ │ ├── prompt-sw.ts │ │ └── routes │ │ │ ├── +layout.svelte │ │ │ ├── +layout.ts │ │ │ ├── +page.svelte │ │ │ ├── +page.ts │ │ │ └── about │ │ │ ├── +page.svelte │ │ │ └── +page.ts │ ├── static │ │ ├── favicon.svg │ │ ├── robots.txt │ │ ├── svelte-welcome.png │ │ └── svelte-welcome.webp │ ├── svelte.config.js │ ├── test │ │ └── build.test.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.mts └── sveltekit-ts │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── adapter.mjs │ ├── client-test │ ├── offline.spec.ts │ └── sw.spec.ts │ ├── package.json │ ├── playwright.config.ts │ ├── pnpm-workspace.yaml │ ├── pwa.mjs │ ├── src │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── hooks-servers.ts │ ├── lib │ │ ├── Counter.svelte │ │ ├── ReloadPrompt.svelte │ │ └── header │ │ │ ├── Header.svelte │ │ │ └── svelte-logo.svg │ ├── prompt-sw.ts │ └── routes │ │ ├── +layout.svelte │ │ ├── +layout.ts │ │ ├── +page.svelte │ │ ├── +page.ts │ │ └── about │ │ ├── +page.svelte │ │ └── +page.ts │ ├── static │ ├── favicon.ico │ ├── favicon.svg │ ├── pwa-192x192.png │ ├── pwa-512x512.png │ ├── robots.txt │ ├── svelte-welcome.png │ └── svelte-welcome.webp │ ├── svelte.config.js │ ├── test │ └── build.test.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.mts ├── hero.png ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── config.ts ├── index.ts ├── plugins │ ├── SvelteKitPlugin.ts │ └── log.ts └── types.ts └── tsconfig.json /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | 5 | 16 | 17 | ### Linked Issues 18 | 19 | 20 | 21 | ### Additional Context 22 | 23 | 24 | 25 | --- 26 | 27 | > [!TIP] 28 | > The author of this PR can publish a _preview release_ by commenting `/publish` below. 29 | -------------------------------------------------------------------------------- /.github/workflows/cr-comment.yml: -------------------------------------------------------------------------------- 1 | name: Add continuous release label 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | permissions: 8 | pull-requests: write 9 | 10 | jobs: 11 | label: 12 | if: ${{ github.event.issue.pull_request && (github.event.comment.user.id == github.event.issue.user.id || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'COLLABORATOR') && startsWith(github.event.comment.body, '/publish') }} 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - run: gh issue edit ${{ github.event.issue.number }} --add-label cr-tracked --repo ${{ github.repository }} 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.CR_PAT }} 19 | -------------------------------------------------------------------------------- /.github/workflows/cr.yml: -------------------------------------------------------------------------------- 1 | name: CR 2 | 3 | env: 4 | PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' 5 | 6 | on: 7 | pull_request: 8 | branches: [main] 9 | types: [opened, synchronize, labeled, ready_for_review] 10 | 11 | permissions: {} 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.event.number }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | release: 19 | if: ${{ !github.event.pull_request.draft && contains(github.event.pull_request.labels.*.name, 'cr-tracked') }} 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: pnpm/action-setup@v4.0.0 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: 20 28 | registry-url: https://registry.npmjs.org/ 29 | cache: pnpm 30 | - run: pnpm install 31 | - run: pnpm build 32 | - run: pnpx pkg-pr-new publish --compact --no-template --pnpm 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - uses: actions/setup-node@v3 20 | with: 21 | node-version: 20.17.0 22 | 23 | - run: npx changelogithub 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dev-dist 5 | # intellij stuff 6 | .idea/ 7 | /test-results/ 8 | /playwright-report/ 9 | /playwright/.cache/ 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | ignore-workspace-root-check=true 3 | shamefully-hoist=true 4 | strict-peer-dependencies=false 5 | auto-install-peers=true 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | Hi! We are really excited that you are interested in contributing to `@vite-pwa/sveltekit`. Before submitting your contribution, please make sure to take a moment and read through the following guide. 4 | 5 | Refer also to https://github.com/antfu/contribute. 6 | 7 | ## Set up your local development environment 8 | 9 | The `@vite-pwa/sveltekit` repo is a monorepo using pnpm workspaces. The package manager used to install and link dependencies must be [pnpm](https://pnpm.io/). 10 | 11 | To develop and test the `@vite-pwa/sveltekit` package: 12 | 13 | 1. Fork the `@vite-pwa/sveltekit` repository to your own GitHub account and then clone it to your local device. 14 | 15 | 2. Ensure using the latest Node.js (18.13+) 16 | 17 | 3. `@vite-pwa/sveltekit` uses pnpm v8. If you are working on multiple projects with different versions of pnpm, it's recommend to enable [Corepack](https://github.com/nodejs/corepack) by running `corepack enable`. 18 | 19 | 4. Check out a branch where you can work and commit your changes: 20 | ```shell 21 | git checkout -b my-new-branch 22 | ``` 23 | 24 | 5. Run `pnpm i` in `@vite-pwa/sveltekit`'s root folder 25 | 26 | 6. Run `pnpm run build` in `@vite-pwa/sveltekit`'s root folder. 27 | 28 | ## Testing changes 29 | 30 | To test your changes locally, change to `examples/sveltekit-ts` folder and run `pnpm run build- && pnpm run preview` where `` is the name of the example you want to test. 31 | 32 | ## Running tests 33 | 34 | Before running tests, you'll need to install [Playwright](https://playwright.dev/) Chromium browser: `pnpm playwright install chromium`. 35 | 36 | Run `pnpm run test` in `@vite-pwa/sveltekit`'s root folder or inside `examples/sveltekit-ts` folder after build `@vite-pwa/sveltekit`. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-PRESENT Anthony Fu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | @vite-pwa/sveltekit - Zero-config PWA for SvelteKit
3 | Zero-config PWA Plugin for SvelteKit 4 |

5 | 6 |

7 | 8 | NPM version 9 | 10 | 11 | NPM Downloads 12 | 13 | 14 | Docs & Guides 15 | 16 |
17 | 18 | GitHub stars 19 | 20 |

21 | 22 |
23 | 24 |

25 | 26 | 27 | 28 |

29 | 30 | ## 🚀 Features 31 | 32 | - 📖 [**Documentation & guides**](https://vite-pwa-org.netlify.app/) 33 | - 👌 **Zero-Config**: sensible built-in default configs for common use cases 34 | - 🔩 **Extensible**: expose the full ability to customize the behavior of the plugin 35 | - 🦾 **Type Strong**: written in [TypeScript](https://www.typescriptlang.org/) 36 | - 🔌 **Offline Support**: generate service worker with offline support (via Workbox) 37 | - ⚡ **Fully tree shakable**: auto inject Web App Manifest 38 | - 💬 **Prompt for new content**: built-in support for Vanilla JavaScript, Vue 3, React, Svelte, SolidJS and Preact 39 | - ⚙️ **Stale-while-revalidate**: automatic reload when new content is available 40 | - ✨ **Static assets handling**: configure static assets for offline support 41 | - 🐞 **Development Support**: debug your custom service worker logic as you develop your application 42 | - 🛠️ **Versatile**: integration with meta frameworks: [îles](https://github.com/ElMassimo/iles), [SvelteKit](https://github.com/sveltejs/kit), [VitePress](https://github.com/vuejs/vitepress), [Astro](https://github.com/withastro/astro), [Nuxt 3](https://github.com/nuxt/nuxt) and [Remix](https://github.com/remix-run/remix) 43 | - 💥 **PWA Assets Generator**: generate all the PWA assets from a single command and a single source image 44 | - 🚀 **PWA Assets Integration**: serving, generating and injecting PWA Assets on the fly in your application 45 | 46 | ## 📦 Install 47 | 48 | > From v0.3.0, `@vite-pwa/sveltekit` supports SvelteKit 2 (should also support SvelteKit 1). 49 | 50 | > From v0.2.0, `@vite-pwa/sveltekit` requires **SvelteKit 1.3.1 or above**. 51 | 52 | ```bash 53 | npm i @vite-pwa/sveltekit -D 54 | 55 | # yarn 56 | yarn add @vite-pwa/sveltekit -D 57 | 58 | # pnpm 59 | pnpm add @vite-pwa/sveltekit -D 60 | ``` 61 | 62 | ## 🦄 Usage 63 | 64 | Add `SvelteKitPWA` plugin to `vite.config.js / vite.config.ts` and configure it: 65 | 66 | ```ts 67 | // vite.config.js / vite.config.ts 68 | import { sveltekit } from '@sveltejs/kit/vite' 69 | import { SvelteKitPWA } from '@vite-pwa/sveltekit' 70 | 71 | export default { 72 | plugins: [ 73 | sveltekit(), 74 | SvelteKitPWA() 75 | ] 76 | } 77 | ``` 78 | 79 | Read the [📖 documentation](https://vite-pwa-org.netlify.app/frameworks/sveltekit) for a complete guide on how to configure and use 80 | this plugin. 81 | 82 | ## 👀 Full config 83 | 84 | Check out the type declaration [src/types.ts](./src/types.ts) and the following links for more details. 85 | 86 | - [Web app manifests](https://developer.mozilla.org/en-US/docs/Web/Manifest) 87 | - [Workbox](https://developers.google.com/web/tools/workbox) 88 | 89 | ## 📄 License 90 | 91 | [MIT](./LICENSE) License © 2022-PRESENT [Anthony Fu](https://github.com/antfu) 92 | -------------------------------------------------------------------------------- /build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | entries: [ 5 | 'src/index', 6 | ], 7 | clean: true, 8 | declaration: true, 9 | externals: [ 10 | 'tinyglobby', 11 | 'vite', 12 | 'vite-plugin-pwa', 13 | 'workbox-build', 14 | ], 15 | rollup: { 16 | emitCJS: false, 17 | dts: { 18 | respectExternal: true, 19 | }, 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default await antfu( 4 | { 5 | svelte: true, 6 | ignores: [ 7 | '**/build/**', 8 | '**/dist/**', 9 | '**/dev-dist/**', 10 | '**/node_modules/**', 11 | '**/*.svelte', 12 | ], 13 | }, 14 | ) 15 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | /dev-dist 15 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:svelte/recommended', 8 | ], 9 | globals: { 10 | __DATE__: 'readonly', 11 | __RELOAD_SW__: 'readonly', 12 | }, 13 | plugins: [ 14 | '@typescript-eslint', 15 | ], 16 | ignorePatterns: ['*.cjs'], 17 | overrides: [ 18 | { 19 | files: ['*.svelte'], 20 | parser: 'svelte-eslint-parser', 21 | parserOptions: { 22 | parser: '@typescript-eslint/parser', 23 | }, 24 | }, 25 | ], 26 | parserOptions: { 27 | sourceType: 'module', 28 | ecmaVersion: 2020, 29 | extraFileExtensions: ['.svelte'], 30 | }, 31 | rules: { 32 | 'no-shadow': ['error'], 33 | '@typescript-eslint/no-explicit-any': 'error', 34 | '@typescript-eslint/no-non-null-assertion': 'error', 35 | '@typescript-eslint/no-unused-vars': [ 36 | // prevent variables with a _ prefix from being marked as unused 37 | 'warn', 38 | { 39 | argsIgnorePattern: '^_', 40 | }, 41 | ], 42 | }, 43 | env: { 44 | browser: true, 45 | es2017: true, 46 | node: true, 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | .vercel 10 | .output 11 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/.npmrc: -------------------------------------------------------------------------------- 1 | shell-emulator=true 2 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/README.md: -------------------------------------------------------------------------------- 1 | # sv 2 | 3 | Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npx sv create 12 | 13 | # create a new project in my-app 14 | npx sv create my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/adapter.mjs: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import AdapterNode from '@sveltejs/adapter-node' 3 | import AdpaterStatic from '@sveltejs/adapter-static' 4 | 5 | export const nodeAdapter = process.env.NODE_ADAPTER === 'true' 6 | 7 | export const adapter = nodeAdapter ? AdapterNode() : AdpaterStatic() 8 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/client-test/offline.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | // eslint-disable-next-line ts/ban-ts-comment 3 | // @ts-ignore 4 | import { generateSW } from '../pwa.mjs' 5 | 6 | test('Test offline and trailing slashes', async ({ browser }) => { 7 | // test offline + trailing slashes routes 8 | const context = await browser.newContext() 9 | const offlinePage = await context.newPage() 10 | await offlinePage.goto('/') 11 | const offlineSwURL = await offlinePage.evaluate(async () => { 12 | const registration = await Promise.race([ 13 | // eslint-disable-next-line ts/ban-ts-comment 14 | // @ts-ignore 15 | navigator.serviceWorker.ready, 16 | new Promise((_, reject) => setTimeout(() => reject(new Error('Service worker registration failed: time out')), 10000)), 17 | ]) 18 | // @ts-expect-error registration is of type unknown 19 | return registration.active?.scriptURL 20 | }) 21 | const offlineSwName = generateSW ? 'sw.js' : 'prompt-sw.js' 22 | expect(offlineSwURL).toBe(`http://localhost:4173/${offlineSwName}`) 23 | await context.setOffline(true) 24 | const aboutAnchor = offlinePage.getByRole('link', { name: 'About' }) 25 | expect(await aboutAnchor.getAttribute('href')).toBe('/about') 26 | await aboutAnchor.click({ noWaitAfter: false }) 27 | const url = await offlinePage.evaluate(async () => { 28 | await new Promise(resolve => setTimeout(resolve, 3000)) 29 | return location.href 30 | }) 31 | expect(url).toBe('http://localhost:4173/about') 32 | expect(offlinePage.locator('li[aria-current="page"] a').getByText('About')).toBeTruthy() 33 | await offlinePage.reload({ waitUntil: 'load' }) 34 | expect(offlinePage.url()).toBe('http://localhost:4173/about') 35 | expect(offlinePage.locator('li[aria-current="page"] a').getByText('About')).toBeTruthy() 36 | // Dispose context once it's no longer needed. 37 | await context.close() 38 | }) 39 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/client-test/sw.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | // eslint-disable-next-line ts/ban-ts-comment 3 | // @ts-ignore 4 | import { generateSW } from '../pwa.mjs' 5 | 6 | test('The service worker is registered and cache storage is present', async ({ page }) => { 7 | await page.goto('/') 8 | 9 | const swURL = await page.evaluate(async () => { 10 | const registration = await Promise.race([ 11 | // eslint-disable-next-line ts/ban-ts-comment 12 | // @ts-ignore 13 | navigator.serviceWorker.ready, 14 | new Promise((_, reject) => setTimeout(() => reject(new Error('Service worker registration failed: time out')), 10000)), 15 | ]) 16 | // @ts-expect-error registration is of type unknown 17 | return registration.active?.scriptURL 18 | }) 19 | const swName = generateSW ? 'sw.js' : 'prompt-sw.js' 20 | expect(swURL).toBe(`http://localhost:4173/${swName}`) 21 | 22 | const cacheContents = await page.evaluate(async () => { 23 | const cacheState: Record> = {} 24 | for (const cacheName of await caches.keys()) { 25 | const cache = await caches.open(cacheName) 26 | cacheState[cacheName] = (await cache.keys()).map(req => req.url) 27 | } 28 | return cacheState 29 | }) 30 | 31 | expect(Object.keys(cacheContents).length).toEqual(1) 32 | 33 | const key = 'workbox-precache-v2-http://localhost:4173/' 34 | 35 | expect(Object.keys(cacheContents)[0]).toEqual(key) 36 | 37 | const urls = cacheContents[key].map(url => url.slice('http://localhost:4173/'.length)) 38 | 39 | /* 40 | 'http://localhost:4173/about?__WB_REVISION__=38251751d310c9b683a1426c22c135a2', 41 | 'http://localhost:4173/?__WB_REVISION__=073370aa3804305a787b01180cd6b8aa', 42 | 'http://localhost:4173/manifest.webmanifest?__WB_REVISION__=27df2fa4f35d014b42361148a2207da3' 43 | */ 44 | expect(urls.some(url => url.startsWith('manifest.webmanifest?__WB_REVISION__='))).toEqual(true) 45 | expect(urls.some(url => url.startsWith('?__WB_REVISION__='))).toEqual(true) 46 | expect(urls.some(url => url.startsWith('about?__WB_REVISION__='))).toEqual(true) 47 | // dontCacheBustURLsMatching: any asset in _app/immutable folder shouldn't have a revision (?__WB_REVISION__=) 48 | expect(urls.some(url => url.startsWith('_app/immutable/') && url.endsWith('.css'))).toEqual(true) 49 | expect(urls.some(url => url.startsWith('_app/immutable/') && url.endsWith('.js'))).toEqual(true) 50 | expect(urls.some(url => url.includes('_app/version.json?__WB_REVISION__='))).toEqual(true) 51 | }) 52 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltekit-ts-assets-generator", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "sw-dev": "vite dev", 7 | "dev": "vite dev", 8 | "dev-generate": "GENERATE_SW=true vite dev", 9 | "dev-generate-suppress-w": "GENERATE_SW=true SUPPRESS_WARNING=true vite dev", 10 | "build-generate-sw": "GENERATE_SW=true vite build", 11 | "build-generate-sw-node": "NODE_ADAPTER=true GENERATE_SW=true vite build", 12 | "build-inject-manifest": "vite build", 13 | "build-inject-manifest-node": "NODE_ADAPTER=true vite build", 14 | "build-self-destroying": "SELF_DESTROYING_SW=true vite build", 15 | "preview": "vite preview --port=4173", 16 | "preview-node": "PORT=4173 node build", 17 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 18 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 19 | "lint": "eslint .", 20 | "lint-fix": "nr lint --fix", 21 | "test-generate-sw": "nr build-generate-sw && GENERATE_SW=true vitest run && GENERATE_SW=true playwright test", 22 | "test-generate-sw-node": "nr build-generate-sw-node && NODE_ADAPTER=true GENERATE_SW=true vitest run && NODE_ADAPTER=true GENERATE_SW=true playwright test", 23 | "test-inject-manifest": "nr build-inject-manifest && vitest run && playwright test", 24 | "test-inject-manifest-node": "nr build-inject-manifest-node && NODE_ADAPTER=true vitest run && NODE_ADAPTER=true playwright test", 25 | "test": "nr test-generate-sw && nr test-generate-sw-node && nr test-inject-manifest && nr test-inject-manifest-node" 26 | }, 27 | "dependencies": { 28 | "@fontsource/fira-mono": "^5.0.8" 29 | }, 30 | "devDependencies": { 31 | "@playwright/test": "^1.40.0", 32 | "@sveltejs/adapter-node": "^2.0.0", 33 | "@sveltejs/adapter-static": "^3.0.0", 34 | "@sveltejs/kit": "^2.7.4", 35 | "@types/cookie": "^0.6.0", 36 | "@typescript-eslint/eslint-plugin": "^6.14.0", 37 | "@typescript-eslint/parser": "^6.14.0", 38 | "@vite-pwa/sveltekit": "workspace:*", 39 | "eslint": "^9.23.0", 40 | "eslint-plugin-svelte": "^3.3.3", 41 | "svelte": "^5.1.9", 42 | "svelte-check": "^4.0.5", 43 | "tslib": "^2.8.1", 44 | "typescript": "^5.6.3", 45 | "vitest": "^2.1.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { defineConfig, devices } from '@playwright/test' 3 | 4 | // eslint-disable-next-line ts/ban-ts-comment 5 | // @ts-ignore 6 | import { nodeAdapter } from './adapter.mjs' 7 | 8 | const url = 'http://localhost:4173' 9 | 10 | /** 11 | * Read environment variables from file. 12 | * https://github.com/motdotla/dotenv 13 | */ 14 | // require('dotenv').config(); 15 | 16 | /** 17 | * See https://playwright.dev/docs/test-configuration. 18 | */ 19 | export default defineConfig({ 20 | testDir: './client-test', 21 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */ 22 | outputDir: 'test-results/', 23 | timeout: 5 * 1000, 24 | expect: { 25 | /** 26 | * Maximum time expect() should wait for the condition to be met. 27 | * For example in `await expect(locator).toHaveText();` 28 | */ 29 | timeout: 1000, 30 | }, 31 | /* Run tests in files in parallel */ 32 | fullyParallel: true, 33 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 34 | forbidOnly: !!process.env.CI, 35 | /* Retry on CI only */ 36 | retries: 0, 37 | /* Opt out of parallel tests on CI. */ 38 | workers: process.env.CI ? 1 : undefined, 39 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 40 | reporter: 'line', 41 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 42 | use: { 43 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 44 | actionTimeout: 0, 45 | /* Base URL to use in actions like `await page.goto('/')`. */ 46 | baseURL: url, 47 | // offline: true, 48 | 49 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 50 | trace: 'on-first-retry', 51 | }, 52 | 53 | /* Configure projects for major browsers */ 54 | projects: [ 55 | { 56 | name: 'chromium', 57 | use: { ...devices['Desktop Chrome'] }, 58 | }, 59 | 60 | // { 61 | // name: 'firefox', 62 | // use: { ...devices['Desktop Firefox'] }, 63 | // }, 64 | 65 | // { 66 | // name: 'webkit', 67 | // use: { ...devices['Desktop Safari'] }, 68 | // }, 69 | 70 | /* Test against mobile viewports. */ 71 | // { 72 | // name: 'Mobile Chrome', 73 | // use: { ...devices['Pixel 5'] }, 74 | // }, 75 | // { 76 | // name: 'Mobile Safari', 77 | // use: { ...devices['iPhone 12'] }, 78 | // }, 79 | 80 | /* Test against branded browsers. */ 81 | // { 82 | // name: 'Microsoft Edge', 83 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 84 | // }, 85 | // { 86 | // name: 'Google Chrome', 87 | // use: { ..devices['Desktop Chrome'], channel: 'chrome' }, 88 | // }, 89 | ], 90 | 91 | /* Run your local dev server before starting the tests */ 92 | webServer: { 93 | command: nodeAdapter ? 'pnpm run preview-node' : 'pnpm run preview', 94 | url, 95 | reuseExistingServer: !process.env.CI, 96 | }, 97 | }) 98 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/pnpm-workspace.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/sveltekit/de3bd8e20458e77409c0786996a2defd82e39530/examples/sveltekit-ts-assets-generator/pnpm-workspace.yaml -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/pwa-assets.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createAppleSplashScreens, 3 | defineConfig, 4 | minimal2023Preset, 5 | } from '@vite-pwa/assets-generator/config' 6 | 7 | export default defineConfig({ 8 | headLinkOptions: { 9 | preset: '2023', 10 | }, 11 | preset: { 12 | ...minimal2023Preset, 13 | appleSplashScreens: createAppleSplashScreens({ 14 | padding: 0.3, 15 | resizeOptions: { fit: 'contain', background: 'white' }, 16 | darkResizeOptions: { fit: 'contain', background: 'black' }, 17 | linkMediaOptions: { 18 | log: true, 19 | addMediaScreen: true, 20 | xhtml: true, 21 | }, 22 | }, ['iPad Air 9.7"']), 23 | }, 24 | images: 'static/favicon.svg', 25 | }) 26 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/pwa.mjs: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | export const generateSW = process.env.GENERATE_SW === 'true' 4 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/app.css: -------------------------------------------------------------------------------- 1 | @import '@fontsource/fira-mono'; 2 | 3 | :root { 4 | font-family: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 5 | Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 6 | --font-mono: 'Fira Mono', monospace; 7 | --pure-white: #ffffff; 8 | --primary-color: #b9c6d2; 9 | --secondary-color: #d0dde9; 10 | --tertiary-color: #edf0f8; 11 | --accent-color: #ff3e00; 12 | --heading-color: rgba(0, 0, 0, 0.7); 13 | --text-color: #444444; 14 | --background-without-opacity: rgba(255, 255, 255, 0.7); 15 | --column-width: 42rem; 16 | --column-margin-top: 4rem; 17 | } 18 | 19 | body { 20 | min-height: 100vh; 21 | margin: 0; 22 | background-color: var(--primary-color); 23 | background: linear-gradient( 24 | 180deg, 25 | var(--primary-color) 0%, 26 | var(--secondary-color) 10.45%, 27 | var(--tertiary-color) 41.35% 28 | ); 29 | } 30 | 31 | body::before { 32 | content: ''; 33 | width: 80vw; 34 | height: 100vh; 35 | position: absolute; 36 | top: 0; 37 | left: 10vw; 38 | z-index: -1; 39 | background: radial-gradient( 40 | 50% 50% at 50% 50%, 41 | var(--pure-white) 0%, 42 | rgba(255, 255, 255, 0) 100% 43 | ); 44 | opacity: 0.05; 45 | } 46 | 47 | #svelte { 48 | min-height: 100vh; 49 | display: flex; 50 | flex-direction: column; 51 | } 52 | 53 | h1, 54 | h2, 55 | p { 56 | font-weight: 400; 57 | color: var(--heading-color); 58 | } 59 | 60 | p { 61 | line-height: 1.5; 62 | } 63 | 64 | a { 65 | color: var(--accent-color); 66 | text-decoration: none; 67 | } 68 | 69 | a:hover { 70 | text-decoration: underline; 71 | } 72 | 73 | h1 { 74 | font-size: 2rem; 75 | text-align: center; 76 | } 77 | 78 | h2 { 79 | font-size: 1rem; 80 | } 81 | 82 | pre { 83 | font-size: 16px; 84 | font-family: var(--font-mono); 85 | background-color: rgba(255, 255, 255, 0.45); 86 | border-radius: 3px; 87 | box-shadow: 2px 2px 6px rgb(255 255 255 / 25%); 88 | padding: 0.5em; 89 | overflow-x: auto; 90 | color: var(--text-color); 91 | } 92 | 93 | input, 94 | button { 95 | font-size: inherit; 96 | font-family: inherit; 97 | } 98 | 99 | button:focus:not(:focus-visible) { 100 | outline: none; 101 | } 102 | 103 | @media (min-width: 720px) { 104 | h1 { 105 | font-size: 2.4rem; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/app.d.ts: -------------------------------------------------------------------------------- 1 | import 'vite-plugin-pwa/svelte' 2 | import 'vite-plugin-pwa/info' 3 | import 'vite-plugin-pwa/pwa-assets' 4 | 5 | // See https://kit.svelte.dev/docs/types#app 6 | // for information about these interfaces 7 | // and what to do when importing types 8 | declare global { 9 | declare const __DATE__: string 10 | declare const __RELOAD_SW__: boolean 11 | namespace App { 12 | interface Locals { 13 | userid: string 14 | buildDate: string 15 | periodicUpdates: boolean 16 | } 17 | 18 | // interface PageData {} 19 | 20 | // interface Platform {} 21 | } 22 | } 23 | 24 | export {} 25 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %sveltekit.head% 7 | 8 | 9 |
%sveltekit.body%
10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/hooks-servers.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit' 2 | 3 | export const handle: Handle = async ({ event, resolve }) => { 4 | let userid = event.cookies.get('userid') 5 | 6 | if (!userid) { 7 | // if this is the first time the user has visited this app, 8 | // set a cookie so that we recognise them when they return 9 | userid = crypto.randomUUID() 10 | event.cookies.set('userid', userid, { path: '/' }) 11 | } 12 | 13 | event.locals.userid = userid 14 | 15 | return resolve(event) 16 | } 17 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/lib/Counter.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | 26 | 27 |
28 |
29 | 30 | {Math.floor($displayedCount)} 31 |
32 |
33 | 34 | 39 |
40 | 41 | 107 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/lib/ReloadPrompt.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 | {#if toast} 45 | 66 | {/if} 67 | 68 |
69 | {buildDate} 70 |
71 | 72 | 100 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/lib/header/Header.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
11 | 12 | SvelteKit 13 | 14 |
15 | 16 | 32 | 33 |
34 | {buildDate} 35 |
36 |
37 | 38 | 132 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/lib/header/svelte-logo.svg: -------------------------------------------------------------------------------- 1 | svelte-logo -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/prompt-sw.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 6 | import { NavigationRoute, registerRoute } from 'workbox-routing' 7 | 8 | declare let self: ServiceWorkerGlobalScope 9 | 10 | self.addEventListener('message', (event) => { 11 | if (event.data && event.data.type === 'SKIP_WAITING') 12 | self.skipWaiting() 13 | }) 14 | 15 | // self.__WB_MANIFEST is default injection point 16 | precacheAndRoute(self.__WB_MANIFEST) 17 | 18 | // clean old assets 19 | cleanupOutdatedCaches() 20 | 21 | let allowlist: undefined | RegExp[] 22 | if (import.meta.env.DEV) 23 | allowlist = [/^\/$/] 24 | 25 | // to allow work offline 26 | registerRoute(new NavigationRoute( 27 | createHandlerBoundToURL('/'), 28 | { allowlist }, 29 | )) 30 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | {#if pwaAssetsHead.themeColor} 19 | 20 | {/if} 21 | {#each pwaAssetsHead.links as link} 22 | 23 | {/each} 24 | 25 | {@html webManifest} 26 | 27 | 28 |
29 | 30 |
31 | {@render children?.()} 32 |
33 | 34 | 37 | 38 | {#await import('$lib/ReloadPrompt.svelte') then { default: ReloadPrompt }} 39 | 40 | {/await} 41 | 42 | 72 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true 2 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | Home 7 | 8 | 9 | 10 |
11 |

12 | 13 | 14 | 15 | Welcome 16 | 17 | 18 | 19 | to your new
SvelteKit app 20 |

21 | 22 |

23 | try editing src/routes/+page.svelte 24 |

25 | 26 | 27 |
28 | 29 | 58 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/routes/+page.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true 2 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/routes/about/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | About 3 | 4 | 5 | 6 |
7 |

About this app

8 | 9 |

10 | This is a SvelteKit app. You can make your own by typing the 11 | following into your command line and following the prompts: 12 |

13 | 14 |
npm create svelte@latest
15 | 16 |

17 | The page you're looking at is purely static HTML, with no client-side interactivity needed. 18 | Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening 19 | the devtools network panel and reloading. 20 |

21 | 22 |
23 | 24 | 31 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/src/routes/about/+page.ts: -------------------------------------------------------------------------------- 1 | import { dev } from '$app/environment' 2 | 3 | // we don't need any JS on this page, though we'll load 4 | // it in dev so that we get hot module replacement... 5 | export const csr = dev 6 | 7 | // since there's no dynamic data here, we can prerender 8 | // it so that it gets served as a static asset in prod 9 | export const prerender = true 10 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 52 | 56 | 58 | 65 | 68 | 72 | 73 | 80 | 83 | 87 | 91 | 92 | 93 | 97 | 101 | 104 | 112 | 120 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/static/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/static/svelte-welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/sveltekit/de3bd8e20458e77409c0786996a2defd82e39530/examples/sveltekit-ts-assets-generator/static/svelte-welcome.png -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/static/svelte-welcome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/sveltekit/de3bd8e20458e77409c0786996a2defd82e39530/examples/sveltekit-ts-assets-generator/static/svelte-welcome.webp -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | import { adapter } from './adapter.mjs' 3 | // you don't need to do this if you're using generateSW strategy in your app 4 | import { generateSW } from './pwa.mjs' 5 | 6 | /** @type {import('@sveltejs/kit').Config} */ 7 | const config = { 8 | // Consult https://github.com/sveltejs/svelte-preprocess 9 | // for more information about preprocessors 10 | preprocess: vitePreprocess(), 11 | 12 | kit: { 13 | adapter, 14 | serviceWorker: { 15 | register: false, 16 | }, 17 | files: { 18 | // you don't need to do this if you're using generateSW strategy in your app 19 | serviceWorker: generateSW ? undefined : 'src/prompt-sw.ts', 20 | }, 21 | }, 22 | } 23 | 24 | export default config 25 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/test/build.test.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from 'node:fs' 2 | import { describe, expect, it } from 'vitest' 3 | // eslint-disable-next-line ts/ban-ts-comment 4 | // @ts-ignore 5 | import { nodeAdapter } from '../adapter.mjs' 6 | // eslint-disable-next-line ts/ban-ts-comment 7 | // @ts-ignore 8 | import { generateSW } from '../pwa.mjs' 9 | 10 | describe(`test-build: ${nodeAdapter ? 'node' : 'static'} adapter`, () => { 11 | it(`service worker is generated: ${generateSW ? 'sw.js' : 'prompt-sw.js'}`, () => { 12 | const swName = `./build/${nodeAdapter ? 'client/' : ''}${generateSW ? 'sw.js' : 'prompt-sw.js'}` 13 | expect(existsSync(swName), `${swName} doesn't exist`).toBeTruthy() 14 | const webManifest = `./build/${nodeAdapter ? 'client/' : ''}manifest.webmanifest` 15 | expect(existsSync(webManifest), `${webManifest} doesn't exist`).toBeTruthy() 16 | const swContent = readFileSync(swName, 'utf-8') 17 | let match: RegExpMatchArray | null 18 | if (generateSW) { 19 | match = swContent.match(/define\(\['\.\/(workbox-\w+)'/) 20 | expect(match && match.length === 2, `workbox-***.js entry not found in ${swName}`).toBeTruthy() 21 | const workboxName = `./build/${nodeAdapter ? 'client/' : ''}${match?.[1]}.js` 22 | expect(existsSync(workboxName), `${workboxName} doesn't exist`).toBeTruthy() 23 | } 24 | match = swContent.match(/"url":\s*"manifest\.webmanifest"/) 25 | expect(match && match.length === 1, 'missing manifest.webmanifest in sw precache manifest').toBeTruthy() 26 | match = swContent.match(/"url":\s*"\/"/) 27 | expect(match && match.length === 1, 'missing entry point route (/) in sw precache manifest').toBeTruthy() 28 | match = swContent.match(/"url":\s*"about"/) 29 | expect(match && match.length === 1, 'missing about route (/about) in sw precache manifest').toBeTruthy() 30 | if (nodeAdapter) { 31 | match = swContent.match(/"url":\s*"server\//) 32 | expect(match === null, 'found server/ entries in sw precache manifest').toBeTruthy() 33 | } 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "bundler", 5 | "resolveJsonModule": true, 6 | "allowJs": true, 7 | "checkJs": true, 8 | "strict": true, 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "skipLibCheck": true 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // 16 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 17 | // from the referenced tsconfig.json - TypeScript does not merge them in 18 | } 19 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/vite.config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from 'vite' 2 | import process from 'node:process' 3 | import { sveltekit } from '@sveltejs/kit/vite' 4 | import { SvelteKitPWA } from '@vite-pwa/sveltekit' 5 | // you don't need to do this if you're using generateSW strategy in your app 6 | import { generateSW } from './pwa.mjs' 7 | 8 | const config: UserConfig = { 9 | // WARN: this will not be necessary on your project 10 | logLevel: 'info', 11 | // WARN: this will not be necessary on your project 12 | build: { 13 | minify: false, 14 | }, 15 | // WARN: this will not be necessary on your project 16 | define: { 17 | '__DATE__': `'${new Date().toISOString()}'`, 18 | '__RELOAD_SW__': false, 19 | 'process.env.NODE_ENV': process.env.NODE_ENV === 'production' ? '"production"' : '"development"', 20 | }, 21 | // WARN: this will not be necessary on your project 22 | server: { 23 | fs: { 24 | // Allow serving files from hoisted root node_modules 25 | allow: ['../..'], 26 | }, 27 | }, 28 | plugins: [ 29 | sveltekit(), 30 | SvelteKitPWA({ 31 | srcDir: './src', 32 | mode: 'development', 33 | // you don't need to do this if you're using generateSW strategy in your app 34 | strategies: generateSW ? 'generateSW' : 'injectManifest', 35 | // you don't need to do this if you're using generateSW strategy in your app 36 | filename: generateSW ? undefined : 'prompt-sw.ts', 37 | scope: '/', 38 | base: '/', 39 | selfDestroying: process.env.SELF_DESTROYING_SW === 'true', 40 | pwaAssets: { 41 | config: true, 42 | }, 43 | manifest: { 44 | short_name: 'SvelteKit PWA', 45 | name: 'SvelteKit PWA', 46 | start_url: '/', 47 | scope: '/', 48 | display: 'standalone', 49 | theme_color: '#ffffff', 50 | background_color: '#ffffff', 51 | }, 52 | injectManifest: { 53 | globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2}'], 54 | }, 55 | workbox: { 56 | globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2}'], 57 | }, 58 | devOptions: { 59 | enabled: false, 60 | suppressWarnings: process.env.SUPPRESS_WARNING === 'true', 61 | type: 'module', 62 | navigateFallback: '/', 63 | }, 64 | // if you have shared info in svelte config file put in a separate module and use it also here 65 | kit: { 66 | includeVersionFile: true, 67 | }, 68 | }, 69 | ), 70 | ], 71 | } 72 | 73 | export default config 74 | -------------------------------------------------------------------------------- /examples/sveltekit-ts-assets-generator/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['test/*.test.ts'], 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | /dev-dist 15 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:svelte/recommended', 8 | ], 9 | globals: { 10 | __DATE__: 'readonly', 11 | __RELOAD_SW__: 'readonly', 12 | }, 13 | plugins: [ 14 | '@typescript-eslint', 15 | ], 16 | ignorePatterns: ['*.cjs'], 17 | overrides: [ 18 | { 19 | files: ['*.svelte'], 20 | parser: 'svelte-eslint-parser', 21 | parserOptions: { 22 | parser: '@typescript-eslint/parser', 23 | }, 24 | }, 25 | ], 26 | parserOptions: { 27 | sourceType: 'module', 28 | ecmaVersion: 2020, 29 | extraFileExtensions: ['.svelte'], 30 | }, 31 | rules: { 32 | 'no-shadow': ['error'], 33 | '@typescript-eslint/no-explicit-any': 'error', 34 | '@typescript-eslint/no-non-null-assertion': 'error', 35 | '@typescript-eslint/no-unused-vars': [ 36 | // prevent variables with a _ prefix from being marked as unused 37 | 'warn', 38 | { 39 | argsIgnorePattern: '^_', 40 | }, 41 | ], 42 | }, 43 | env: { 44 | browser: true, 45 | es2017: true, 46 | node: true, 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | .vercel 10 | .output 11 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/.npmrc: -------------------------------------------------------------------------------- 1 | shell-emulator=true 2 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/README.md: -------------------------------------------------------------------------------- 1 | # sv 2 | 3 | Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npx sv create 12 | 13 | # create a new project in my-app 14 | npx sv create my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/adapter.mjs: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import AdapterNode from '@sveltejs/adapter-node' 3 | import AdpaterStatic from '@sveltejs/adapter-static' 4 | 5 | export const nodeAdapter = process.env.NODE_ADAPTER === 'true' 6 | 7 | export const adapter = nodeAdapter ? AdapterNode() : AdpaterStatic() 8 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/client-test/offline.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | // eslint-disable-next-line ts/ban-ts-comment 3 | // @ts-ignore 4 | import { generateSW } from '../pwa.mjs' 5 | 6 | test('Test offline and trailing slashes', async ({ browser }) => { 7 | // test offline + trailing slashes routes 8 | const context = await browser.newContext() 9 | const offlinePage = await context.newPage() 10 | await offlinePage.goto('/') 11 | const offlineSwURL = await offlinePage.evaluate(async () => { 12 | const registration = await Promise.race([ 13 | // eslint-disable-next-line ts/ban-ts-comment 14 | // @ts-ignore 15 | navigator.serviceWorker.ready, 16 | new Promise((_, reject) => setTimeout(() => reject(new Error('Service worker registration failed: time out')), 10000)), 17 | ]) 18 | // @ts-expect-error registration is of type unknown 19 | return registration.active?.scriptURL 20 | }) 21 | const offlineSwName = generateSW ? 'sw.js' : 'prompt-sw.js' 22 | expect(offlineSwURL).toBe(`http://localhost:4173/${offlineSwName}`) 23 | await context.setOffline(true) 24 | const aboutAnchor = offlinePage.getByRole('link', { name: 'About' }) 25 | expect(await aboutAnchor.getAttribute('href')).toBe('/about') 26 | await aboutAnchor.click({ noWaitAfter: false }) 27 | const url = await offlinePage.evaluate(async () => { 28 | await new Promise(resolve => setTimeout(resolve, 3000)) 29 | return location.href 30 | }) 31 | expect(url).toBe('http://localhost:4173/about') 32 | expect(offlinePage.locator('li[aria-current="page"] a').getByText('About')).toBeTruthy() 33 | await offlinePage.reload({ waitUntil: 'load' }) 34 | expect(offlinePage.url()).toBe('http://localhost:4173/about') 35 | expect(offlinePage.locator('li[aria-current="page"] a').getByText('About')).toBeTruthy() 36 | // Dispose context once it's no longer needed. 37 | await context.close() 38 | }) 39 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/client-test/sw.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | // eslint-disable-next-line ts/ban-ts-comment 3 | // @ts-ignore 4 | import { generateSW } from '../pwa.mjs' 5 | 6 | test('The service worker is registered and cache storage is present', async ({ page }) => { 7 | await page.goto('/') 8 | 9 | const swURL = await page.evaluate(async () => { 10 | const registration = await Promise.race([ 11 | // eslint-disable-next-line ts/ban-ts-comment 12 | // @ts-ignore 13 | navigator.serviceWorker.ready, 14 | new Promise((_, reject) => setTimeout(() => reject(new Error('Service worker registration failed: time out')), 10000)), 15 | ]) 16 | // @ts-expect-error registration is of type unknown 17 | return registration.active?.scriptURL 18 | }) 19 | const swName = generateSW ? 'sw.js' : 'prompt-sw.js' 20 | expect(swURL).toBe(`http://localhost:4173/${swName}`) 21 | 22 | const cacheContents = await page.evaluate(async () => { 23 | const cacheState: Record> = {} 24 | for (const cacheName of await caches.keys()) { 25 | const cache = await caches.open(cacheName) 26 | cacheState[cacheName] = (await cache.keys()).map(req => req.url) 27 | } 28 | return cacheState 29 | }) 30 | 31 | expect(Object.keys(cacheContents).length).toEqual(1) 32 | 33 | const key = 'workbox-precache-v2-http://localhost:4173/' 34 | 35 | expect(Object.keys(cacheContents)[0]).toEqual(key) 36 | 37 | const urls = cacheContents[key].map(url => url.slice('http://localhost:4173/'.length)) 38 | 39 | /* 40 | 'http://localhost:4173/about?__WB_REVISION__=38251751d310c9b683a1426c22c135a2', 41 | 'http://localhost:4173/?__WB_REVISION__=073370aa3804305a787b01180cd6b8aa', 42 | 'http://localhost:4173/manifest.webmanifest?__WB_REVISION__=27df2fa4f35d014b42361148a2207da3' 43 | */ 44 | expect(urls.some(url => url.startsWith('manifest.webmanifest?__WB_REVISION__='))).toEqual(true) 45 | expect(urls.some(url => url.startsWith('?__WB_REVISION__='))).toEqual(true) 46 | expect(urls.some(url => url.startsWith('about?__WB_REVISION__='))).toEqual(true) 47 | // dontCacheBustURLsMatching: any asset in _app/immutable folder shouldn't have a revision (?__WB_REVISION__=) 48 | expect(urls.some(url => url.startsWith('_app/immutable/') && url.endsWith('.css'))).toEqual(true) 49 | expect(urls.some(url => url.startsWith('_app/immutable/') && url.endsWith('.js'))).toEqual(true) 50 | expect(urls.some(url => url.includes('_app/version.json?__WB_REVISION__='))).toEqual(true) 51 | }) 52 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltekit-ts", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "scripts": { 6 | "sw-dev": "vite dev", 7 | "dev": "vite dev", 8 | "dev-generate": "GENERATE_SW=true vite dev", 9 | "dev-generate-suppress-w": "GENERATE_SW=true SUPPRESS_WARNING=true vite dev", 10 | "build-generate-sw": "GENERATE_SW=true vite build", 11 | "build-generate-sw-node": "NODE_ADAPTER=true GENERATE_SW=true vite build", 12 | "build-inject-manifest": "vite build", 13 | "build-inject-manifest-node": "NODE_ADAPTER=true vite build", 14 | "build-self-destroying": "SELF_DESTROYING_SW=true vite build", 15 | "preview": "vite preview --port=4173", 16 | "preview-node": "PORT=4173 node build", 17 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 18 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 19 | "lint": "eslint .", 20 | "lint-fix": "nr lint --fix", 21 | "test-generate-sw": "nr build-generate-sw && GENERATE_SW=true vitest run && GENERATE_SW=true playwright test", 22 | "test-generate-sw-node": "nr build-generate-sw-node && NODE_ADAPTER=true GENERATE_SW=true vitest run && NODE_ADAPTER=true GENERATE_SW=true playwright test", 23 | "test-inject-manifest": "nr build-inject-manifest && vitest run && playwright test", 24 | "test-inject-manifest-node": "nr build-inject-manifest-node && NODE_ADAPTER=true vitest run && NODE_ADAPTER=true playwright test", 25 | "test": "nr test-generate-sw && nr test-generate-sw-node && nr test-inject-manifest && nr test-inject-manifest-node" 26 | }, 27 | "dependencies": { 28 | "@fontsource/fira-mono": "^5.0.8" 29 | }, 30 | "devDependencies": { 31 | "@playwright/test": "^1.40.0", 32 | "@sveltejs/adapter-node": "^2.0.0", 33 | "@sveltejs/adapter-static": "^3.0.0", 34 | "@sveltejs/kit": "^2.7.4", 35 | "@types/cookie": "^0.6.0", 36 | "@typescript-eslint/eslint-plugin": "^6.14.0", 37 | "@typescript-eslint/parser": "^6.14.0", 38 | "@vite-pwa/sveltekit": "workspace:*", 39 | "eslint": "^9.23.0", 40 | "eslint-plugin-svelte": "^3.3.3", 41 | "svelte": "^5.1.9", 42 | "svelte-check": "^4.0.5", 43 | "tslib": "^2.8.1", 44 | "typescript": "^5.6.3", 45 | "vitest": "^2.1.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { defineConfig, devices } from '@playwright/test' 3 | 4 | // eslint-disable-next-line ts/ban-ts-comment 5 | // @ts-ignore 6 | import { nodeAdapter } from './adapter.mjs' 7 | 8 | const url = 'http://localhost:4173' 9 | 10 | /** 11 | * Read environment variables from file. 12 | * https://github.com/motdotla/dotenv 13 | */ 14 | // require('dotenv').config(); 15 | 16 | /** 17 | * See https://playwright.dev/docs/test-configuration. 18 | */ 19 | export default defineConfig({ 20 | testDir: './client-test', 21 | /* Folder for test artifacts such as screenshots, videos, traces, etc. */ 22 | outputDir: 'test-results/', 23 | timeout: 5 * 1000, 24 | expect: { 25 | /** 26 | * Maximum time expect() should wait for the condition to be met. 27 | * For example in `await expect(locator).toHaveText();` 28 | */ 29 | timeout: 1000, 30 | }, 31 | /* Run tests in files in parallel */ 32 | fullyParallel: true, 33 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 34 | forbidOnly: !!process.env.CI, 35 | /* Retry on CI only */ 36 | retries: 0, 37 | /* Opt out of parallel tests on CI. */ 38 | workers: process.env.CI ? 1 : undefined, 39 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 40 | reporter: 'line', 41 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 42 | use: { 43 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 44 | actionTimeout: 0, 45 | /* Base URL to use in actions like `await page.goto('/')`. */ 46 | baseURL: url, 47 | // offline: true, 48 | 49 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 50 | trace: 'on-first-retry', 51 | }, 52 | 53 | /* Configure projects for major browsers */ 54 | projects: [ 55 | { 56 | name: 'chromium', 57 | use: { ...devices['Desktop Chrome'] }, 58 | }, 59 | 60 | // { 61 | // name: 'firefox', 62 | // use: { ...devices['Desktop Firefox'] }, 63 | // }, 64 | 65 | // { 66 | // name: 'webkit', 67 | // use: { ...devices['Desktop Safari'] }, 68 | // }, 69 | 70 | /* Test against mobile viewports. */ 71 | // { 72 | // name: 'Mobile Chrome', 73 | // use: { ...devices['Pixel 5'] }, 74 | // }, 75 | // { 76 | // name: 'Mobile Safari', 77 | // use: { ...devices['iPhone 12'] }, 78 | // }, 79 | 80 | /* Test against branded browsers. */ 81 | // { 82 | // name: 'Microsoft Edge', 83 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 84 | // }, 85 | // { 86 | // name: 'Google Chrome', 87 | // use: { ..devices['Desktop Chrome'], channel: 'chrome' }, 88 | // }, 89 | ], 90 | 91 | /* Run your local dev server before starting the tests */ 92 | webServer: { 93 | command: nodeAdapter ? 'pnpm run preview-node' : 'pnpm run preview', 94 | url, 95 | reuseExistingServer: !process.env.CI, 96 | }, 97 | }) 98 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/pnpm-workspace.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/sveltekit/de3bd8e20458e77409c0786996a2defd82e39530/examples/sveltekit-ts/pnpm-workspace.yaml -------------------------------------------------------------------------------- /examples/sveltekit-ts/pwa.mjs: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | 3 | export const generateSW = process.env.GENERATE_SW === 'true' 4 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/app.css: -------------------------------------------------------------------------------- 1 | @import '@fontsource/fira-mono'; 2 | 3 | :root { 4 | font-family: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 5 | Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 6 | --font-mono: 'Fira Mono', monospace; 7 | --pure-white: #ffffff; 8 | --primary-color: #b9c6d2; 9 | --secondary-color: #d0dde9; 10 | --tertiary-color: #edf0f8; 11 | --accent-color: #ff3e00; 12 | --heading-color: rgba(0, 0, 0, 0.7); 13 | --text-color: #444444; 14 | --background-without-opacity: rgba(255, 255, 255, 0.7); 15 | --column-width: 42rem; 16 | --column-margin-top: 4rem; 17 | } 18 | 19 | body { 20 | min-height: 100vh; 21 | margin: 0; 22 | background-color: var(--primary-color); 23 | background: linear-gradient( 24 | 180deg, 25 | var(--primary-color) 0%, 26 | var(--secondary-color) 10.45%, 27 | var(--tertiary-color) 41.35% 28 | ); 29 | } 30 | 31 | body::before { 32 | content: ''; 33 | width: 80vw; 34 | height: 100vh; 35 | position: absolute; 36 | top: 0; 37 | left: 10vw; 38 | z-index: -1; 39 | background: radial-gradient( 40 | 50% 50% at 50% 50%, 41 | var(--pure-white) 0%, 42 | rgba(255, 255, 255, 0) 100% 43 | ); 44 | opacity: 0.05; 45 | } 46 | 47 | #svelte { 48 | min-height: 100vh; 49 | display: flex; 50 | flex-direction: column; 51 | } 52 | 53 | h1, 54 | h2, 55 | p { 56 | font-weight: 400; 57 | color: var(--heading-color); 58 | } 59 | 60 | p { 61 | line-height: 1.5; 62 | } 63 | 64 | a { 65 | color: var(--accent-color); 66 | text-decoration: none; 67 | } 68 | 69 | a:hover { 70 | text-decoration: underline; 71 | } 72 | 73 | h1 { 74 | font-size: 2rem; 75 | text-align: center; 76 | } 77 | 78 | h2 { 79 | font-size: 1rem; 80 | } 81 | 82 | pre { 83 | font-size: 16px; 84 | font-family: var(--font-mono); 85 | background-color: rgba(255, 255, 255, 0.45); 86 | border-radius: 3px; 87 | box-shadow: 2px 2px 6px rgb(255 255 255 / 25%); 88 | padding: 0.5em; 89 | overflow-x: auto; 90 | color: var(--text-color); 91 | } 92 | 93 | input, 94 | button { 95 | font-size: inherit; 96 | font-family: inherit; 97 | } 98 | 99 | button:focus:not(:focus-visible) { 100 | outline: none; 101 | } 102 | 103 | @media (min-width: 720px) { 104 | h1 { 105 | font-size: 2.4rem; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/app.d.ts: -------------------------------------------------------------------------------- 1 | import 'vite-plugin-pwa/svelte' 2 | import 'vite-plugin-pwa/info' 3 | 4 | // See https://kit.svelte.dev/docs/types#app 5 | // for information about these interfaces 6 | // and what to do when importing types 7 | declare global { 8 | declare const __DATE__: string 9 | declare const __RELOAD_SW__: boolean 10 | namespace App { 11 | interface Locals { 12 | userid: string 13 | buildDate: string 14 | periodicUpdates: boolean 15 | } 16 | 17 | // interface PageData {} 18 | 19 | // interface Platform {} 20 | } 21 | } 22 | 23 | export {} 24 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | %sveltekit.head% 14 | 15 | 16 |
%sveltekit.body%
17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/hooks-servers.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit' 2 | 3 | export const handle: Handle = async ({ event, resolve }) => { 4 | let userid = event.cookies.get('userid') 5 | 6 | if (!userid) { 7 | // if this is the first time the user has visited this app, 8 | // set a cookie so that we recognise them when they return 9 | userid = crypto.randomUUID() 10 | event.cookies.set('userid', userid, { path: '/' }) 11 | } 12 | 13 | event.locals.userid = userid 14 | 15 | return resolve(event) 16 | } 17 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/lib/Counter.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | 26 | 27 |
28 |
29 | 30 | {Math.floor($displayedCount)} 31 |
32 |
33 | 34 | 39 |
40 | 41 | 107 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/lib/ReloadPrompt.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 | {#if toast} 45 | 66 | {/if} 67 | 68 |
69 | {buildDate} 70 |
71 | 72 | 100 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/lib/header/Header.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 |
11 | 12 | SvelteKit 13 | 14 |
15 | 16 | 32 | 33 |
34 | {buildDate} 35 |
36 |
37 | 38 | 132 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/lib/header/svelte-logo.svg: -------------------------------------------------------------------------------- 1 | svelte-logo -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/prompt-sw.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 6 | import { NavigationRoute, registerRoute } from 'workbox-routing' 7 | 8 | declare let self: ServiceWorkerGlobalScope 9 | 10 | self.addEventListener('message', (event) => { 11 | if (event.data && event.data.type === 'SKIP_WAITING') 12 | self.skipWaiting() 13 | }) 14 | 15 | // self.__WB_MANIFEST is default injection point 16 | precacheAndRoute(self.__WB_MANIFEST) 17 | 18 | // clean old assets 19 | cleanupOutdatedCaches() 20 | 21 | let allowlist: undefined | RegExp[] 22 | if (import.meta.env.DEV) 23 | allowlist = [/^\/$/] 24 | 25 | // to allow work offline 26 | registerRoute(new NavigationRoute( 27 | createHandlerBoundToURL('/'), 28 | { allowlist }, 29 | )) 30 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | {@html webManifest} 19 | 20 | 21 |
22 | 23 |
24 | {@render children?.()} 25 |
26 | 27 | 30 | 31 | {#await import('$lib/ReloadPrompt.svelte') then { default: ReloadPrompt }} 32 | 33 | {/await} 34 | 35 | 65 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true 2 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | Home 7 | 8 | 9 | 10 |
11 |

12 | 13 | 14 | 15 | Welcome 16 | 17 | 18 | 19 | to your new
SvelteKit app 20 |

21 | 22 |

23 | try editing src/routes/+page.svelte 24 |

25 | 26 | 27 |
28 | 29 | 58 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/routes/+page.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true 2 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/routes/about/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | About 3 | 4 | 5 | 6 |
7 |

About this app

8 | 9 |

10 | This is a SvelteKit app. You can make your own by typing the 11 | following into your command line and following the prompts: 12 |

13 | 14 |
npm create svelte@latest
15 | 16 |

17 | The page you're looking at is purely static HTML, with no client-side interactivity needed. 18 | Because of that, we don't need to load any JavaScript. Try viewing the page's source, or opening 19 | the devtools network panel and reloading. 20 |

21 | 22 |
23 | 24 | 31 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/src/routes/about/+page.ts: -------------------------------------------------------------------------------- 1 | import { dev } from '$app/environment' 2 | 3 | // we don't need any JS on this page, though we'll load 4 | // it in dev so that we get hot module replacement... 5 | export const csr = dev 6 | 7 | // since there's no dynamic data here, we can prerender 8 | // it so that it gets served as a static asset in prod 9 | export const prerender = true 10 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/sveltekit/de3bd8e20458e77409c0786996a2defd82e39530/examples/sveltekit-ts/static/favicon.ico -------------------------------------------------------------------------------- /examples/sveltekit-ts/static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 52 | 56 | 58 | 65 | 68 | 72 | 73 | 80 | 83 | 87 | 91 | 92 | 93 | 97 | 101 | 104 | 112 | 120 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/static/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/sveltekit/de3bd8e20458e77409c0786996a2defd82e39530/examples/sveltekit-ts/static/pwa-192x192.png -------------------------------------------------------------------------------- /examples/sveltekit-ts/static/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/sveltekit/de3bd8e20458e77409c0786996a2defd82e39530/examples/sveltekit-ts/static/pwa-512x512.png -------------------------------------------------------------------------------- /examples/sveltekit-ts/static/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/static/svelte-welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/sveltekit/de3bd8e20458e77409c0786996a2defd82e39530/examples/sveltekit-ts/static/svelte-welcome.png -------------------------------------------------------------------------------- /examples/sveltekit-ts/static/svelte-welcome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/sveltekit/de3bd8e20458e77409c0786996a2defd82e39530/examples/sveltekit-ts/static/svelte-welcome.webp -------------------------------------------------------------------------------- /examples/sveltekit-ts/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | import { adapter } from './adapter.mjs' 3 | // you don't need to do this if you're using generateSW strategy in your app 4 | import { generateSW } from './pwa.mjs' 5 | 6 | /** @type {import('@sveltejs/kit').Config} */ 7 | const config = { 8 | // Consult https://github.com/sveltejs/svelte-preprocess 9 | // for more information about preprocessors 10 | preprocess: vitePreprocess(), 11 | 12 | kit: { 13 | adapter, 14 | serviceWorker: { 15 | register: false, 16 | }, 17 | files: { 18 | // you don't need to do this if you're using generateSW strategy in your app 19 | serviceWorker: generateSW ? undefined : 'src/prompt-sw.ts', 20 | }, 21 | }, 22 | } 23 | 24 | export default config 25 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/test/build.test.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync } from 'node:fs' 2 | import { describe, expect, it } from 'vitest' 3 | // eslint-disable-next-line ts/ban-ts-comment 4 | // @ts-ignore 5 | import { nodeAdapter } from '../adapter.mjs' 6 | // eslint-disable-next-line ts/ban-ts-comment 7 | // @ts-ignore 8 | import { generateSW } from '../pwa.mjs' 9 | 10 | describe(`test-build: ${nodeAdapter ? 'node' : 'static'} adapter`, () => { 11 | it(`service worker is generated: ${generateSW ? 'sw.js' : 'prompt-sw.js'}`, () => { 12 | const swName = `./build/${nodeAdapter ? 'client/' : ''}${generateSW ? 'sw.js' : 'prompt-sw.js'}` 13 | expect(existsSync(swName), `${swName} doesn't exist`).toBeTruthy() 14 | const webManifest = `./build/${nodeAdapter ? 'client/' : ''}manifest.webmanifest` 15 | expect(existsSync(webManifest), `${webManifest} doesn't exist`).toBeTruthy() 16 | const swContent = readFileSync(swName, 'utf-8') 17 | let match: RegExpMatchArray | null 18 | if (generateSW) { 19 | match = swContent.match(/define\(\['\.\/(workbox-\w+)'/) 20 | expect(match && match.length === 2, `workbox-***.js entry not found in ${swName}`).toBeTruthy() 21 | const workboxName = `./build/${nodeAdapter ? 'client/' : ''}${match?.[1]}.js` 22 | expect(existsSync(workboxName), `${workboxName} doesn't exist`).toBeTruthy() 23 | } 24 | match = swContent.match(/"url":\s*"manifest\.webmanifest"/) 25 | expect(match && match.length === 1, 'missing manifest.webmanifest in sw precache manifest').toBeTruthy() 26 | match = swContent.match(/"url":\s*"\/"/) 27 | expect(match && match.length === 1, 'missing entry point route (/) in sw precache manifest').toBeTruthy() 28 | match = swContent.match(/"url":\s*"about"/) 29 | expect(match && match.length === 1, 'missing about route (/about) in sw precache manifest').toBeTruthy() 30 | if (nodeAdapter) { 31 | match = swContent.match(/"url":\s*"server\//) 32 | expect(match === null, 'found server/ entries in sw precache manifest').toBeTruthy() 33 | } 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "bundler", 5 | "resolveJsonModule": true, 6 | "allowJs": true, 7 | "checkJs": true, 8 | "strict": true, 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "skipLibCheck": true 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // 16 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 17 | // from the referenced tsconfig.json - TypeScript does not merge them in 18 | } 19 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/vite.config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from 'vite' 2 | import process from 'node:process' 3 | import { sveltekit } from '@sveltejs/kit/vite' 4 | import { SvelteKitPWA } from '@vite-pwa/sveltekit' 5 | // you don't need to do this if you're using generateSW strategy in your app 6 | import { generateSW } from './pwa.mjs' 7 | 8 | const config: UserConfig = { 9 | // WARN: this will not be necessary on your project 10 | logLevel: 'info', 11 | // WARN: this will not be necessary on your project 12 | build: { 13 | minify: false, 14 | }, 15 | // WARN: this will not be necessary on your project 16 | define: { 17 | '__DATE__': `'${new Date().toISOString()}'`, 18 | '__RELOAD_SW__': false, 19 | 'process.env.NODE_ENV': process.env.NODE_ENV === 'production' ? '"production"' : '"development"', 20 | }, 21 | // WARN: this will not be necessary on your project 22 | server: { 23 | fs: { 24 | // Allow serving files from hoisted root node_modules 25 | allow: ['../..'], 26 | }, 27 | }, 28 | plugins: [ 29 | sveltekit(), 30 | SvelteKitPWA({ 31 | srcDir: './src', 32 | mode: 'development', 33 | // you don't need to do this if you're using generateSW strategy in your app 34 | strategies: generateSW ? 'generateSW' : 'injectManifest', 35 | // you don't need to do this if you're using generateSW strategy in your app 36 | filename: generateSW ? undefined : 'prompt-sw.ts', 37 | scope: '/', 38 | base: '/', 39 | selfDestroying: process.env.SELF_DESTROYING_SW === 'true', 40 | manifest: { 41 | short_name: 'SvelteKit PWA', 42 | name: 'SvelteKit PWA', 43 | start_url: '/', 44 | scope: '/', 45 | display: 'standalone', 46 | theme_color: '#ffffff', 47 | background_color: '#ffffff', 48 | icons: [ 49 | { 50 | src: '/pwa-192x192.png', 51 | sizes: '192x192', 52 | type: 'image/png', 53 | }, 54 | { 55 | src: '/pwa-512x512.png', 56 | sizes: '512x512', 57 | type: 'image/png', 58 | }, 59 | { 60 | src: '/pwa-512x512.png', 61 | sizes: '512x512', 62 | type: 'image/png', 63 | purpose: 'any maskable', 64 | }, 65 | ], 66 | }, 67 | injectManifest: { 68 | globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2}'], 69 | }, 70 | workbox: { 71 | globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2}'], 72 | }, 73 | devOptions: { 74 | enabled: true, 75 | suppressWarnings: process.env.SUPPRESS_WARNING === 'true', 76 | type: 'module', 77 | navigateFallback: '/', 78 | }, 79 | // if you have shared info in svelte config file put in a separate module and use it also here 80 | kit: { 81 | includeVersionFile: true, 82 | }, 83 | }, 84 | ), 85 | ], 86 | } 87 | 88 | export default config 89 | -------------------------------------------------------------------------------- /examples/sveltekit-ts/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['test/*.test.ts'], 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/sveltekit/de3bd8e20458e77409c0786996a2defd82e39530/hero.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vite-pwa/sveltekit", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "packageManager": "pnpm@10.7.0", 6 | "description": "Zero-config PWA for SvelteKit", 7 | "author": "antfu ", 8 | "license": "MIT", 9 | "funding": "https://github.com/sponsors/antfu", 10 | "homepage": "https://github.com/vite-pwa/sveltekit#readme", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/vite-pwa/sveltekit.git" 14 | }, 15 | "bugs": "https://github.com/vite-pwa/sveltekit/issues", 16 | "keywords": [ 17 | "sveltekit", 18 | "workbox", 19 | "pwa", 20 | "vite", 21 | "vite-plugin" 22 | ], 23 | "exports": { 24 | ".": { 25 | "types": "./dist/index.d.ts", 26 | "default": "./dist/index.mjs" 27 | }, 28 | "./package.json": "./package.json" 29 | }, 30 | "types": "dist/index.d.ts", 31 | "typesVersions": { 32 | "*": { 33 | "*": [ 34 | "./dist/index.d.ts" 35 | ] 36 | } 37 | }, 38 | "files": [ 39 | "dist" 40 | ], 41 | "engines": { 42 | "node": ">=18.13" 43 | }, 44 | "scripts": { 45 | "build": "unbuild", 46 | "lint": "eslint .", 47 | "lint:fix": "nr lint --fix", 48 | "prepublishOnly": "npm run build", 49 | "release": "bumpp && npm publish", 50 | "test": "pnpm run -C examples/sveltekit-ts test && pnpm run -C examples/sveltekit-ts-assets-generator test" 51 | }, 52 | "peerDependencies": { 53 | "@sveltejs/kit": "^1.3.1 || ^2.0.1", 54 | "@vite-pwa/assets-generator": "^1.0.0" 55 | }, 56 | "peerDependenciesMeta": { 57 | "@vite-pwa/assets-generator": { 58 | "optional": true 59 | } 60 | }, 61 | "dependencies": { 62 | "kolorist": "^1.8.0", 63 | "tinyglobby": "^0.2.9", 64 | "vite-plugin-pwa": "^1.0.0" 65 | }, 66 | "devDependencies": { 67 | "@antfu/eslint-config": "^4.11.0", 68 | "@antfu/ni": "^0.23.2", 69 | "@types/debug": "^4.1.8", 70 | "@types/node": "^18.17.15", 71 | "@typescript-eslint/eslint-plugin": "^6.13.2", 72 | "bumpp": "^9.2.0", 73 | "eslint": "^9.23.0", 74 | "eslint-plugin-svelte": "^3.3.3", 75 | "typescript": "^5.7.2", 76 | "unbuild": "^3.2.0", 77 | "vite": "^5.0.10" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - examples/* 3 | ignoredBuiltDependencies: 4 | - '@sveltejs/kit' 5 | - esbuild 6 | onlyBuiltDependencies: 7 | - sharp 8 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import type { ResolvedConfig } from 'vite' 2 | import type { VitePWAOptions } from 'vite-plugin-pwa' 3 | import type { ManifestEntry, ManifestTransform } from 'workbox-build' 4 | import type { KitOptions } from './types' 5 | import { resolve } from 'node:path' 6 | 7 | export function configureSvelteKitOptions( 8 | kit: KitOptions, 9 | viteOptions: ResolvedConfig, 10 | options: Partial, 11 | ) { 12 | const { 13 | base = viteOptions.base ?? '/', 14 | adapterFallback, 15 | outDir = `${viteOptions.root}/.svelte-kit`, 16 | assets = 'static', 17 | } = kit 18 | 19 | // Vite will copy public folder to the globDirectory after pwa plugin runs: 20 | // globDirectory is the build folder. 21 | // SvelteKit will copy to the globDirectory before pwa plugin runs (via Vite client build in writeBundle hook): 22 | // globDirectory is the kit client output folder. 23 | // We need to disable includeManifestIcons: any icon in the static folder will be twice in the sw's precache manifest. 24 | if (typeof options.includeManifestIcons === 'undefined') 25 | options.includeManifestIcons = false 26 | 27 | let config: Partial< 28 | import('workbox-build').BasePartial 29 | & import('workbox-build').GlobPartial 30 | & import('workbox-build').RequiredGlobDirectoryPartial 31 | > 32 | 33 | if (options.strategies === 'injectManifest') { 34 | if (!options.srcDir) 35 | options.srcDir = 'src' 36 | 37 | if (!options.filename) 38 | options.filename = 'service-worker.js' 39 | 40 | options.injectManifest = options.injectManifest ?? {} 41 | config = options.injectManifest 42 | } 43 | else { 44 | options.workbox = options.workbox ?? {} 45 | // the user may want to disable offline support 46 | if (!('navigateFallback' in options.workbox)) 47 | options.workbox.navigateFallback = adapterFallback ?? base 48 | 49 | config = options.workbox 50 | } 51 | 52 | // SvelteKit outDir is `.svelte-kit/output/client`. 53 | // We need to include the parent folder since SvelteKit will generate SSG in `.svelte-kit/output/prerendered` folder. 54 | if (!config.globDirectory) 55 | config.globDirectory = `${outDir}/output` 56 | 57 | let buildAssetsDir = kit.appDir ?? '_app/' 58 | if (buildAssetsDir[0] === '/') 59 | buildAssetsDir = buildAssetsDir.slice(1) 60 | if (buildAssetsDir[buildAssetsDir.length - 1] !== '/') 61 | buildAssetsDir += '/' 62 | 63 | if (!config.modifyURLPrefix) { 64 | config.globPatterns = buildGlobPatterns(config.globPatterns) 65 | if (kit.includeVersionFile) 66 | config.globPatterns.push(`client/${buildAssetsDir}version.json`) 67 | } 68 | 69 | // exclude server assets: sw is built on SSR build 70 | config.globIgnores = buildGlobIgnores(config.globIgnores) 71 | 72 | // Vite 5 support: allow override dontCacheBustURLsMatching 73 | if (!('dontCacheBustURLsMatching' in config)) 74 | config.dontCacheBustURLsMatching = new RegExp(`${buildAssetsDir}immutable/`) 75 | 76 | if (!config.manifestTransforms) { 77 | config.manifestTransforms = [createManifestTransform( 78 | base, 79 | config.globDirectory, 80 | options.strategies === 'injectManifest' 81 | ? undefined 82 | : (options.manifestFilename ?? 'manifest.webmanifest'), 83 | kit, 84 | )] 85 | } 86 | 87 | if (options.pwaAssets) { 88 | options.pwaAssets.integration = { 89 | baseUrl: base, 90 | publicDir: resolve(viteOptions.root, assets), 91 | outDir: resolve(outDir, 'output/client'), 92 | } 93 | } 94 | } 95 | 96 | function createManifestTransform( 97 | base: string, 98 | outDir: string, 99 | webManifestName?: string, 100 | options?: KitOptions, 101 | ): ManifestTransform { 102 | return async (entries) => { 103 | const defaultAdapterFallback = 'prerendered/fallback.html' 104 | const suffix = options?.trailingSlash === 'always' ? '/' : '' 105 | let adapterFallback = options?.adapterFallback 106 | let excludeFallback = false 107 | // the fallback will be always generated by SvelteKit. 108 | // The adapter will copy the fallback only if it is provided in its options: we need to exclude it 109 | if (!adapterFallback) { 110 | adapterFallback = defaultAdapterFallback 111 | excludeFallback = true 112 | } 113 | 114 | // the fallback will be always in .svelte-kit/output/prerendered/fallback.html 115 | const manifest = entries 116 | .filter(({ url }) => !(excludeFallback && url === defaultAdapterFallback)) 117 | .map((e) => { 118 | let url = e.url 119 | // client assets in `.svelte-kit/output/client` folder. 120 | // SSG pages in `.svelte-kit/output/prerendered/pages` folder. 121 | // static adapter with load functions in `.svelte-kit/output/prerendered/dependencies//__data.json`. 122 | // fallback page in `.svelte-kit/output/prerendered` folder (fallback.html is the default). 123 | if (url.startsWith('client/')) 124 | url = url.slice(7) 125 | else if (url.startsWith('prerendered/dependencies/')) 126 | url = url.slice(25) 127 | else if (url.startsWith('prerendered/pages/')) 128 | url = url.slice(18) 129 | else if (url === defaultAdapterFallback) 130 | url = adapterFallback! 131 | 132 | if (url.endsWith('.html')) { 133 | if (url.startsWith('/')) 134 | url = url.slice(1) 135 | 136 | if (url === 'index.html') { 137 | url = base 138 | } 139 | else { 140 | const idx = url.lastIndexOf('/') 141 | if (idx > -1) { 142 | // abc/index.html -> abc/? 143 | if (url.endsWith('/index.html')) 144 | url = `${url.slice(0, idx)}${suffix}` 145 | // abc/def.html -> abc/def/? 146 | else 147 | url = `${url.substring(0, url.lastIndexOf('.'))}${suffix}` 148 | } 149 | else { 150 | // xxx.html -> xxx/? 151 | url = `${url.substring(0, url.lastIndexOf('.'))}${suffix}` 152 | } 153 | } 154 | } 155 | 156 | e.url = url 157 | 158 | return e 159 | }) 160 | 161 | if (options?.spa && options?.adapterFallback) { 162 | const name = typeof options.spa === 'object' && options.spa.fallbackMapping 163 | ? options.spa.fallbackMapping 164 | : options.adapterFallback 165 | if (typeof options.spa === 'object' && typeof options.spa.fallbackRevision === 'function') { 166 | manifest.push({ 167 | url: name, 168 | revision: await options.spa.fallbackRevision(), 169 | size: 0, 170 | }) 171 | } 172 | else { 173 | manifest.push(await buildManifestEntry( 174 | name, 175 | resolve(outDir, 'client/_app/version.json'), 176 | )) 177 | } 178 | } 179 | 180 | if (!webManifestName) 181 | return { manifest } 182 | 183 | return { manifest: manifest.filter(e => e.url !== webManifestName) } 184 | } 185 | } 186 | 187 | function buildGlobPatterns(globPatterns?: string[]) { 188 | if (globPatterns) { 189 | if (!globPatterns.some(g => g.startsWith('prerendered/'))) 190 | globPatterns.push('prerendered/**/*.{html,json}') 191 | 192 | if (!globPatterns.some(g => g.startsWith('client/'))) 193 | globPatterns.push('client/**/*.{js,css,ico,png,svg,webp,webmanifest}') 194 | 195 | if (!globPatterns.some(g => g.includes('webmanifest'))) 196 | globPatterns.push('client/*.webmanifest') 197 | 198 | return globPatterns 199 | } 200 | 201 | return ['client/**/*.{js,css,ico,png,svg,webp,webmanifest}', 'prerendered/**/*.{html,json}'] 202 | } 203 | 204 | function buildGlobIgnores(globIgnores?: string[]) { 205 | if (globIgnores) { 206 | if (!globIgnores.some(g => g.startsWith('server/'))) 207 | globIgnores.push('server/**') 208 | 209 | return globIgnores 210 | } 211 | 212 | return ['server/**'] 213 | } 214 | 215 | async function buildManifestEntry(url: string, path: string): Promise { 216 | const [crypto, createReadStream] = await Promise.all([ 217 | import('node:crypto').then(m => m.default), 218 | import('node:fs').then(m => m.createReadStream), 219 | ]) 220 | 221 | return new Promise((resolve, reject) => { 222 | const cHash = crypto.createHash('MD5') 223 | const stream = createReadStream(path) 224 | stream.on('error', (err) => { 225 | reject(err) 226 | }) 227 | stream.on('data', (chunk) => { 228 | // @ts-expect-error TS2345: Argument of type string | Buffer is not assignable to parameter of type BinaryLike 229 | cHash.update(chunk) 230 | }) 231 | stream.on('end', () => { 232 | return resolve({ 233 | url, 234 | size: 0, 235 | revision: `${cHash.digest('hex')}`, 236 | }) 237 | }) 238 | }) 239 | } 240 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | import type { VitePluginPWAAPI } from 'vite-plugin-pwa' 3 | import type { SvelteKitPWAOptions } from './types' 4 | import { VitePWA } from 'vite-plugin-pwa' 5 | import { configureSvelteKitOptions } from './config' 6 | import { SvelteKitPlugin } from './plugins/SvelteKitPlugin' 7 | 8 | export function SvelteKitPWA(userOptions: Partial = {}): Plugin[] { 9 | if (!userOptions.integration) 10 | userOptions.integration = {} 11 | 12 | userOptions.integration.closeBundleOrder = 'pre' 13 | userOptions.integration.configureOptions = ( 14 | viteConfig, 15 | options, 16 | ) => configureSvelteKitOptions( 17 | userOptions.kit ?? {}, 18 | viteConfig, 19 | options, 20 | ) 21 | 22 | const plugins = VitePWA(userOptions) 23 | 24 | const plugin = plugins.find(p => p && typeof p === 'object' && 'name' in p && p.name === 'vite-plugin-pwa') 25 | const resolveVitePluginPWAAPI = (): VitePluginPWAAPI | undefined => { 26 | return plugin?.api 27 | } 28 | 29 | return [ 30 | // remove the build plugin: we're using a custom one 31 | ...plugins.filter(p => p && typeof p === 'object' && 'name' in p && p.name !== 'vite-plugin-pwa:build'), 32 | SvelteKitPlugin(userOptions, resolveVitePluginPWAAPI), 33 | ] 34 | } 35 | 36 | export * from './types' 37 | -------------------------------------------------------------------------------- /src/plugins/SvelteKitPlugin.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, ResolvedConfig } from 'vite' 2 | import type { VitePluginPWAAPI, VitePWAOptions } from 'vite-plugin-pwa' 3 | import { lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises' 4 | import { join } from 'node:path' 5 | import { glob } from 'tinyglobby' 6 | 7 | export function SvelteKitPlugin( 8 | options: Partial, 9 | apiResolver: () => VitePluginPWAAPI | undefined, 10 | ) { 11 | let viteConfig: ResolvedConfig 12 | return { 13 | name: 'vite-plugin-pwa:sveltekit:build', 14 | apply: 'build', 15 | enforce: 'pre', 16 | configResolved(config) { 17 | viteConfig = config 18 | }, 19 | async generateBundle(_, bundle) { 20 | // generate only for client 21 | if (viteConfig.build.ssr) 22 | return 23 | 24 | const api = apiResolver() 25 | if (!api) 26 | return 27 | 28 | const assetsGenerator = await api.pwaAssetsGenerator() 29 | if (assetsGenerator) 30 | assetsGenerator.injectManifestIcons() 31 | 32 | api.generateBundle(bundle, this) 33 | }, 34 | writeBundle: { 35 | sequential: true, 36 | enforce: 'pre', 37 | async handler() { 38 | const api = apiResolver() 39 | if (!api || viteConfig.build.ssr) 40 | return 41 | 42 | const assetsGenerator = await api.pwaAssetsGenerator() 43 | if (assetsGenerator) 44 | await assetsGenerator.generate() 45 | }, 46 | }, 47 | closeBundle: { 48 | sequential: true, 49 | enforce: 'pre', 50 | async handler() { 51 | const api = apiResolver() 52 | 53 | if (api && !api.disabled && viteConfig.build.ssr) { 54 | const webManifest = options.manifestFilename ?? 'manifest.webmanifest' 55 | let swName = options.filename ?? 'sw.js' 56 | const outDir = options.outDir ?? `${viteConfig.root}/.svelte-kit/output` 57 | const clientOutputDir = join(outDir, 'client') 58 | await mkdir(clientOutputDir, { recursive: true }) 59 | if (!options.strategies || options.strategies === 'generateSW' || options.selfDestroying) { 60 | let path: string 61 | let existsFile: boolean 62 | 63 | // remove kit sw: we'll regenerate the sw 64 | if (options.selfDestroying && options.strategies === 'injectManifest') { 65 | if (swName.endsWith('.ts')) 66 | swName = swName.replace(/\.ts$/, '.js') 67 | 68 | path = join(clientOutputDir, 'service-worker.js').replace('\\/g', '/') 69 | existsFile = await isFile(path) 70 | if (existsFile) 71 | await rm(path) 72 | } 73 | 74 | // regenerate sw before adapter runs: we need to include generated html pages 75 | await api.generateSW() 76 | 77 | const serverOutputDir = join(outDir, 'server') 78 | path = join(serverOutputDir, swName).replace(/\\/g, '/') 79 | existsFile = await isFile(path) 80 | if (existsFile) { 81 | const sw = await readFile(path, 'utf-8') 82 | await writeFile( 83 | join(clientOutputDir, swName).replace('\\/g', '/'), 84 | sw, 85 | 'utf-8', 86 | ) 87 | await rm(path) 88 | } 89 | // move also workbox-*.js when using generateSW 90 | const result = await glob({ 91 | patterns: ['workbox-*.js'], 92 | cwd: serverOutputDir, 93 | onlyFiles: true, 94 | expandDirectories: false, 95 | }) 96 | if (result && result.length > 0) { 97 | path = join(serverOutputDir, result[0]).replace(/\\/g, '/') 98 | await writeFile( 99 | join(clientOutputDir, result[0]).replace('\\/g', '/'), 100 | await readFile(path, 'utf-8'), 101 | 'utf-8', 102 | ) 103 | await rm(path) 104 | } 105 | // remove also web manifest in server folder 106 | path = join(serverOutputDir, webManifest).replace(/\\/g, '/') 107 | existsFile = await isFile(path) 108 | if (existsFile) 109 | await rm(path) 110 | 111 | return 112 | } 113 | 114 | if (swName.endsWith('.ts')) 115 | swName = swName.replace(/\.ts$/, '.js') 116 | 117 | const injectionPoint = !options.injectManifest || !('injectionPoint' in options.injectManifest) || !!options.injectManifest.injectionPoint 118 | 119 | if (injectionPoint) { 120 | // kit fixes sw name to 'service-worker.js' 121 | const injectManifestOptions: import('workbox-build').InjectManifestOptions = { 122 | globDirectory: outDir.replace(/\\/g, '/'), 123 | ...options.injectManifest ?? {}, 124 | swSrc: join(clientOutputDir, 'service-worker.js').replace(/\\/g, '/'), 125 | swDest: join(clientOutputDir, 'service-worker.js').replace(/\\/g, '/'), 126 | } 127 | 128 | const [injectManifest, logWorkboxResult] = await Promise.all([ 129 | import('workbox-build').then(m => m.injectManifest), 130 | import('./log').then(m => m.logWorkboxResult), 131 | ]) 132 | 133 | // inject the manifest 134 | const buildResult = await injectManifest(injectManifestOptions) 135 | // log workbox result 136 | logWorkboxResult('injectManifest', viteConfig, buildResult) 137 | // rename the sw 138 | if (swName !== 'service-worker.js') { 139 | await rename( 140 | join(clientOutputDir, 'service-worker.js').replace('\\/g', '/'), 141 | join(clientOutputDir, swName).replace('\\/g', '/'), 142 | ) 143 | } 144 | } 145 | else { 146 | const { logWorkboxResult } = await import('./log') 147 | // log workbox result 148 | logWorkboxResult('injectManifest', viteConfig) 149 | if (swName !== 'service-worker.js') { 150 | await rename( 151 | join(clientOutputDir, 'service-worker.js').replace('\\/g', '/'), 152 | join(clientOutputDir, swName).replace('\\/g', '/'), 153 | ) 154 | } 155 | } 156 | } 157 | }, 158 | }, 159 | } 160 | } 161 | 162 | async function isFile(path: string) { 163 | try { 164 | const stats = await lstat(path) 165 | return stats.isFile() 166 | } 167 | catch { 168 | return false 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/plugins/log.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import type { ResolvedConfig } from 'vite' 3 | import type { BuildResult } from 'workbox-build' 4 | import { relative } from 'node:path' 5 | import { cyan, dim, green, magenta, yellow } from 'kolorist' 6 | import { version } from '../../package.json' 7 | 8 | export function logWorkboxResult(strategy: string, viteOptions: ResolvedConfig, buildResult?: BuildResult) { 9 | const { root, logLevel = 'info' } = viteOptions 10 | 11 | if (logLevel === 'silent') 12 | return 13 | 14 | if (!buildResult) { 15 | console.info([ 16 | '', 17 | `${cyan(`SvelteKit VitePWA v${version}`)}`, 18 | `mode ${magenta(strategy)}`, 19 | ].join('\n')) 20 | return 21 | } 22 | 23 | const { count, size, filePaths, warnings } = buildResult 24 | 25 | if (logLevel === 'info') { 26 | console.info([ 27 | '', 28 | `${cyan(`SvelteKit VitePWA v${version}`)}`, 29 | `mode ${magenta(strategy)}`, 30 | `precache ${green(`${count} entries`)} ${dim(`(${(size / 1024).toFixed(2)} KiB)`)}`, 31 | 'files generated', 32 | ...filePaths.map(p => ` ${dim(relative(root, p))}`), 33 | ].join('\n')) 34 | } 35 | 36 | // log build warning 37 | warnings && warnings.length > 0 && console.warn(yellow([ 38 | 'warnings', 39 | ...warnings.map(w => ` ${w}`), 40 | '', 41 | ].join('\n'))) 42 | } 43 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { VitePWAOptions } from 'vite-plugin-pwa' 2 | 3 | export interface KitOptions { 4 | /** 5 | * The base path for your application: by default will use the Vite base. 6 | * 7 | * @deprecated since ^0.1.0 version, the plugin has SvelteKit ^1.0.0 as peer dependency, Vite's base is now properly configured. 8 | * @default '/' 9 | * @see https://kit.svelte.dev/docs/configuration#paths 10 | */ 11 | base?: string 12 | 13 | /** 14 | * The static folder for your application. 15 | * 16 | * @default 'static' 17 | * @see https://kit.svelte.dev/docs/configuration#files 18 | */ 19 | assets?: string 20 | 21 | /** 22 | * @default '.svelte-kit' 23 | * @see https://kit.svelte.dev/docs/configuration#outdir 24 | */ 25 | outDir?: string 26 | 27 | /** 28 | * @see https://kit.svelte.dev/docs/adapter-static#options-fallback 29 | */ 30 | adapterFallback?: string 31 | 32 | /** 33 | * Check your SvelteKit version, `trailingSlash` should be used in `+page[jt]s` files or `+layout.[jt]s. 34 | * @default 'never' 35 | */ 36 | trailingSlash?: 'never' | 'always' | 'ignore' 37 | 38 | /** 39 | * @default `_app` 40 | * @see https://kit.svelte.dev/docs/configuration#appdir 41 | */ 42 | appDir?: string 43 | 44 | /** 45 | * Include `${appDir}/version.json` in the service worker precache manifest? 46 | * 47 | * @default false 48 | */ 49 | includeVersionFile?: boolean 50 | 51 | /** 52 | * Enable SPA mode for the application. 53 | * 54 | * By default, the plugin will use `adapterFallback` to include the entry in the service worker 55 | * precache manifest. 56 | * 57 | * If you are using a logical name for the fallback, you can use the object syntax with the 58 | * `fallbackMapping`. 59 | * 60 | * For example, if you're using `fallback: 'app.html'` in your static adapter and your server 61 | * is redirecting to `/app`, you can configure `fallbackMapping: '/app'`. 62 | * 63 | * Since the static adapter will run after the PWA plugin generates the service worker, 64 | * the PWA plugin doesn't have access to the adapter fallback page to include the revision in the 65 | * service worker precache manifest. 66 | * To generate the revision for the fallback page, the PWA plugin will use the 67 | * `.svelte-kit/output/client/_app/version.json` file. 68 | * You can configure the `fallbackRevision` to generate a custom revision. 69 | * 70 | * @see https://svelte.dev/docs/kit/single-page-apps 71 | */ 72 | spa?: true | { 73 | fallbackMapping?: string 74 | fallbackRevision?: () => Promise 75 | } 76 | } 77 | 78 | export interface SvelteKitPWAOptions extends Partial { 79 | kit?: KitOptions 80 | } 81 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "resolveJsonModule": true, 7 | "types": [ 8 | "vite-plugin-pwa" 9 | ], 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noEmit": true 14 | }, 15 | "exclude": [ 16 | "dist", 17 | "node_modules", 18 | "test" 19 | ] 20 | } 21 | --------------------------------------------------------------------------------