├── .github ├── pull_request_template.md └── workflows │ ├── cr-comment.yml │ ├── cr.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── client.d.ts ├── docs ├── .vitepress │ ├── components.d.ts │ ├── config.ts │ ├── contributors.ts │ └── theme │ │ ├── components │ │ ├── CleanupOutdatedCaches.md │ │ ├── ExamplesBehaviors.md │ │ ├── ExamplesGenerateSW.md │ │ ├── ExamplesInjectManifest.md │ │ ├── GenerateSWCleanupOutdatedCaches.md │ │ ├── GenerateSWSourceMap.md │ │ ├── HomePage.vue │ │ ├── InjectManifestCleanupOutdatedCaches.md │ │ ├── InjectManifestSourceMap.md │ │ ├── PromptForUpdateImg.vue │ │ ├── ReloadPrompt.vue │ │ ├── RunExamples.md │ │ ├── SsrSsg.md │ │ └── TypeScriptError2307.md │ │ ├── index.ts │ │ └── styles │ │ ├── main.css │ │ └── vars.css ├── deployment │ ├── apache.md │ ├── aws.md │ ├── index.md │ ├── netlify.md │ ├── nginx.md │ └── vercel.md ├── examples │ ├── astro.md │ ├── iles.md │ ├── index.md │ ├── preact.md │ ├── react.md │ ├── solidjs.md │ ├── svelte.md │ ├── sveltekit.md │ ├── vitepress.md │ └── vue.md ├── frameworks │ ├── astro.md │ ├── iles.md │ ├── index.md │ ├── preact.md │ ├── react.md │ ├── solidjs.md │ ├── svelte.md │ ├── sveltekit.md │ ├── vitepress.md │ └── vue.md ├── guide │ ├── auto-update.md │ ├── development.md │ ├── faq.md │ ├── index.md │ ├── inject-manifest.md │ ├── periodic-sw-updates.md │ ├── prompt-for-update.md │ ├── pwa-minimal-requirements.md │ ├── register-service-worker.md │ ├── service-worker-precache.md │ ├── service-worker-strategies-and-behaviors.md │ ├── static-assets.md │ └── unregister-service-worker.md ├── index.md ├── netlify.toml ├── package.json ├── plugins │ └── navbar.ts ├── public │ ├── _headers │ ├── apple-touch-icon.png │ ├── banner_dark.svg │ ├── banner_light.svg │ ├── favicon.ico │ ├── favicon.svg │ ├── icon_dark.svg │ ├── icon_gray.svg │ ├── icon_light.svg │ ├── netlify.svg │ ├── og-image.png │ ├── prompt-update.png │ ├── pwa-192x192.png │ ├── pwa-512x512.png │ ├── robots.txt │ ├── safari-pinned-tab.svg │ └── team-avatars │ │ ├── antfu.png │ │ ├── hannoeru.png │ │ └── userquin.png ├── scripts │ ├── assets.ts │ ├── build.ts │ └── pwa.ts ├── tsconfig.json ├── vite.config.ts └── workbox │ ├── generate-sw.md │ ├── index.md │ └── inject-manifest.md ├── eslint.config.js ├── examples ├── assets-generator │ ├── index.html │ ├── package.json │ ├── public │ │ ├── favicon.svg │ │ └── source-test.png │ ├── pwa-assets.config.ts │ ├── src │ │ ├── main.ts │ │ ├── pwa.ts │ │ └── vite-env.ts │ ├── tsconfig.json │ └── vite.config.ts ├── preact-router │ ├── client-test │ │ ├── offline.spec.ts │ │ └── sw.spec.ts │ ├── index.html │ ├── package.json │ ├── playwright.config.ts │ ├── public │ │ ├── favicon.svg │ │ ├── pwa-192x192.png │ │ └── pwa-512x512.png │ ├── src │ │ ├── App.css │ │ ├── ReloadPrompt.css │ │ ├── ReloadPrompt.tsx │ │ ├── app.tsx │ │ ├── claims-sw.ts │ │ ├── favicon.svg │ │ ├── main.tsx │ │ ├── pages │ │ │ ├── About.tsx │ │ │ ├── Home.css │ │ │ ├── Home.tsx │ │ │ └── hi │ │ │ │ └── [name].tsx │ │ ├── preact.d.ts │ │ ├── prompt-sw.ts │ │ └── vite-env.d.ts │ ├── test │ │ └── build.test.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── vitest.config.mts ├── react-router │ ├── README.md │ ├── client-test │ │ ├── offline.spec.ts │ │ └── sw.spec.ts │ ├── index.html │ ├── package.json │ ├── playwright.config.ts │ ├── public │ │ ├── favicon.svg │ │ ├── pwa-192x192.png │ │ └── pwa-512x512.png │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── ReloadPrompt.css │ │ ├── ReloadPrompt.tsx │ │ ├── claims-sw.ts │ │ ├── index.css │ │ ├── main.tsx │ │ ├── pages │ │ │ ├── About.tsx │ │ │ ├── Home.css │ │ │ ├── Home.tsx │ │ │ └── hi │ │ │ │ └── [name].tsx │ │ ├── prompt-sw.ts │ │ └── vite-env.d.ts │ ├── test │ │ └── build.test.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── vitest.config.mts ├── solid-router │ ├── README.md │ ├── client-test │ │ ├── offline.spec.ts │ │ └── sw.spec.ts │ ├── index.html │ ├── package.json │ ├── playwright.config.ts │ ├── public │ │ ├── favicon.svg │ │ ├── pwa-192x192.png │ │ └── pwa-512x512.png │ ├── src │ │ ├── ReloadPrompt.module.css │ │ ├── ReloadPrompt.tsx │ │ ├── app.module.css │ │ ├── app.tsx │ │ ├── claims-sw.ts │ │ ├── errors │ │ │ └── 404.tsx │ │ ├── index.tsx │ │ ├── pages │ │ │ ├── about.tsx │ │ │ ├── hi │ │ │ │ ├── [name].data.ts │ │ │ │ └── [name].tsx │ │ │ ├── home.module.css │ │ │ └── home.tsx │ │ ├── prompt-sw.ts │ │ ├── routes.ts │ │ └── vite-env.d.ts │ ├── test │ │ └── build.test.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── vitest.config.mts ├── svelte-routify │ ├── README.md │ ├── client-test │ │ ├── offline.spec.ts │ │ └── sw.spec.ts │ ├── index.html │ ├── package.json │ ├── playwright.config.ts │ ├── public │ │ ├── favicon.svg │ │ ├── pwa-192x192.png │ │ └── pwa-512x512.png │ ├── src │ │ ├── App.svelte │ │ ├── claims-sw.ts │ │ ├── lib │ │ │ ├── Counter.svelte │ │ │ ├── Go.svelte │ │ │ └── ReloadPrompt.svelte │ │ ├── main.ts │ │ ├── pages │ │ │ ├── _fallback.svelte │ │ │ ├── _layout.svelte │ │ │ ├── about.svelte │ │ │ ├── hi │ │ │ │ └── [name].svelte │ │ │ └── index.svelte │ │ ├── prompt-sw.ts │ │ └── vite-env.d.ts │ ├── svelte.config.js │ ├── test │ │ └── build.test.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite-env.d.ts │ ├── vite.config.js │ └── vitest.config.mts ├── sveltekit-pwa │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── pwa-configuration.js │ ├── pwa.js │ ├── src │ │ ├── app.html │ │ ├── claims-sw.ts │ │ ├── global.d.ts │ │ ├── lib │ │ │ └── components │ │ │ │ ├── Counter.svelte │ │ │ │ ├── Go.svelte │ │ │ │ └── ReloadPrompt.svelte │ │ ├── prompt-sw.ts │ │ └── routes │ │ │ ├── __layout.svelte │ │ │ ├── about.svelte │ │ │ ├── hi │ │ │ └── [name].svelte │ │ │ └── index.svelte │ ├── static │ │ ├── favicon.svg │ │ ├── pwa-192x192.png │ │ └── pwa-512x512.png │ ├── svelte.config.js │ └── tsconfig.json ├── vanilla-js-custom-sw │ ├── index.html │ ├── package.json │ ├── public │ │ ├── favicon.svg │ │ ├── pwa-192x192.png │ │ └── pwa-512x512.png │ ├── service-worker │ │ └── sw.js │ ├── src │ │ └── main.js │ └── vite.config.js ├── vanilla-ts-dev-options │ ├── README.md │ ├── index.html │ ├── package.json │ ├── public │ │ ├── favicon.svg │ │ ├── pwa-192x192.png │ │ └── pwa-512x512.png │ ├── src │ │ ├── main.ts │ │ └── vite-env.ts │ ├── tsconfig.json │ └── vite.config.ts ├── vanilla-ts-no-ip │ ├── README.md │ ├── client-test │ │ └── sw.spec.ts │ ├── index.html │ ├── package.json │ ├── playwright.config.ts │ ├── public │ │ ├── favicon.svg │ │ ├── pwa-192x192.png │ │ └── pwa-512x512.png │ ├── src │ │ ├── custom-sw.ts │ │ ├── main.ts │ │ └── vite-env.ts │ ├── test │ │ └── build.test.ts │ ├── tsconfig.json │ ├── vite.config.ts │ └── vitest.config.mts ├── vue-basic-cdn │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.vue │ │ ├── ReloadPrompt.vue │ │ ├── index.css │ │ ├── main.ts │ │ └── shims-vue.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── vue-router │ ├── README.md │ ├── client-test │ ├── offline.spec.ts │ └── sw.spec.ts │ ├── index.html │ ├── package.json │ ├── playwright.config.ts │ ├── public │ ├── favicon.svg │ ├── pwa-192x192.png │ └── pwa-512x512.png │ ├── src │ ├── App.vue │ ├── ReloadPrompt.vue │ ├── claims-sw.ts │ ├── index.css │ ├── main.ts │ ├── my-worker.js │ ├── pages │ │ ├── about.vue │ │ ├── hi │ │ │ └── [name].vue │ │ └── home.vue │ ├── prompt-sw.ts │ ├── shims-vue.d.ts │ ├── vite-env.d.ts │ └── workerImport.js │ ├── test │ └── build.test.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── vitest.config.mts ├── index.d.ts ├── info.d.ts ├── netlify.toml ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── preact.d.ts ├── pwa-assets.d.ts ├── react.d.ts ├── scripts ├── build.ts ├── commands.ts ├── dev.ts └── run-examples.ts ├── solid.d.ts ├── src ├── api.ts ├── assets.ts ├── cache.ts ├── client │ ├── build │ │ ├── preact.ts │ │ ├── react.ts │ │ ├── register.ts │ │ ├── solid.ts │ │ ├── svelte.ts │ │ └── vue.ts │ ├── dev │ │ ├── preact.ts │ │ ├── react.ts │ │ ├── register.ts │ │ ├── solid.ts │ │ ├── svelte.ts │ │ └── vue.ts │ └── type.d.ts ├── constants.ts ├── context.ts ├── html.ts ├── index.ts ├── log.ts ├── modules.ts ├── options.ts ├── plugins │ ├── build.ts │ ├── dev.ts │ ├── info.ts │ ├── main.ts │ └── pwa-assets.ts ├── pwa-assets │ ├── build.ts │ ├── config.ts │ ├── dev.ts │ ├── generator.ts │ ├── html.ts │ ├── manifest.ts │ ├── options.ts │ ├── types.ts │ └── utils.ts ├── types.ts ├── utils.ts └── vite-build.ts ├── svelte.d.ts ├── tsconfig.json ├── types ├── index.d.ts └── package.json ├── vanillajs.d.ts └── vue.d.ts /.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 | dist-examples-root 6 | # intellij stuff 7 | .idea/ 8 | # routify 9 | .routify/ 10 | examples/*/test-results/ 11 | examples/*/playwright-report/ 12 | examples/*/playwright/.cache/ 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check=true 2 | shamefully-hoist=true 3 | strict-peer-dependencies=false 4 | auto-install-peers=true 5 | shell-emulator=true 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the ESlint flat config support 3 | "eslint.experimental.useFlatConfig": true, 4 | 5 | // Disable the default formatter, use eslint instead 6 | "prettier.enable": false, 7 | "editor.formatOnSave": false, 8 | 9 | // Auto fix 10 | "editor.codeActionsOnSave": { 11 | "source.fixAll.eslint": "explicit", 12 | "source.organizeImports": "never" 13 | }, 14 | 15 | // Silent the stylistic rules in you IDE, but still auto fix them 16 | "eslint.rules.customizations": [ 17 | { "rule": "style/*", "severity": "off" }, 18 | { "rule": "*-indent", "severity": "off" }, 19 | { "rule": "*-spacing", "severity": "off" }, 20 | { "rule": "*-spaces", "severity": "off" }, 21 | { "rule": "*-order", "severity": "off" }, 22 | { "rule": "*-dangle", "severity": "off" }, 23 | { "rule": "*-newline", "severity": "off" }, 24 | { "rule": "*quotes", "severity": "off" }, 25 | { "rule": "*semi", "severity": "off" } 26 | ], 27 | 28 | // Enable eslint for all supported languages 29 | "eslint.validate": [ 30 | "javascript", 31 | "javascriptreact", 32 | "typescript", 33 | "typescriptreact", 34 | "vue", 35 | "html", 36 | "markdown", 37 | "json", 38 | "jsonc", 39 | "yaml" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-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 | -------------------------------------------------------------------------------- /client.d.ts: -------------------------------------------------------------------------------- 1 | import './vue.d.ts' 2 | import './preact.d.ts' 3 | import './react.d.ts' 4 | import './svelte.d.ts' 5 | import './solid.d.ts' 6 | import './vanillajs.d.ts' 7 | -------------------------------------------------------------------------------- /docs/.vitepress/components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/core/pull/3399 4 | import '@vue/runtime-core' 5 | 6 | export {} 7 | 8 | declare module '@vue/runtime-core' { 9 | export interface GlobalComponents { 10 | CleanupOutdatedCaches: typeof import('./theme/components/CleanupOutdatedCaches.md')['default'] 11 | ExamplesBehaviors: typeof import('./theme/components/ExamplesBehaviors.md')['default'] 12 | ExamplesGenerateSW: typeof import('./theme/components/ExamplesGenerateSW.md')['default'] 13 | ExamplesInjectManifest: typeof import('./theme/components/ExamplesInjectManifest.md')['default'] 14 | GenerateSWCleanupOutdatedCaches: typeof import('./theme/components/GenerateSWCleanupOutdatedCaches.md')['default'] 15 | GenerateSWSourceMap: typeof import('./theme/components/GenerateSWSourceMap.md')['default'] 16 | HomePage: typeof import('./theme/components/HomePage.vue')['default'] 17 | InjectManifestCleanupOutdatedCaches: typeof import('./theme/components/InjectManifestCleanupOutdatedCaches.md')['default'] 18 | InjectManifestSourceMap: typeof import('./theme/components/InjectManifestSourceMap.md')['default'] 19 | PromptForUpdateImg: typeof import('./theme/components/PromptForUpdateImg.vue')['default'] 20 | ReloadPrompt: typeof import('./theme/components/ReloadPrompt.vue')['default'] 21 | RouterLink: typeof import('vue-router')['RouterLink'] 22 | RouterView: typeof import('vue-router')['RouterView'] 23 | RunExamples: typeof import('./theme/components/RunExamples.md')['default'] 24 | SsrSsg: typeof import('./theme/components/SsrSsg.md')['default'] 25 | TypeScriptError2307: typeof import('./theme/components/TypeScriptError2307.md')['default'] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/.vitepress/contributors.ts: -------------------------------------------------------------------------------- 1 | export interface SocialEntry { 2 | icon: string 3 | link: string 4 | } 5 | 6 | export interface CoreTeam { 7 | avatar: string 8 | name: string 9 | // required to download avatars from GitHub 10 | github: string 11 | twitter: string 12 | sponsor?: string 13 | title?: string 14 | org?: string 15 | desc?: string 16 | links?: SocialEntry[] 17 | } 18 | 19 | function createLinks(tm: CoreTeam): CoreTeam { 20 | tm.links = [ 21 | { icon: 'github', link: `https://github.com/${tm.github}` }, 22 | { icon: 'twitter', link: `https://twitter.com/${tm.twitter}` }, 23 | ] 24 | return tm 25 | } 26 | 27 | const plainTeamMembers = [ 28 | { 29 | avatar: '/team-avatars/antfu.png', 30 | name: 'Anthony Fu', 31 | github: 'antfu', 32 | twitter: 'antfu7', 33 | sponsor: 'https://github.com/sponsors/antfu', 34 | title: 'A fanatical open sourceror, working', 35 | org: 'NuxtLabs', 36 | desc: 'Core team member of Vite & Vue', 37 | }, 38 | { 39 | avatar: '/team-avatars/userquin.png', 40 | name: 'Joaquín Sánchez', 41 | github: 'userquin', 42 | twitter: 'userquin', 43 | title: 'A fullstack and android developer', 44 | desc: 'Vite\'s fanatical follower', 45 | }, 46 | { 47 | avatar: '/team-avatars/hannoeru.png', 48 | name: 'ハン / Han', 49 | github: 'hannoeru', 50 | twitter: 'hannoeru', 51 | title: 'Student / Front-End Engineer', 52 | desc: '@windi_css member', 53 | }, 54 | ] 55 | 56 | const teamMembers = plainTeamMembers.map(tm => createLinks(tm)) 57 | 58 | export { teamMembers } 59 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/CleanupOutdatedCaches.md: -------------------------------------------------------------------------------- 1 | The service worker will store all your application assets in a browser cache (or set of caches). Every time you make changes to your application and rebuild it, the `service worker` will be also rebuild, including in its precache manifest all new modified assets, which will have their revision changed (all assets that have been modified will have a new version). Assets that have not been modified will also be included in the service worker precache manifest, but their revision will not change from the previous one. 2 | 3 | ::: tip Precache Manifest Entry Revision 4 | The precache manifest entry revision is just a `MD5` hash of the asset content, if an asset is not modififed, the calculated hash will be always the same. 5 | ::: 6 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/ExamplesBehaviors.md: -------------------------------------------------------------------------------- 1 | - `Prompt for update`: 2 | - Show `Ready to work offline` on first visit and once the `service worker` ready. 3 | - Show `Prompt for update` when new `service worker` available. 4 | 5 | - `Auto update`: 6 | - Show `Ready to work offline` on first visit and once the `service worker` ready. 7 | - When new content available, the service worker will be updated automatically. 8 | 9 | - `Prompt for update` with `Periodic service worker updates`: 10 | - Show `Ready to work offline` on first visit and once the `service worker` ready. 11 | - Show `Prompt for update` when new `service worker` available. 12 | - The example project will register a `Periodic service worker updates` 13 | 14 | - `Auto update` with `Periodic service worker updates`: 15 | - Show `Ready to work offline` on first visit and once the `service worker` ready. 16 | - The example project will register a `Periodic service worker updates` 17 | - When new content available, the service worker will be updated automatically. 18 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/ExamplesGenerateSW.md: -------------------------------------------------------------------------------- 1 | `generateSW` has the following behaviors: 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/ExamplesInjectManifest.md: -------------------------------------------------------------------------------- 1 | `injectManifest` has the following behavior: 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/GenerateSWCleanupOutdatedCaches.md: -------------------------------------------------------------------------------- 1 | When the browser detects and installs the new version of your application, it will have in the cache storage all new assets and also the old ones. To delete old assets (from previous versions that are no longer necessary), you have to configure an option in the `workbox` entry of the plugin configuration. 2 | 3 | When using the `generateSW` strategy, it is not necessary to configure it, the plugin will activate it by default. 4 | 5 | We strongly recommend you to **NOT** deactivate the option. If you are curious, you can deactivate it using the following code in your plugin configuration: 6 | 7 | ```ts 8 | import { VitePWA } from 'vite-plugin-pwa' 9 | 10 | export default defineConfig({ 11 | plugins: [ 12 | VitePWA({ 13 | workbox: { 14 | cleanupOutdatedCaches: false 15 | } 16 | }) 17 | ] 18 | }) 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/GenerateSWSourceMap.md: -------------------------------------------------------------------------------- 1 | Since plugin version `0.11.2`, your service worker's source map will not be generated as it uses the `build.sourcemap` option from the Vite config, which by default is `false`. 2 | 3 | Your service worker source map will be generated when Vite's `build.sourcemap` configuration option has the value `true`, `'online'` or `'hidden'`, and you have not configured the `workbox.sourcemap` option in the plugin configuration. If you configure the `workbox.sourcemap` option, the plugin will not change that value. 4 | 5 | If you want to generate the source map of your service worker, you can use this code: 6 | 7 | ```ts 8 | import { VitePWA } from 'vite-plugin-pwa' 9 | 10 | export default defineConfig({ 11 | plugins: [ 12 | VitePWA({ 13 | workbox: { 14 | sourcemap: true 15 | } 16 | }) 17 | ] 18 | }) 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/HomePage.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 46 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/InjectManifestCleanupOutdatedCaches.md: -------------------------------------------------------------------------------- 1 | When the user installs the new version of the application, we will have on the service worker cache all new assets and also the old ones. To delete old assets (from previous versions that are no longer necessary), and since you are building your own service worker, you will need to add the following code to your custom service worker: 2 | 3 | ```js 4 | import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching' 5 | 6 | cleanupOutdatedCaches() 7 | 8 | precacheAndRoute(self.__WB_MANIFEST) 9 | ``` 10 | 11 | We strongly recommend you to include previous code on your custom service worker. 12 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/InjectManifestSourceMap.md: -------------------------------------------------------------------------------- 1 | Since you are building your own service worker, this plugin will use Vite's `build.sourcemap` configuration option, which default value is `false`, to generate the source map. 2 | 3 | If you want to generate the source map for your service worker, you will need to generate the source map for the entire application. 4 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/PromptForUpdateImg.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/ReloadPrompt.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 52 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/RunExamples.md: -------------------------------------------------------------------------------- 1 | ::: warning 2 | Before following the instructions below, read the [Contribution Guide](https://github.com/antfu/vite-plugin-pwa/blob/main/CONTRIBUTING.md). 3 | ::: 4 | 5 | Make sure you install project dependencies, and build the repo on your local clone/fork: 6 | 7 | ```shell 8 | cd vite-plugin-pwa 9 | pnpm install 10 | pnpm run build 11 | ``` 12 | 13 | To run the examples, execute the following script from your shell (from root folder), it will start a CLI where you will select the framework and the pwa options: 14 | 15 | ```shell 16 | pnpm run examples 17 | ``` 18 | 19 | If you don't run `pnpm build` first, you may see an error like, `failed to load config` or `Please verify that the package.json has a valid "main" entry`. 20 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/SsrSsg.md: -------------------------------------------------------------------------------- 1 | If you are using `SSR/SSG`, you need to import `virtual:pwa-register` module using dynamic import and checking if `window` is not `undefined`. 2 | 3 | You can register the service worker on `src/pwa.ts` module: 4 | 5 | ```ts 6 | import { registerSW } from 'virtual:pwa-register' 7 | 8 | registerSW({ /* ... */ }) 9 | ``` 10 | 11 | and then import it from your `main.ts`: 12 | 13 | ```ts 14 | if (typeof window !== 'undefined') 15 | import('./pwa') 16 | ``` 17 | 18 | You can see the [FAQ](/guide/faq#navigator-window-is-undefined) entry for more info. 19 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/TypeScriptError2307.md: -------------------------------------------------------------------------------- 1 | If your **TypeScript** build step or **IDE** complain about not being able to find modules or type definitions on imports, add the following to the `compilerOptions.types` array of your `tsconfig.json`: 2 | 3 | ```json 4 | { 5 | "compilerOptions": { 6 | "types": [ 7 | "vite-plugin-pwa/client" 8 | ] 9 | } 10 | } 11 | ``` 12 | 13 | Or you can add the following reference in any of your `d.ts` files (for example, in `vite-env.d.ts` or `global.d.ts`): 14 | ```ts 15 | /// 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import Theme from 'vitepress/theme' 3 | 4 | import './styles/main.css' 5 | import './styles/vars.css' 6 | 7 | import 'uno.css' 8 | 9 | import HomePage from './components/HomePage.vue' 10 | import ReloadPrompt from './components/ReloadPrompt.vue' 11 | 12 | export default { 13 | ...Theme, 14 | Layout() { 15 | return h(Theme.Layout, null, { 16 | 'home-features-after': () => h(HomePage), 17 | 'layout-bottom': () => h(ReloadPrompt), 18 | }) 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /docs/deployment/apache.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Apache Http Server 2.4+ | Deployment 3 | next: Getting Started | Workbox 4 | --- 5 | 6 | # Apache Http Server 2.4+ 7 | 8 | ## Configure `manifest.webmanifest` mime type 9 | 10 | You need to configure the following mime type (see basic configuration below): 11 | 12 | ```ini 13 | 14 | # Manifest file 15 | AddType application/manifest+json webmanifest 16 | 17 | ``` 18 | 19 | ## Basic configuration with http to https redirection 20 | 21 | Update your `httpd.conf` configuration file with: 22 | 23 | ```ini 24 | # httpd.conf 25 | ServerRoot "" 26 | 27 | Listen 80 28 | ServerName www.yourdomain.com 29 | 30 | DocumentRoot "" 31 | 32 | # modules 33 | LoadModule mime_module modules/mod_mime.so 34 | LoadModule rewrite_module modules/mod_rewrite.so 35 | 36 | # mime types 37 | 38 | # Manifest file 39 | AddType application/manifest+json webmanifest 40 | 41 | 42 | # your https configuration 43 | Include conf/extra/https-www.yourdomain.com.conf 44 | 45 | 46 | SSLRandomSeed startup builtin 47 | SSLRandomSeed connect builtin 48 | 49 | 50 | 51 | ServerName www.yourdomain.com 52 | 53 | RewriteEngine On 54 | 55 | # disable TRACE and TRACK methods 56 | RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK) 57 | RewriteRule .* - [F] 58 | 59 | Options +FollowSymlinks 60 | RewriteCond %{SERVER_PORT} !443 61 | 62 | RewriteRule (.*) https://www.yourdomain.com/ [L,R] 63 | 64 | ErrorLog logs/www.yourdomain.com-error_log 65 | CustomLog logs/www.yourdomain.com-access_log combined 66 | 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/deployment/aws.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: AWS Amplify | Deployment 3 | --- 4 | 5 | # AWS Amplify 6 | 7 | ::: info WIP 8 | Will coming soon. 9 | ::: 10 | -------------------------------------------------------------------------------- /docs/deployment/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started | Deploy 3 | prev: Astro | Examples 4 | --- 5 | 6 | # Getting Started 7 | 8 | Since you need to install your application as a [Progressive Web App](https://web.dev/progressive-web-apps/), you must configure your server to meet [PWA Minimal Requirements](/guide/#pwa-minimal-requirements), that is, your server **must**: 9 | - serve `manifest.webmanifest` with `application/manifest+json` mime type 10 | - you must serve your application over `https` 11 | - you must redirect from `http` to `https` 12 | 13 | ## Servers 14 | 15 | - [Netlify](/deployment/netlify) 16 | - [AWS Amplify](/deployment/aws) 17 | - [Vercel](/deployment/aws) 18 | - [NGINX](/deployment/nginx) 19 | - [Apache Http Server 2.4+](/deployment/apache) 20 | 21 | 22 | ## Testing your application on production 23 | 24 | Once you deploy your application to your server, you can test it using [WebPageTest](https://www.webpagetest.org/). 25 | 26 | There are many test sites, but we suggest you use `WebPageTest` as this is the most comprehensive in terms of test: 27 | - Security. 28 | - First byte time. 29 | - Keep alive enabled. 30 | - Compress transfer. 31 | - Cache static content. 32 | - Effective use of CDN. 33 | - Lighthouse: Core Web Vitals, Performance, Images size optimization... 34 | - And more... 35 | 36 | Enter the url of your application, click `Start Test` button, wait for the test to finish, the `WebPageTest` result will hint you what things on your application must be fixed/changed. The `WebPageTest` result will also score your application, it will also test your site with `Lighthouse`. 37 | 38 | For example, go to [WebPageTest](https://www.webpagetest.org/), enter `https://vite-pwa-org.netlify.app/`, click `Start Test` button, wait a few seconds for the test to finish, and see the results for this site. 39 | -------------------------------------------------------------------------------- /docs/deployment/netlify.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Netlify | Deployment 3 | --- 4 | 5 | # Netlify 6 | 7 | ## Configure `manifest.webmanifest` mime type 8 | 9 | You need to register the correct MIME type for the web manifest by adding a headers table to your `netlify.toml` file (see basic deployment below): 10 | ```toml 11 | [[headers]] 12 | for = "/manifest.webmanifest" 13 | [headers.values] 14 | Content-Type = "application/manifest+json" 15 | ``` 16 | 17 | ## Cache-Control 18 | 19 | As a general rule, files in `/assets/` can have a very long cache time, as everything in there should contain a hash in the filename. 20 | 21 | Add another headers table to your `netlify.toml` file (see basic deployment below): 22 | 23 | ```toml 24 | [[headers]] 25 | for = "/assets/*" 26 | [headers.values] 27 | cache-control = ''' 28 | max-age=31536000, 29 | immutable 30 | ''' 31 | ``` 32 | 33 | ## Configure http to https redirection 34 | 35 | Netlify will redirect automatically, so you don't worry about it. 36 | 37 | ## Basic deployment example 38 | 39 | Add `netlify.toml` file to the root directory with the content: 40 | 41 | ```toml 42 | [build.environment] 43 | NPM_FLAGS = "--prefix=/dev/null" 44 | NODE_VERSION = "14" 45 | 46 | [build] 47 | publish = "dist" 48 | command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run build" 49 | 50 | [[redirects]] 51 | from = "/*" 52 | to = "/index.html" 53 | status = 200 54 | 55 | [[headers]] 56 | for = "/manifest.webmanifest" 57 | [headers.values] 58 | Content-Type = "application/manifest+json" 59 | 60 | [[headers]] 61 | for = "/assets/*" 62 | [headers.values] 63 | cache-control = ''' 64 | max-age=31536000, 65 | immutable 66 | ''' 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/deployment/vercel.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vercel | Deployment 3 | --- 4 | 5 | # Vercel 6 | 7 | ::: info WIP 8 | Will coming soon. 9 | ::: 10 | -------------------------------------------------------------------------------- /docs/examples/astro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Astro | Examples 3 | next: Getting Started | Deploy 4 | --- 5 | 6 | # Astro 7 | 8 | ::: info WIP 9 | Will coming soon. 10 | ::: 11 | -------------------------------------------------------------------------------- /docs/examples/iles.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: îles | Frameworks 3 | --- 4 | 5 | # îles 6 | 7 | You can test `îles` using the source code from its documentation website, you can find it under [docs](https://github.com/ElMassimo/iles/tree/main/docs) package/directory. 8 | 9 | The behavior used in this website is [Prompt for update](/guide/prompt-for-update). 10 | -------------------------------------------------------------------------------- /docs/examples/preact.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Preact | Examples 3 | --- 4 | 5 | # Preact 6 | 7 | The `Preact` example project can be found on `examples/preact-router` package/directory. 8 | 9 | The router used on this example project is [preact-router](https://github.com/preactjs/preact-router). 10 | 11 | The `Preact` example has been created using `create-vite` template with `pnpx`: 12 | 13 | ::: details pnpx create-vite 14 | 15 | ```shell 16 | pnpx create-vite 17 | + create-vite 2.6.6 18 | √ Project name: ... preact-router 19 | √ Select a framework: » preact 20 | √ Select a variant: » preact-ts 21 | 22 | Scaffolding project in examples\preact-router... 23 | 24 | Done. Now run: 25 | 26 | cd preact-router 27 | npm install 28 | npm run dev 29 | ``` 30 | ::: 31 | 32 | To test `new content available`, you should rerun the corresponding script, and then refresh the page. 33 | 34 | ## Executing the examples 35 | 36 | 37 | 38 | ## generateSW 39 | 40 | 41 | 42 | ## injectManifest 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/examples/react.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React | Examples 3 | --- 4 | 5 | # React 6 | 7 | The `React` example project can be found on `examples/react-router` package/directory. 8 | 9 | The router used on this example project is [react-router](https://reactrouter.com/). 10 | 11 | The `React` example has been created using `create-vite` template with `pnpx`: 12 | 13 | ::: details pnpx create-vite 14 | 15 | ```shell 16 | pnpx create-vite 17 | + create-vite 2.5.4 18 | √ Project name: ... react-router 19 | √ Select a framework: » react 20 | √ Select a variant: » react-ts 21 | 22 | Scaffolding project in examples\react-router... 23 | 24 | Done. Now run: 25 | 26 | cd react-router 27 | npm install 28 | npm run dev 29 | ``` 30 | ::: 31 | 32 | To test `new content available`, you should rerun the corresponding script, and then refresh the page. 33 | 34 | ## Executing the examples 35 | 36 | 37 | 38 | ## generateSW 39 | 40 | 41 | 42 | ## injectManifest 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/examples/solidjs.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: SolidJS | Examples 3 | --- 4 | 5 | # SolidJS 6 | 7 | The `SolidJS` example project can be found on `examples/solid-router` package/directory. 8 | 9 | The router used on this example project is [solid-app-router](https://github.com/solidjs/solid-app-router). 10 | 11 | The `SolidJS` example has been created using `https://github.com/solidjs/templates` template with `npx`: 12 | 13 | ::: details npx degit solidjs/templates/ts-router solid-router 14 | 15 | ```shell 16 | npx degit solidjs/templates/ts-router solid-router 17 | > cloned solidjs/templates#HEAD to solid-router 18 | ``` 19 | ::: 20 | 21 | To test `new content available`, you should rerun the corresponding script, and then refresh the page. 22 | 23 | ## Executing the examples 24 | 25 | 26 | 27 | ## generateSW 28 | 29 | 30 | 31 | ## injectManifest 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/examples/svelte.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Svelte | Examples 3 | --- 4 | 5 | # Svelte 6 | 7 | The `Svelte` example project can be found on `examples/svelte-routify` package/directory. 8 | 9 | The router used on this example project is [@roxi/routify](https://routify.dev/). 10 | 11 | The `Svelte` example has been created using `create-vite` template with `pnpx`: 12 | 13 | ::: details pnpx create-vite 14 | ```shell 15 | pnpx create-vite 16 | + create-vite 2.5.4 17 | √ Project name: ... svelte-routify 18 | √ Select a framework: » svelte 19 | √ Select a variant: » svelte-ts 20 | 21 | Scaffolding project in examples\svelte-routify... 22 | 23 | Done. Now run: 24 | 25 | cd svelte-routify 26 | npm install 27 | npm run dev 28 | ``` 29 | ::: 30 | 31 | To test `new content available`, you should rerun the corresponding script, and then refresh the page. 32 | 33 | ## Executing the examples 34 | 35 | 36 | 37 | ## generateSW 38 | 39 | 40 | 41 | ## injectManifest 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/examples/sveltekit.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: SvelteKit | Examples 3 | --- 4 | 5 | # SvelteKit 6 | 7 | The `SvelteKit` example project can be found on `examples/sveltekit-pwa` package/directory and it is configured with `@sveltejs/adapter-static` adapter. 8 | 9 | The `SvelteKit` example has been created using `svelte@next` template with `pnpm`: 10 | 11 | ::: details pnpm create svelte@next sveltekit-pwa 12 | ```shell 13 | pnpm create svelte@next sveltekit-pwa 14 | + create-svelte 2.0.0-next.89 15 | 16 | Progress: resolved 5, reused 5, downloaded 0, added 5, done 17 | 18 | create-svelte version 2.0.0-next.89 19 | 20 | Welcome to SvelteKit! 21 | 22 | This is beta software; expect bugs and missing features. 23 | 24 | Problems? Open an issue on https://github.com/sveltejs/kit/issues if none exists already. 25 | 26 | √ Which Svelte app template? » Skeleton project 27 | √ Use TypeScript? ... No / Yes 28 | √ Add ESLint for code linting? ... No / Yes 29 | √ Add Prettier for code formatting? ... No / Yes 30 | 31 | Your project is ready! 32 | ✔ Typescript 33 | Inside Svelte components, use 17 | 18 | 27 | 28 | 66 | ` 67 | } 68 | }, 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /docs/public/_headers: -------------------------------------------------------------------------------- 1 | / 2 | X-Frame-Options: DENY 3 | X-XSS-Protection: 1; mode=block 4 | 5 | /guide/ 6 | X-Frame-Options: DENY 7 | X-XSS-Protection: 1; mode=block 8 | 9 | /frameworks/ 10 | X-Frame-Options: DENY 11 | X-XSS-Protection: 1; mode=block 12 | 13 | /examples/ 14 | X-Frame-Options: DENY 15 | X-XSS-Protection: 1; mode=block 16 | 17 | /deployment/ 18 | X-Frame-Options: DENY 19 | X-XSS-Protection: 1; mode=block 20 | 21 | /workbox/ 22 | X-Frame-Options: DENY 23 | X-XSS-Protection: 1; mode=block 24 | 25 | /*.html 26 | X-Frame-Options: DENY 27 | X-XSS-Protection: 1; mode=block 28 | 29 | /* 30 | X-Content-Type-Options: nosniff 31 | Referrer-Policy: no-referrer 32 | Strict-Transport-Security: max-age=31536000; includeSubDomains 33 | 34 | /assets/* 35 | cache-control: max-age=31536000 36 | cache-control: immutable 37 | -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/docs/public/og-image.png -------------------------------------------------------------------------------- /docs/public/prompt-update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/docs/public/prompt-update.png -------------------------------------------------------------------------------- /docs/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/docs/public/pwa-192x192.png -------------------------------------------------------------------------------- /docs/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/docs/public/pwa-512x512.png -------------------------------------------------------------------------------- /docs/public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /docs/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/public/team-avatars/antfu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/docs/public/team-avatars/antfu.png -------------------------------------------------------------------------------- /docs/public/team-avatars/hannoeru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/docs/public/team-avatars/hannoeru.png -------------------------------------------------------------------------------- /docs/public/team-avatars/userquin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/docs/public/team-avatars/userquin.png -------------------------------------------------------------------------------- /docs/scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { resolveConfig } from 'vite' 2 | import type { VitePluginPWAAPI } from 'vite-plugin-pwa' 3 | import { optimizePages } from './assets' 4 | import { pwa } from './pwa' 5 | 6 | export async function buildEnd() { 7 | await optimizePages() 8 | const config = await resolveConfig({ plugins: [pwa()] }, 'build', 'production') 9 | // when `vite-plugin-pwa` is presented, use it to regenerate SW after rendering 10 | const pwaPlugin: VitePluginPWAAPI = config.plugins.find(i => i.name === 'vite-plugin-pwa')?.api 11 | if (pwaPlugin && pwaPlugin.generateSW && !pwaPlugin.disabled) 12 | await pwaPlugin.generateSW() 13 | } 14 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "incremental": false, 5 | "target": "es2016", 6 | "lib": ["DOM", "ESNext"], 7 | "baseUrl": ".", 8 | "module": "ESNext", 9 | "moduleResolution": "bundler", 10 | "paths": { 11 | "~/*": ["src/*"] 12 | }, 13 | "resolveJsonModule": true, 14 | "types": [ 15 | "@types/fs-extra", 16 | "node", 17 | "vite/client", 18 | "vite-plugin-pwa/client", 19 | "vitepress" 20 | ], 21 | "strict": true, 22 | "strictNullChecks": true, 23 | "noUnusedLocals": true, 24 | "esModuleInterop": true, 25 | "forceConsistentCasingInFileNames": true, 26 | "skipLibCheck": true 27 | }, 28 | "include": [ 29 | "./*.ts", 30 | "./.vitepress/**/*.ts", 31 | "./.vitepress/**/*.vue" 32 | ], 33 | "exclude": ["dist", "node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /docs/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import Components from 'unplugin-vue-components/vite' 3 | import { presetAttributify, presetUno } from 'unocss' 4 | import Unocss from 'unocss/vite' 5 | import NavbarFix from './plugins/navbar' 6 | 7 | export default defineConfig({ 8 | ssr: { 9 | format: 'cjs', 10 | }, 11 | legacy: { 12 | buildSsrCjsExternalHeuristics: true, 13 | }, 14 | build: { 15 | // sourcemap: true, 16 | // minify: false, 17 | ssrManifest: false, 18 | manifest: false, 19 | }, 20 | optimizeDeps: { 21 | exclude: [ 22 | '@vueuse/core', 23 | 'vitepress', 24 | ], 25 | }, 26 | server: { 27 | hmr: { 28 | overlay: false, 29 | }, 30 | }, 31 | plugins: [ 32 | // https://github.com/antfu/vite-plugin-components 33 | Components({ 34 | dirs: [ 35 | '.vitepress/theme/components', 36 | ], 37 | // allow auto load markdown components under `./src/components/` 38 | extensions: ['vue', 'md'], 39 | 40 | // allow auto import and register components used in markdown 41 | include: [/\.vue$/, /\.vue\?vue/, /\.md$/], 42 | 43 | // generate `components.d.ts` for ts support with Volar 44 | dts: '.vitepress/components.d.ts', 45 | }), 46 | 47 | NavbarFix(), 48 | 49 | // https://github.com/unocss/unocss 50 | Unocss({ 51 | presets: [presetUno(), presetAttributify()], 52 | }), 53 | ], 54 | }) 55 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default await antfu( 4 | { 5 | ignores: [ 6 | 'netlify.toml', 7 | '**/build/**', 8 | '**/dist/**', 9 | '**/docs/**', 10 | '**/dev-dist/**', 11 | ], 12 | }, 13 | { 14 | rules: { 15 | 'node/prefer-global/process': 'off', 16 | 'ts/no-this-alias': 'off', 17 | 'no-restricted-globals': 'off', 18 | 'node/handle-callback-err': 'off', 19 | }, 20 | }, 21 | { 22 | files: ['**/examples/**'], 23 | rules: { 24 | 'no-console': 'off', 25 | }, 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /examples/assets-generator/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PWA Assets Generator 7 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/assets-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "assets-generator", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "dev:png": "PNG=true vite", 11 | "build:png": "PNG=true vite build", 12 | "preview:png": "PNG=true vite preview", 13 | "dev:inline:svg": "INLINE_PWA_ASSETS=true vite", 14 | "build:inline:svg": "INLINE_PWA_ASSETS=true vite build", 15 | "preview:inline:svg": "INLINE_PWA_ASSETS=true vite preview", 16 | "dev:inline:png": "INLINE_PWA_ASSETS=true PNG=true vite", 17 | "build:inline:png": "INLINE_PWA_ASSETS=true PNG=true vite build", 18 | "preview:inline:png": "INLINE_PWA_ASSETS=true PNG=true vite preview" 19 | }, 20 | "devDependencies": { 21 | "@vite-pwa/assets-generator": "^1.0.0", 22 | "rimraf": "^5.0.5", 23 | "typescript": "^5.3.3", 24 | "vite": "^5.0.10", 25 | "vite-plugin-pwa": "workspace:*", 26 | "workbox-window": "^7.3.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/assets-generator/public/source-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/assets-generator/public/source-test.png -------------------------------------------------------------------------------- /examples/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 | basePath: '/', 21 | xhtml: true, 22 | }, 23 | }, ['iPad Air 9.7"']), 24 | }, 25 | images: process.env.PNG ? 'public/source-test.png' : 'public/favicon.svg', 26 | }) 27 | -------------------------------------------------------------------------------- /examples/assets-generator/src/main.ts: -------------------------------------------------------------------------------- 1 | const date = __DATE__ 2 | 3 | const app = document.querySelector('#app')! 4 | 5 | app.innerHTML = ` 6 |
7 | PWA Logo 8 |

PWA Assets Generator

9 |
10 |

${date}

11 |
12 |
13 | ` 14 | 15 | window.addEventListener('load', () => { 16 | import('./pwa.ts') 17 | }) 18 | -------------------------------------------------------------------------------- /examples/assets-generator/src/pwa.ts: -------------------------------------------------------------------------------- 1 | import { pwaAssetsHead } from 'virtual:pwa-assets/head' 2 | import { pwaAssetsIcons } from 'virtual:pwa-assets/icons' 3 | import { pwaInfo } from 'virtual:pwa-info' 4 | import { registerSW } from 'virtual:pwa-register' 5 | 6 | console.log(pwaInfo) 7 | console.log(pwaAssetsHead) 8 | console.log(pwaAssetsIcons) 9 | 10 | registerSW({ 11 | immediate: true, 12 | onNeedRefresh() { 13 | console.log('onNeedRefresh message should not appear') 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /examples/assets-generator/src/vite-env.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | declare const __DATE__: string 6 | -------------------------------------------------------------------------------- /examples/assets-generator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ES2020", "DOM", "DOM.Iterable", "WebWorker"], 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "types": ["vite/client", "vite-plugin-pwa/vanillajs", "vite-plugin-pwa/info", "vite-plugin-pwa/pwa-assets"], 12 | "allowImportingTsExtensions": true, 13 | 14 | /* Linting */ 15 | "strict": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noEmit": true, 20 | "isolatedModules": true, 21 | "skipLibCheck": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/preact-router/client-test/offline.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | const injectManifest = process.env.SW === 'true' 4 | const swName = `${injectManifest ? 'claims-sw.js' : 'sw.js'}` 5 | 6 | test('Preact: Test offline', 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 | 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 any 17 | return registration.active?.scriptURL 18 | }) 19 | expect(offlineSwURL).toBe(`http://localhost:4173/${swName}`) 20 | await context.setOffline(true) 21 | const aboutAnchor = offlinePage.getByRole('link', { name: 'About' }) 22 | expect(await aboutAnchor.getAttribute('href')).toBe('/about') 23 | await aboutAnchor.click({ noWaitAfter: false }) 24 | const url = await offlinePage.evaluate(async () => { 25 | await new Promise(resolve => setTimeout(resolve, 3000)) 26 | return location.href 27 | }) 28 | expect(url).toBe('http://localhost:4173/about') 29 | expect(offlinePage.locator('a').getByText('Go Home')).toBeTruthy() 30 | await offlinePage.reload({ waitUntil: 'load' }) 31 | expect(offlinePage.url()).toBe('http://localhost:4173/about') 32 | expect(offlinePage.locator('a').getByText('Go Home')).toBeTruthy() 33 | // Dispose context once it's no longer needed. 34 | await context.close() 35 | }) 36 | -------------------------------------------------------------------------------- /examples/preact-router/client-test/sw.spec.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { expect, test } from '@playwright/test' 3 | 4 | const injectManifest = process.env.SW === 'true' 5 | const swName = `${injectManifest ? 'claims-sw.js' : 'sw.js'}` 6 | 7 | test('Preact: The service worker is registered and cache storage is present', async ({ page }) => { 8 | await page.goto('/') 9 | 10 | const swURL = await page.evaluate(async () => { 11 | const registration = await Promise.race([ 12 | navigator.serviceWorker.ready, 13 | new Promise((_resolve, reject) => setTimeout(() => reject(new Error('Service worker registration failed: time out')), 10000)), 14 | ]) 15 | // @ts-expect-error registration is of type any 16 | return registration.active?.scriptURL 17 | }) 18 | expect(swURL).toBe(`http://localhost:4173/${swName}`) 19 | 20 | const cacheContents = await page.evaluate(async () => { 21 | const cacheState: Record> = {} 22 | for (const cacheName of await caches.keys()) { 23 | const cache = await caches.open(cacheName) 24 | cacheState[cacheName] = (await cache.keys()).map(req => req.url) 25 | } 26 | return cacheState 27 | }) 28 | 29 | expect(Object.keys(cacheContents).length).toEqual(1) 30 | 31 | const key = 'workbox-precache-v2-http://localhost:4173/' 32 | 33 | expect(Object.keys(cacheContents)[0]).toEqual(key) 34 | 35 | const urls = cacheContents[key].map(url => url.slice('http://localhost:4173/'.length)) 36 | 37 | /* 38 | 'http://localhost:4173/index.html?__WB_REVISION__=073370aa3804305a787b01180cd6b8aa', 39 | 'http://localhost:4173/manifest.webmanifest?__WB_REVISION__=27df2fa4f35d014b42361148a2207da3' 40 | */ 41 | expect(urls.some(url => url.startsWith('manifest.webmanifest?__WB_REVISION__='))).toEqual(true) 42 | expect(urls.some(url => url.startsWith('index.html?__WB_REVISION__='))).toEqual(true) 43 | expect(urls.some(url => url.startsWith(`${swName}`))).toEqual(false) 44 | }) 45 | -------------------------------------------------------------------------------- /examples/preact-router/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Vite App 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/preact-router/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/preact-router/public/pwa-192x192.png -------------------------------------------------------------------------------- /examples/preact-router/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/preact-router/public/pwa-512x512.png -------------------------------------------------------------------------------- /examples/preact-router/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | padding: 1em; 4 | margin: 0 auto; 5 | } 6 | 7 | .App-built { 8 | margin: 1rem 0; 9 | } 10 | 11 | .App-title { 12 | color: #03607c; 13 | font-size: 2.2rem; 14 | } 15 | 16 | button { 17 | font-size: calc(10px + 2vmin); 18 | } 19 | -------------------------------------------------------------------------------- /examples/preact-router/src/ReloadPrompt.css: -------------------------------------------------------------------------------- 1 | .ReloadPrompt-container { 2 | padding: 0; 3 | margin: 0; 4 | width: 0; 5 | height: 0; 6 | } 7 | .ReloadPrompt-date { 8 | visibility: hidden; 9 | } 10 | .ReloadPrompt-toast { 11 | position: fixed; 12 | right: 0; 13 | bottom: 0; 14 | margin: 16px; 15 | padding: 12px; 16 | border: 1px solid #8885; 17 | border-radius: 4px; 18 | z-index: 1; 19 | text-align: left; 20 | box-shadow: 3px 4px 5px 0 #8885; 21 | background-color: white; 22 | } 23 | .ReloadPrompt-toast-message { 24 | margin-bottom: 8px; 25 | } 26 | .ReloadPrompt-toast-button { 27 | border: 1px solid #8885; 28 | outline: none; 29 | margin-right: 5px; 30 | border-radius: 2px; 31 | padding: 3px 10px; 32 | } 33 | -------------------------------------------------------------------------------- /examples/preact-router/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { Router } from 'preact-router' 2 | 3 | import About from './pages/About' 4 | import Hi from './pages/hi/[name]' 5 | import Home from './pages/Home' 6 | import ReloadPrompt from './ReloadPrompt' 7 | 8 | import './App.css' 9 | 10 | export function App() { 11 | // replaced dyanmicaly 12 | const date = '__DATE__' 13 | return ( 14 | <> 15 |
16 | PWA Logo 17 |

PWA Preact!

18 |
19 | Built at: 20 | {date} 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /examples/preact-router/src/claims-sw.ts: -------------------------------------------------------------------------------- 1 | import { clientsClaim } from 'workbox-core' 2 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 3 | import { NavigationRoute, registerRoute } from 'workbox-routing' 4 | 5 | declare let self: ServiceWorkerGlobalScope 6 | 7 | // self.__WB_MANIFEST is default injection point 8 | precacheAndRoute(self.__WB_MANIFEST) 9 | 10 | // clean old assets 11 | cleanupOutdatedCaches() 12 | 13 | let allowlist: undefined | RegExp[] 14 | if (import.meta.env.DEV) 15 | allowlist = [/^\/$/] 16 | 17 | // to allow work offline 18 | registerRoute(new NavigationRoute( 19 | createHandlerBoundToURL('index.html'), 20 | { allowlist }, 21 | )) 22 | 23 | self.skipWaiting() 24 | clientsClaim() 25 | -------------------------------------------------------------------------------- /examples/preact-router/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/preact-router/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'preact' 2 | import { App } from './app' 3 | 4 | render(, document.getElementById('app')!) 5 | -------------------------------------------------------------------------------- /examples/preact-router/src/pages/About.tsx: -------------------------------------------------------------------------------- 1 | function About() { 2 | // replaced dyanmicaly 3 | const date = '__DATE__' 4 | return ( 5 |
6 |
7 | /about 8 | {' '} 9 | route, built at: 10 | {' '} 11 | { date } 12 |
13 |
14 | Go Home 15 |
16 | ) 17 | } 18 | 19 | export default About 20 | -------------------------------------------------------------------------------- /examples/preact-router/src/pages/Home.css: -------------------------------------------------------------------------------- 1 | .Home { 2 | text-align: center; 3 | padding: 1em; 4 | margin: 0 auto; 5 | } 6 | 7 | .Home-built { 8 | margin: 1rem 0; 9 | } 10 | 11 | .Home-title { 12 | color: #03607c; 13 | font-size: 2.2rem; 14 | } 15 | 16 | button { 17 | font-size: calc(10px + 2vmin); 18 | } 19 | -------------------------------------------------------------------------------- /examples/preact-router/src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import { route } from 'preact-router' 2 | import { useState } from 'preact/hooks' 3 | import './Home.css' 4 | 5 | function Home() { 6 | const [count, setCount] = useState(0) 7 | 8 | const [name, setName] = useState('') 9 | 10 | // @ts-expect-error just ignore 11 | const handleChange = (event) => { 12 | setName(event.target.value || '') 13 | } 14 | 15 | // @ts-expect-error just ignore 16 | const handleSubmit = (event) => { 17 | event.preventDefault() 18 | if (name) 19 | route(`/hi/${name}`) 20 | } 21 | 22 | return ( 23 |
24 |

25 | 30 |

31 |
32 |
33 | 34 | 35 |
36 |
37 | About 38 |
39 |
40 | ) 41 | } 42 | 43 | export default Home 44 | -------------------------------------------------------------------------------- /examples/preact-router/src/pages/hi/[name].tsx: -------------------------------------------------------------------------------- 1 | import type { RouteProps } from 'preact-router' 2 | 3 | function Hi(props: RouteProps<{ name: string }>) { 4 | // replaced dyanmicaly 5 | const date = '__DATE__' 6 | return ( 7 |
8 |
9 | /hi 10 | {' '} 11 | route, built at: 12 | {' '} 13 | { date } 14 |
15 |

16 | Hi: 17 | { props.name } 18 |

19 |
20 | Go Home 21 |
22 | ) 23 | } 24 | 25 | export default Hi 26 | -------------------------------------------------------------------------------- /examples/preact-router/src/preact.d.ts: -------------------------------------------------------------------------------- 1 | import JSX = preact.JSX 2 | -------------------------------------------------------------------------------- /examples/preact-router/src/prompt-sw.ts: -------------------------------------------------------------------------------- 1 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 2 | import { NavigationRoute, registerRoute } from 'workbox-routing' 3 | 4 | declare let self: ServiceWorkerGlobalScope 5 | 6 | self.addEventListener('message', (event) => { 7 | if (event.data && event.data.type === 'SKIP_WAITING') 8 | self.skipWaiting() 9 | }) 10 | 11 | // self.__WB_MANIFEST is default injection point 12 | precacheAndRoute(self.__WB_MANIFEST) 13 | 14 | // clean old assets 15 | cleanupOutdatedCaches() 16 | 17 | // to allow work offline 18 | registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'))) 19 | -------------------------------------------------------------------------------- /examples/preact-router/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | -------------------------------------------------------------------------------- /examples/preact-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "preact", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "useDefineForClassFields": true, 8 | "module": "ESNext", 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "allowImportingTsExtensions": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noEmit": true, 21 | "isolatedModules": true, 22 | "skipLibCheck": true 23 | }, 24 | "references": [{ "path": "./tsconfig.node.json" }], 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /examples/preact-router/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/preact-router/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/react-router/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | This example relies on [https-localhost](https://github.com/daquinoaldo/https-localhost) to serve the dist files on https://localhost/. Please refer to it's docs for the steps to setup your local environment. 4 | 5 | ```bash 6 | npm run start 7 | ``` 8 | 9 | Open up https://localhost/, then restart the server, you will see a notification ask you to restart reload the offline content. 10 | -------------------------------------------------------------------------------- /examples/react-router/client-test/offline.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | const injectManifest = process.env.SW === 'true' 4 | const swName = `${injectManifest ? 'claims-sw.js' : 'sw.js'}` 5 | 6 | test('React: Test offline', 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 | 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 any 17 | return registration.active?.scriptURL 18 | }) 19 | expect(offlineSwURL).toBe(`http://localhost:4173/${swName}`) 20 | await context.setOffline(true) 21 | const aboutAnchor = offlinePage.getByRole('link', { name: 'About' }) 22 | expect(await aboutAnchor.getAttribute('href')).toBe('/about') 23 | await aboutAnchor.click({ noWaitAfter: false }) 24 | const url = await offlinePage.evaluate(async () => { 25 | await new Promise(resolve => setTimeout(resolve, 3000)) 26 | return location.href 27 | }) 28 | expect(url).toBe('http://localhost:4173/about') 29 | expect(offlinePage.locator('a').getByText('Go Home')).toBeTruthy() 30 | await offlinePage.reload({ waitUntil: 'load' }) 31 | expect(offlinePage.url()).toBe('http://localhost:4173/about') 32 | expect(offlinePage.locator('a').getByText('Go Home')).toBeTruthy() 33 | // Dispose context once it's no longer needed. 34 | await context.close() 35 | }) 36 | -------------------------------------------------------------------------------- /examples/react-router/client-test/sw.spec.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { expect, test } from '@playwright/test' 3 | 4 | const injectManifest = process.env.SW === 'true' 5 | const swName = `${injectManifest ? 'claims-sw.js' : 'sw.js'}` 6 | 7 | test('React: The service worker is registered and cache storage is present', async ({ page }) => { 8 | await page.goto('/') 9 | 10 | const swURL = await page.evaluate(async () => { 11 | const registration = await Promise.race([ 12 | navigator.serviceWorker.ready, 13 | new Promise((_resolve, reject) => setTimeout(() => reject(new Error('Service worker registration failed: time out')), 10000)), 14 | ]) 15 | // @ts-expect-error registration is of type any 16 | return registration.active?.scriptURL 17 | }) 18 | expect(swURL).toBe(`http://localhost:4173/${swName}`) 19 | 20 | const cacheContents = await page.evaluate(async () => { 21 | const cacheState: Record> = {} 22 | for (const cacheName of await caches.keys()) { 23 | const cache = await caches.open(cacheName) 24 | cacheState[cacheName] = (await cache.keys()).map(req => req.url) 25 | } 26 | return cacheState 27 | }) 28 | 29 | expect(Object.keys(cacheContents).length).toEqual(1) 30 | 31 | const key = 'workbox-precache-v2-http://localhost:4173/' 32 | 33 | expect(Object.keys(cacheContents)[0]).toEqual(key) 34 | 35 | const urls = cacheContents[key].map(url => url.slice('http://localhost:4173/'.length)) 36 | 37 | /* 38 | 'http://localhost:4173/index.html?__WB_REVISION__=073370aa3804305a787b01180cd6b8aa', 39 | 'http://localhost:4173/manifest.webmanifest?__WB_REVISION__=27df2fa4f35d014b42361148a2207da3' 40 | */ 41 | expect(urls.some(url => url.startsWith('manifest.webmanifest?__WB_REVISION__='))).toEqual(true) 42 | expect(urls.some(url => url.startsWith('index.html?__WB_REVISION__='))).toEqual(true) 43 | expect(urls.some(url => url.startsWith(`${swName}`))).toEqual(false) 44 | }) 45 | -------------------------------------------------------------------------------- /examples/react-router/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Vite App 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/react-router/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/react-router/public/pwa-192x192.png -------------------------------------------------------------------------------- /examples/react-router/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/react-router/public/pwa-512x512.png -------------------------------------------------------------------------------- /examples/react-router/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | padding: 1em; 4 | margin: 0 auto; 5 | } 6 | 7 | .App-built { 8 | margin: 1rem 0; 9 | } 10 | 11 | .App-title { 12 | color: #03607c; 13 | font-size: 2.2rem; 14 | } 15 | 16 | button { 17 | font-size: calc(10px + 2vmin); 18 | } 19 | -------------------------------------------------------------------------------- /examples/react-router/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Outlet } from 'react-router-dom' 3 | import ReloadPrompt from './ReloadPrompt' 4 | import './App.css' 5 | 6 | function App() { 7 | // replaced dyanmicaly 8 | const date = '__DATE__' 9 | 10 | return ( 11 |
12 | PWA Logo 13 |

PWA React!

14 |
15 | Built at: 16 | {date} 17 |
18 | 19 | 20 |
21 | ) 22 | } 23 | 24 | export default App 25 | -------------------------------------------------------------------------------- /examples/react-router/src/ReloadPrompt.css: -------------------------------------------------------------------------------- 1 | .ReloadPrompt-container { 2 | padding: 0; 3 | margin: 0; 4 | width: 0; 5 | height: 0; 6 | } 7 | .ReloadPrompt-date { 8 | visibility: hidden; 9 | } 10 | .ReloadPrompt-toast { 11 | position: fixed; 12 | right: 0; 13 | bottom: 0; 14 | margin: 16px; 15 | padding: 12px; 16 | border: 1px solid #8885; 17 | border-radius: 4px; 18 | z-index: 1; 19 | text-align: left; 20 | box-shadow: 3px 4px 5px 0 #8885; 21 | background-color: white; 22 | } 23 | .ReloadPrompt-toast-message { 24 | margin-bottom: 8px; 25 | } 26 | .ReloadPrompt-toast-button { 27 | border: 1px solid #8885; 28 | outline: none; 29 | margin-right: 5px; 30 | border-radius: 2px; 31 | padding: 3px 10px; 32 | } 33 | -------------------------------------------------------------------------------- /examples/react-router/src/claims-sw.ts: -------------------------------------------------------------------------------- 1 | import { clientsClaim } from 'workbox-core' 2 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 3 | import { NavigationRoute, registerRoute } from 'workbox-routing' 4 | 5 | declare let self: ServiceWorkerGlobalScope 6 | 7 | // self.__WB_MANIFEST is default injection point 8 | precacheAndRoute(self.__WB_MANIFEST) 9 | 10 | // clean old assets 11 | cleanupOutdatedCaches() 12 | 13 | let allowlist: undefined | RegExp[] 14 | if (import.meta.env.DEV) 15 | allowlist = [/^\/$/] 16 | 17 | // to allow work offline 18 | registerRoute(new NavigationRoute( 19 | createHandlerBoundToURL('index.html'), 20 | { allowlist }, 21 | )) 22 | 23 | self.skipWaiting() 24 | clientsClaim() 25 | -------------------------------------------------------------------------------- /examples/react-router/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /examples/react-router/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { BrowserRouter, Route, Routes } from 'react-router-dom' 4 | import App from './App' 5 | import About from './pages/About' 6 | import Hi from './pages/hi/[name]' 7 | import Home from './pages/Home' 8 | import './index.css' 9 | 10 | createRoot(document.getElementById('app')!).render( 11 | 12 | 13 | }> 14 | } /> 15 | } /> 16 | 17 | } /> 18 | 19 | 20 | 21 | , 22 | ) 23 | -------------------------------------------------------------------------------- /examples/react-router/src/pages/About.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function About() { 4 | // replaced dyanmicaly 5 | const date = '__DATE__' 6 | return ( 7 |
8 |
9 | /about 10 | {' '} 11 | route, built at: 12 | {' '} 13 | { date } 14 |
15 |
16 | Go Home 17 |
18 | ) 19 | } 20 | 21 | export default About 22 | -------------------------------------------------------------------------------- /examples/react-router/src/pages/Home.css: -------------------------------------------------------------------------------- 1 | .Home { 2 | text-align: center; 3 | padding: 1em; 4 | margin: 0 auto; 5 | } 6 | 7 | .Home-built { 8 | margin: 1rem 0; 9 | } 10 | 11 | .Home-title { 12 | color: #03607c; 13 | font-size: 2.2rem; 14 | } 15 | 16 | button { 17 | font-size: calc(10px + 2vmin); 18 | } 19 | -------------------------------------------------------------------------------- /examples/react-router/src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useNavigate } from 'react-router' 3 | import './Home.css' 4 | 5 | function Home() { 6 | const [count, setCount] = useState(0) 7 | 8 | const [name, setName] = useState('') 9 | 10 | const router = useNavigate() 11 | 12 | // @ts-expect-error just ignore 13 | const handleChange = (event) => { 14 | setName(event.target.value || '') 15 | } 16 | 17 | // @ts-expect-error just ignore 18 | const handleSubmit = (event) => { 19 | event.preventDefault() 20 | if (name) 21 | router(`/hi/${name}`) 22 | } 23 | 24 | return ( 25 |
26 |

27 | 32 |

33 |
34 |
35 | 36 | 37 |
38 |
39 | About 40 |
41 |
42 | ) 43 | } 44 | 45 | export default Home 46 | -------------------------------------------------------------------------------- /examples/react-router/src/pages/hi/[name].tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useParams } from 'react-router' 3 | 4 | function Hi() { 5 | // replaced dyanmicaly 6 | const date = '__DATE__' 7 | const params = useParams() 8 | return ( 9 |
10 |
11 | /hi 12 | {' '} 13 | route, built at: 14 | {' '} 15 | { date } 16 |
17 |

18 | Hi: 19 | { params.name } 20 |

21 |
22 | Go Home 23 |
24 | ) 25 | } 26 | 27 | export default Hi 28 | -------------------------------------------------------------------------------- /examples/react-router/src/prompt-sw.ts: -------------------------------------------------------------------------------- 1 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 2 | import { NavigationRoute, registerRoute } from 'workbox-routing' 3 | 4 | declare let self: ServiceWorkerGlobalScope 5 | 6 | self.addEventListener('message', (event) => { 7 | if (event.data && event.data.type === 'SKIP_WAITING') 8 | self.skipWaiting() 9 | }) 10 | 11 | // self.__WB_MANIFEST is default injection point 12 | precacheAndRoute(self.__WB_MANIFEST) 13 | 14 | // clean old assets 15 | cleanupOutdatedCaches() 16 | 17 | // to allow work offline 18 | registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'))) 19 | -------------------------------------------------------------------------------- /examples/react-router/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | -------------------------------------------------------------------------------- /examples/react-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "jsx": "react-jsx", 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "useDefineForClassFields": true, 7 | "module": "ESNext", 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "allowImportingTsExtensions": true, 13 | 14 | /* Linting */ 15 | "strict": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noEmit": true, 20 | "isolatedModules": true, 21 | "skipLibCheck": true 22 | }, 23 | "references": [{ "path": "./tsconfig.node.json" }], 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /examples/react-router/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/react-router/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/solid-router/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | This example relies on [https-localhost](https://github.com/daquinoaldo/https-localhost) to serve the dist files on https://localhost/. Please refer to it's docs for the steps to setup your local environment. 4 | 5 | ```bash 6 | npm run start 7 | ``` 8 | 9 | Open up https://localhost/, then restart the server, you will see a notification ask you to restart reload the offline content. 10 | -------------------------------------------------------------------------------- /examples/solid-router/client-test/offline.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | const injectManifest = process.env.SW === 'true' 4 | const swName = `${injectManifest ? 'claims-sw.js' : 'sw.js'}` 5 | 6 | test('Solid: Test offline', 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 | 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 any 17 | return registration.active?.scriptURL 18 | }) 19 | expect(offlineSwURL).toBe(`http://localhost:4173/${swName}`) 20 | await context.setOffline(true) 21 | const aboutAnchor = offlinePage.getByRole('link', { name: 'About' }) 22 | expect(await aboutAnchor.getAttribute('href')).toBe('/about') 23 | await aboutAnchor.click({ noWaitAfter: false }) 24 | const url = await offlinePage.evaluate(async () => { 25 | await new Promise(resolve => setTimeout(resolve, 3000)) 26 | return location.href 27 | }) 28 | expect(url).toBe('http://localhost:4173/about') 29 | expect(offlinePage.locator('a').getByText('Go Home')).toBeTruthy() 30 | await offlinePage.reload({ waitUntil: 'load' }) 31 | expect(offlinePage.url()).toBe('http://localhost:4173/about') 32 | expect(offlinePage.locator('a').getByText('Go Home')).toBeTruthy() 33 | // Dispose context once it's no longer needed. 34 | await context.close() 35 | }) 36 | -------------------------------------------------------------------------------- /examples/solid-router/client-test/sw.spec.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { expect, test } from '@playwright/test' 3 | 4 | const injectManifest = process.env.SW === 'true' 5 | const swName = `${injectManifest ? 'claims-sw.js' : 'sw.js'}` 6 | 7 | test('Solid: The service worker is registered and cache storage is present', async ({ page }) => { 8 | await page.goto('/') 9 | 10 | const swURL = await page.evaluate(async () => { 11 | const registration = await Promise.race([ 12 | navigator.serviceWorker.ready, 13 | new Promise((_resolve, reject) => setTimeout(() => reject(new Error('Service worker registration failed: time out')), 10000)), 14 | ]) 15 | // @ts-expect-error registration is of type any 16 | return registration.active?.scriptURL 17 | }) 18 | expect(swURL).toBe(`http://localhost:4173/${swName}`) 19 | 20 | const cacheContents = await page.evaluate(async () => { 21 | const cacheState: Record> = {} 22 | for (const cacheName of await caches.keys()) { 23 | const cache = await caches.open(cacheName) 24 | cacheState[cacheName] = (await cache.keys()).map(req => req.url) 25 | } 26 | return cacheState 27 | }) 28 | 29 | expect(Object.keys(cacheContents).length).toEqual(1) 30 | 31 | const key = 'workbox-precache-v2-http://localhost:4173/' 32 | 33 | expect(Object.keys(cacheContents)[0]).toEqual(key) 34 | 35 | const urls = cacheContents[key].map(url => url.slice('http://localhost:4173/'.length)) 36 | 37 | /* 38 | 'http://localhost:4173/index.html?__WB_REVISION__=073370aa3804305a787b01180cd6b8aa', 39 | 'http://localhost:4173/manifest.webmanifest?__WB_REVISION__=27df2fa4f35d014b42361148a2207da3' 40 | */ 41 | expect(urls.some(url => url.startsWith('manifest.webmanifest?__WB_REVISION__='))).toEqual(true) 42 | expect(urls.some(url => url.startsWith('index.html?__WB_REVISION__='))).toEqual(true) 43 | expect(urls.some(url => url.startsWith(`${swName}`))).toEqual(false) 44 | }) 45 | -------------------------------------------------------------------------------- /examples/solid-router/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Vite App 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/solid-router/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/solid-router/public/pwa-192x192.png -------------------------------------------------------------------------------- /examples/solid-router/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/solid-router/public/pwa-512x512.png -------------------------------------------------------------------------------- /examples/solid-router/src/ReloadPrompt.module.css: -------------------------------------------------------------------------------- 1 | .Container { 2 | padding: 0; 3 | margin: 0; 4 | width: 0; 5 | height: 0; 6 | } 7 | .Toast { 8 | position: fixed; 9 | right: 0; 10 | bottom: 0; 11 | margin: 16px; 12 | padding: 12px; 13 | border: 1px solid #8885; 14 | border-radius: 4px; 15 | z-index: 1; 16 | text-align: left; 17 | box-shadow: 3px 4px 5px 0 #8885; 18 | background-color: white; 19 | } 20 | .ToastMessage { 21 | margin-bottom: 8px; 22 | } 23 | .ToastButton { 24 | border: 1px solid #8885; 25 | outline: none; 26 | margin-right: 5px; 27 | border-radius: 2px; 28 | padding: 3px 10px; 29 | } 30 | -------------------------------------------------------------------------------- /examples/solid-router/src/app.module.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | padding: 1em; 4 | margin: 0 auto; 5 | } 6 | .Built { 7 | margin: 1rem 0; 8 | } 9 | 10 | .Title { 11 | color: #03607c; 12 | font-size: 2.2rem; 13 | } 14 | 15 | button { 16 | font-size: calc(10px + 2vmin); 17 | } 18 | -------------------------------------------------------------------------------- /examples/solid-router/src/app.tsx: -------------------------------------------------------------------------------- 1 | import type { Component } from 'solid-js' 2 | import { useRoutes } from '@solidjs/router' 3 | import styles from './app.module.css' 4 | import ReloadPrompt from './ReloadPrompt' 5 | import { routes } from './routes' 6 | 7 | const App: Component = () => { 8 | // replaced dynamically 9 | const date = '__DATE__' 10 | 11 | const Route = useRoutes(routes) 12 | 13 | return ( 14 |
15 | PWA Logo 16 |

PWA SolidJS!

17 |
18 | Built at: 19 | {date} 20 |
21 | 22 | 23 |
24 | ) 25 | } 26 | 27 | export default App 28 | -------------------------------------------------------------------------------- /examples/solid-router/src/claims-sw.ts: -------------------------------------------------------------------------------- 1 | import { clientsClaim } from 'workbox-core' 2 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 3 | import { NavigationRoute, registerRoute } from 'workbox-routing' 4 | 5 | declare let self: ServiceWorkerGlobalScope 6 | 7 | // self.__WB_MANIFEST is default injection point 8 | precacheAndRoute(self.__WB_MANIFEST) 9 | 10 | // clean old assets 11 | cleanupOutdatedCaches() 12 | 13 | let allowlist: undefined | RegExp[] 14 | if (import.meta.env.DEV) 15 | allowlist = [/^\/$/] 16 | 17 | // to allow work offline 18 | registerRoute(new NavigationRoute( 19 | createHandlerBoundToURL('index.html'), 20 | { allowlist }, 21 | )) 22 | 23 | self.skipWaiting() 24 | clientsClaim() 25 | -------------------------------------------------------------------------------- /examples/solid-router/src/errors/404.tsx: -------------------------------------------------------------------------------- 1 | export default function NotFound() { 2 | return ( 3 |
4 |

404: Not Found

5 |

It's gone 😞

6 |
7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /examples/solid-router/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Router } from '@solidjs/router' 2 | import { render } from 'solid-js/web' 3 | import App from './app' 4 | 5 | render( 6 | () => ( 7 | 8 | 9 | 10 | ), 11 | document.getElementById('root') as HTMLElement, 12 | ) 13 | -------------------------------------------------------------------------------- /examples/solid-router/src/pages/about.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@solidjs/router' 2 | 3 | export default function About() { 4 | // replaced dyanmicaly 5 | const date = '__DATE__' 6 | 7 | return ( 8 |
9 |
10 | /about 11 | {' '} 12 | route, built at: 13 | {' '} 14 | { date } 15 |
16 |
17 | 18 | Go Home 19 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /examples/solid-router/src/pages/hi/[name].data.ts: -------------------------------------------------------------------------------- 1 | import type { RouteDataFunc } from '@solidjs/router' 2 | 3 | const HiData: RouteDataFunc = (args) => { 4 | const name = args.params.name || '' 5 | return { 6 | name: decodeURI(name), 7 | } 8 | } 9 | 10 | export default HiData 11 | -------------------------------------------------------------------------------- /examples/solid-router/src/pages/hi/[name].tsx: -------------------------------------------------------------------------------- 1 | import { Link, useRouteData } from '@solidjs/router' 2 | 3 | export default function Hi() { 4 | // replaced dyanmicaly 5 | const date = '__DATE__' 6 | const data: { name: string } = useRouteData() 7 | 8 | return ( 9 |
10 |
11 | /hi 12 | {' '} 13 | route, built at: 14 | {' '} 15 | { date } 16 |
17 |

18 | Hi: 19 | { data.name } 20 |

21 |
22 | 23 | Go Home 24 | 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /examples/solid-router/src/pages/home.module.css: -------------------------------------------------------------------------------- 1 | .Home { 2 | text-align: center; 3 | padding: 1em; 4 | margin: 0 auto; 5 | } 6 | button { 7 | font-size: calc(10px + 2vmin); 8 | } 9 | -------------------------------------------------------------------------------- /examples/solid-router/src/pages/home.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate } from '@solidjs/router' 2 | import { createSignal } from 'solid-js' 3 | import styles from './home.module.css' 4 | 5 | export default function Home() { 6 | const navigate = useNavigate() 7 | const [count, setCount] = createSignal(0) 8 | const [name, setName] = createSignal('') 9 | 10 | const handleChange = (event) => { 11 | setName(event.target.value || '') 12 | } 13 | 14 | const handleSubmit = (event) => { 15 | event.preventDefault() 16 | const submit = name() 17 | if (submit && submit.trim().length > 0) 18 | navigate(`/hi/${submit}`) 19 | } 20 | 21 | return ( 22 |
23 |

24 | 29 |

30 |
31 |
32 | 33 | 34 |
35 |
36 | About 37 |
38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /examples/solid-router/src/prompt-sw.ts: -------------------------------------------------------------------------------- 1 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 2 | import { NavigationRoute, registerRoute } from 'workbox-routing' 3 | 4 | declare let self: ServiceWorkerGlobalScope 5 | 6 | self.addEventListener('message', (event) => { 7 | if (event.data && event.data.type === 'SKIP_WAITING') 8 | self.skipWaiting() 9 | }) 10 | 11 | // self.__WB_MANIFEST is default injection point 12 | precacheAndRoute(self.__WB_MANIFEST) 13 | 14 | // clean old assets 15 | cleanupOutdatedCaches() 16 | 17 | // to allow work offline 18 | registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'))) 19 | -------------------------------------------------------------------------------- /examples/solid-router/src/routes.ts: -------------------------------------------------------------------------------- 1 | import type { RouteDefinition } from '@solidjs/router' 2 | import { lazy } from 'solid-js' 3 | 4 | import HiData from './pages/hi/[name].data' 5 | import Home from './pages/home' 6 | 7 | export const routes: RouteDefinition[] = [ 8 | { 9 | path: '/', 10 | component: Home, 11 | }, 12 | { 13 | path: '/about', 14 | component: lazy(() => import('./pages/about')), 15 | }, 16 | { 17 | path: '/hi/:name', 18 | component: lazy(() => import('./pages/hi/[name]')), 19 | data: HiData, 20 | }, 21 | { 22 | path: '**', 23 | component: lazy(() => import('./errors/404')), 24 | }, 25 | ] 26 | -------------------------------------------------------------------------------- /examples/solid-router/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | -------------------------------------------------------------------------------- /examples/solid-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "jsx": "preserve", 5 | "jsxImportSource": "solid-js", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "useDefineForClassFields": true, 8 | "module": "ESNext", 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "allowImportingTsExtensions": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noEmit": true, 21 | "isolatedModules": true, 22 | "skipLibCheck": true 23 | }, 24 | "references": [{ "path": "./tsconfig.node.json" }], 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /examples/solid-router/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/solid-router/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/svelte-routify/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | This example relies on [https-localhost](https://github.com/daquinoaldo/https-localhost) to serve the dist files on https://localhost/. Please refer to it's docs for the steps to setup your local environment. 4 | 5 | ```bash 6 | npm run start 7 | ``` 8 | 9 | Open up https://localhost/, then restart the server, you will see a notification ask you to restart reload the offline content. 10 | -------------------------------------------------------------------------------- /examples/svelte-routify/client-test/offline.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | const injectManifest = process.env.SW === 'true' 4 | const swName = `${injectManifest ? 'claims-sw.js' : 'sw.js'}` 5 | 6 | test('Svelte: Test offline', 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 | 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 any 17 | return registration.active?.scriptURL 18 | }) 19 | expect(offlineSwURL).toBe(`http://localhost:4173/${swName}`) 20 | await context.setOffline(true) 21 | const aboutAnchor = offlinePage.getByRole('link', { name: 'About' }) 22 | expect(await aboutAnchor.getAttribute('href')).toBe('/about') 23 | await aboutAnchor.click({ noWaitAfter: false }) 24 | const url = await offlinePage.evaluate(async () => { 25 | await new Promise(resolve => setTimeout(resolve, 3000)) 26 | return location.href 27 | }) 28 | expect(url).toBe('http://localhost:4173/about') 29 | expect(offlinePage.locator('a').getByText('Go Home')).toBeTruthy() 30 | await offlinePage.reload({ waitUntil: 'load' }) 31 | expect(offlinePage.url()).toBe('http://localhost:4173/about') 32 | expect(offlinePage.locator('a').getByText('Go Home')).toBeTruthy() 33 | // Dispose context once it's no longer needed. 34 | await context.close() 35 | }) 36 | -------------------------------------------------------------------------------- /examples/svelte-routify/client-test/sw.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | const injectManifest = process.env.SW === 'true' 4 | const swName = `${injectManifest ? 'claims-sw.js' : 'sw.js'}` 5 | 6 | test('Svelte: 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 | navigator.serviceWorker.ready, 12 | new Promise((_resolve, reject) => setTimeout(() => reject(new Error('Service worker registration failed: time out')), 10000)), 13 | ]) 14 | // @ts-expect-error registration is of type any 15 | return registration.active?.scriptURL 16 | }) 17 | expect(swURL).toBe(`http://localhost:4173/${swName}`) 18 | 19 | const cacheContents = await page.evaluate(async () => { 20 | const cacheState: Record> = {} 21 | for (const cacheName of await caches.keys()) { 22 | const cache = await caches.open(cacheName) 23 | cacheState[cacheName] = (await cache.keys()).map(req => req.url) 24 | } 25 | return cacheState 26 | }) 27 | 28 | expect(Object.keys(cacheContents).length).toEqual(1) 29 | 30 | const key = 'workbox-precache-v2-http://localhost:4173/' 31 | 32 | expect(Object.keys(cacheContents)[0]).toEqual(key) 33 | 34 | const urls = cacheContents[key].map(url => url.slice('http://localhost:4173/'.length)) 35 | 36 | /* 37 | 'http://localhost:4173/index.html?__WB_REVISION__=073370aa3804305a787b01180cd6b8aa', 38 | 'http://localhost:4173/manifest.webmanifest?__WB_REVISION__=27df2fa4f35d014b42361148a2207da3' 39 | */ 40 | expect(urls.some(url => url.startsWith('manifest.webmanifest?__WB_REVISION__='))).toEqual(true) 41 | expect(urls.some(url => url.startsWith('index.html?__WB_REVISION__='))).toEqual(true) 42 | expect(urls.some(url => url.startsWith(`${swName}`))).toEqual(false) 43 | }) 44 | -------------------------------------------------------------------------------- /examples/svelte-routify/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Vite App 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/svelte-routify/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/svelte-routify/public/pwa-192x192.png -------------------------------------------------------------------------------- /examples/svelte-routify/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/svelte-routify/public/pwa-512x512.png -------------------------------------------------------------------------------- /examples/svelte-routify/src/App.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/svelte-routify/src/claims-sw.ts: -------------------------------------------------------------------------------- 1 | import { clientsClaim } from 'workbox-core' 2 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 3 | import { NavigationRoute, registerRoute } from 'workbox-routing' 4 | 5 | declare let self: ServiceWorkerGlobalScope 6 | 7 | // self.__WB_MANIFEST is default injection point 8 | precacheAndRoute(self.__WB_MANIFEST) 9 | 10 | // clean old assets 11 | cleanupOutdatedCaches() 12 | 13 | let allowlist: undefined | RegExp[] 14 | if (import.meta.env.DEV) 15 | allowlist = [/^\/$/] 16 | 17 | // to allow work offline 18 | registerRoute(new NavigationRoute( 19 | createHandlerBoundToURL('index.html'), 20 | { allowlist }, 21 | )) 22 | 23 | self.skipWaiting() 24 | clientsClaim() 25 | -------------------------------------------------------------------------------- /examples/svelte-routify/src/lib/Counter.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /examples/svelte-routify/src/lib/Go.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 |
26 | 27 | 30 |
31 | 32 | 33 | 60 | -------------------------------------------------------------------------------- /examples/svelte-routify/src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.svelte' 2 | 3 | const app = new App({ 4 | target: document.getElementById('app'), 5 | }) 6 | 7 | export default app 8 | -------------------------------------------------------------------------------- /examples/svelte-routify/src/pages/_fallback.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | 18 |
19 |
404
20 |
Page not found. 21 | 22 | Go back 23 |
24 |
25 | -------------------------------------------------------------------------------- /examples/svelte-routify/src/pages/_layout.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | PWA Logo 8 |

Svelte PWA!

9 | 10 |
Built at: { date }
11 | 12 | 13 | 14 |
15 | 16 | 47 | -------------------------------------------------------------------------------- /examples/svelte-routify/src/pages/about.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
/about route, built at: { date }
9 |
10 | Go Home 11 | -------------------------------------------------------------------------------- /examples/svelte-routify/src/pages/hi/[name].svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
/hi route, built at: { date }
11 | 12 |

13 | Hi: { name } 14 |

15 |
16 | Go Home 17 | -------------------------------------------------------------------------------- /examples/svelte-routify/src/pages/index.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 |
13 | 14 | About 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/svelte-routify/src/prompt-sw.ts: -------------------------------------------------------------------------------- 1 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 2 | import { NavigationRoute, registerRoute } from 'workbox-routing' 3 | 4 | declare let self: ServiceWorkerGlobalScope 5 | 6 | self.addEventListener('message', (event) => { 7 | if (event.data && event.data.type === 'SKIP_WAITING') 8 | self.skipWaiting() 9 | }) 10 | 11 | // self.__WB_MANIFEST is default injection point 12 | precacheAndRoute(self.__WB_MANIFEST) 13 | 14 | // clean old assets 15 | cleanupOutdatedCaches() 16 | 17 | // to allow work offline 18 | registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'))) 19 | -------------------------------------------------------------------------------- /examples/svelte-routify/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | -------------------------------------------------------------------------------- /examples/svelte-routify/svelte.config.js: -------------------------------------------------------------------------------- 1 | import sveltePreprocess from 'svelte-preprocess' 2 | 3 | export default { 4 | // Consult https://github.com/sveltejs/svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: sveltePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /examples/svelte-routify/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | 9 | /** 10 | * Typecheck JS in `.svelte` and `.js` files by default. 11 | * Disable checkJs if you'd like to use dynamic types in JS. 12 | * Note that setting allowJs false does not prevent the use 13 | * of JS in `.svelte` files. 14 | */ 15 | "allowJs": true, 16 | "checkJs": true, 17 | "isolatedModules": true 18 | }, 19 | "references": [{ "path": "./tsconfig.node.json" }], 20 | "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/svelte-routify/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "skipLibCheck": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/svelte-routify/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | -------------------------------------------------------------------------------- /examples/svelte-routify/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-pwa/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 5 | plugins: ['svelte3', '@typescript-eslint'], 6 | ignorePatterns: ['*.cjs'], 7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 8 | settings: { 9 | 'svelte3/typescript': () => require('typescript'), 10 | }, 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2019, 14 | }, 15 | env: { 16 | browser: true, 17 | es2017: true, 18 | node: true, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte); 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 | npm init svelte@next 12 | 13 | # create a new project in my-app 14 | npm init svelte@next my-app 15 | ``` 16 | 17 | > Note: the `@next` is temporary 18 | 19 | ## Developing 20 | 21 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 22 | 23 | ```bash 24 | npm run dev 25 | 26 | # or start the server and open the app in a new browser tab 27 | npm run dev -- --open 28 | ``` 29 | 30 | ## Building 31 | 32 | Before creating a production version of your app, install an [adapter](https://kit.svelte.dev/docs#adapters) for your target environment. Then: 33 | 34 | ```bash 35 | npm run build 36 | ``` 37 | 38 | > You can preview the built app with `npm run preview`, regardless of whether you installed an adapter. This should _not_ be used to serve your app in production. 39 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/pwa.js: -------------------------------------------------------------------------------- 1 | import { copyFileSync } from 'node:fs' 2 | import replace from '@rollup/plugin-replace' 3 | import minimist from 'minimist' 4 | import { resolveConfig } from 'vite' 5 | import { VitePWA } from 'vite-plugin-pwa' 6 | 7 | const args = minimist(process.argv.slice(2)) 8 | 9 | process.env.CLAIMS = `${args.CLAIMS === 'true'}` 10 | process.env.RELOAD_SW = `${args.RELOAD_SW === 'true'}` 11 | process.env.SW = `${args.SW === 'true'}` 12 | 13 | const webmanifestDestinations = [ 14 | './.svelte-kit/output/client/', 15 | './build/', 16 | ] 17 | 18 | const swDestinations = [ 19 | './build/', 20 | ] 21 | 22 | async function buildPwa() { 23 | const { pwaConfiguration, replaceOptions } = await import('./pwa-configuration.js') 24 | const config = await resolveConfig( 25 | { 26 | plugins: [ 27 | VitePWA(pwaConfiguration), 28 | replace(replaceOptions), 29 | ], 30 | }, 31 | 'build', 32 | 'production', 33 | ) 34 | // when `vite-plugin-pwa` is presented, use it to regenerate SW after rendering 35 | const pwaPlugin = config.plugins.find(i => i.name === 'vite-plugin-pwa')?.api 36 | if (pwaPlugin?.generateSW) { 37 | console.log('Generating PWA...') 38 | await pwaPlugin.generateSW() 39 | webmanifestDestinations.forEach((d) => { 40 | copyFileSync('./.svelte-kit/output/client/_app/immutable/manifest.webmanifest', `${d}/manifest.webmanifest`) 41 | }) 42 | // don't copy workbox, svelte kit will copy it 43 | if (pwaConfiguration.strategies === 'injectManifest') { 44 | const destName = pwaConfiguration.registerType === 'autoUpdate' ? 'claims-sw.js' : 'prompt-sw.js' 45 | const name = `./.svelte-kit/output/client/${destName}` 46 | swDestinations.forEach((d) => { 47 | copyFileSync(name, `${d}/${destName}`) 48 | }) 49 | } 50 | else { 51 | swDestinations.forEach((d) => { 52 | copyFileSync('./.svelte-kit/output/client/sw.js', `${d}/sw.js`) 53 | }) 54 | } 55 | console.log('Generation of PWA complete') 56 | } 57 | } 58 | 59 | buildPwa() 60 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | %sveltekit.head% 12 | 13 | 14 |
%sveltekit.body%
15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/src/claims-sw.ts: -------------------------------------------------------------------------------- 1 | import { clientsClaim } from 'workbox-core' 2 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 3 | import { NavigationRoute, registerRoute } from 'workbox-routing' 4 | 5 | declare let self: ServiceWorkerGlobalScope 6 | 7 | // self.__WB_MANIFEST is default injection point 8 | precacheAndRoute(self.__WB_MANIFEST) 9 | 10 | // clean old assets 11 | cleanupOutdatedCaches() 12 | 13 | let allowlist: undefined | RegExp[] 14 | if (import.meta.env.DEV) 15 | allowlist = [/^\/$/] 16 | 17 | // to allow work offline 18 | registerRoute(new NavigationRoute( 19 | createHandlerBoundToURL('index.html'), 20 | { allowlist }, 21 | )) 22 | 23 | self.skipWaiting() 24 | clientsClaim() 25 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/src/lib/components/Counter.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/src/lib/components/Go.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | 22 | 25 |
26 | 27 | 52 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/src/prompt-sw.ts: -------------------------------------------------------------------------------- 1 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 2 | import { NavigationRoute, registerRoute } from 'workbox-routing' 3 | 4 | declare let self: ServiceWorkerGlobalScope 5 | 6 | self.addEventListener('message', (event) => { 7 | if (event.data && event.data.type === 'SKIP_WAITING') 8 | self.skipWaiting() 9 | }) 10 | 11 | // self.__WB_MANIFEST is default injection point 12 | precacheAndRoute(self.__WB_MANIFEST) 13 | 14 | // clean old assets 15 | cleanupOutdatedCaches() 16 | 17 | // to allow work offline 18 | registerRoute(new NavigationRoute(createHandlerBoundToURL('/'))) 19 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/src/routes/__layout.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | {#if enableManifest} 19 | 20 | {/if} 21 | 22 | 23 |
24 | PWA Logo 25 |

SvelteKit PWA!

26 | 27 |
Built at: { date }
28 | 29 | 30 | 31 |
32 | 33 | {#if ReloadPrompt} 34 | 35 | {/if} 36 | 37 | 62 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/src/routes/about.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
/about route, built at: { date }
7 |
8 | Go Home 9 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/src/routes/hi/[name].svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
/hi route, built at: { date }
11 | 12 |

13 | Hi: { name } 14 |

15 |
16 | Go Home 17 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/src/routes/index.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 |
9 | 10 | About
11 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/static/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/sveltekit-pwa/static/pwa-192x192.png -------------------------------------------------------------------------------- /examples/sveltekit-pwa/static/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/sveltekit-pwa/static/pwa-512x512.png -------------------------------------------------------------------------------- /examples/sveltekit-pwa/svelte.config.js: -------------------------------------------------------------------------------- 1 | import replace from '@rollup/plugin-replace' 2 | import adapter from '@sveltejs/adapter-static' 3 | import preprocess from 'svelte-preprocess' 4 | import { VitePWA } from 'vite-plugin-pwa' 5 | import { pwaConfiguration, replaceOptions } from './pwa-configuration.js' 6 | 7 | /** @type {import('@sveltejs/kit').Config} */ 8 | const config = { 9 | // Consult https://github.com/sveltejs/svelte-preprocess 10 | // for more information about preprocessors 11 | preprocess: preprocess(), 12 | 13 | kit: { 14 | adapter: adapter(), 15 | 16 | prerender: { 17 | default: true, 18 | }, 19 | 20 | vite: { 21 | plugins: [ 22 | VitePWA(pwaConfiguration), 23 | replace(replaceOptions), 24 | ], 25 | }, 26 | }, 27 | } 28 | 29 | export default config 30 | -------------------------------------------------------------------------------- /examples/sveltekit-pwa/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /examples/vanilla-js-custom-sw/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Vite App 12 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/vanilla-js-custom-sw/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-js-custom-sw", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "build-on-root": "vite build --outDir=../../dist-examples-root", 10 | "preview": "vite preview" 11 | }, 12 | "devDependencies": { 13 | "vite": "^5.0.0", 14 | "vite-plugin-pwa": "workspace:*", 15 | "workbox-cacheable-response": "^7.3.0", 16 | "workbox-core": "^7.3.0", 17 | "workbox-expiration": "^7.3.0", 18 | "workbox-routing": "^7.3.0", 19 | "workbox-strategies": "^7.3.0", 20 | "workbox-window": "^7.3.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/vanilla-js-custom-sw/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/vanilla-js-custom-sw/public/pwa-192x192.png -------------------------------------------------------------------------------- /examples/vanilla-js-custom-sw/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/vanilla-js-custom-sw/public/pwa-512x512.png -------------------------------------------------------------------------------- /examples/vanilla-js-custom-sw/service-worker/sw.js: -------------------------------------------------------------------------------- 1 | import { clientsClaim } from 'workbox-core' 2 | /// 3 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 4 | import { NavigationRoute, registerRoute } from 'workbox-routing' 5 | 6 | // self.__WB_MANIFEST is default injection point 7 | precacheAndRoute(self.__WB_MANIFEST) 8 | 9 | // clean old assets 10 | cleanupOutdatedCaches() 11 | 12 | /** @type {RegExp[] | undefined} */ 13 | let allowlist 14 | if (import.meta.env.DEV) 15 | allowlist = [/^\/$/] 16 | 17 | // to allow work offline 18 | registerRoute(new NavigationRoute( 19 | createHandlerBoundToURL('index.html'), 20 | { allowlist }, 21 | )) 22 | 23 | self.skipWaiting() 24 | clientsClaim() 25 | -------------------------------------------------------------------------------- /examples/vanilla-js-custom-sw/src/main.js: -------------------------------------------------------------------------------- 1 | import { pwaInfo } from 'virtual:pwa-info' 2 | import { registerSW } from 'virtual:pwa-register' 3 | 4 | // eslint-disable-next-line no-undef 5 | const date = __DATE__ 6 | 7 | console.log(pwaInfo) 8 | 9 | const app = document.querySelector('#app') 10 | 11 | app.innerHTML = ` 12 |
13 | PWA Logo 14 |

Vite + Vanilla JavaScript Custom Service Worker

15 |
16 |

${date}

17 |
18 |
19 | ` 20 | 21 | registerSW({ 22 | immediate: true, 23 | onNeedRefresh() { 24 | console.log('onNeedRefresh message should not appear') 25 | }, 26 | onOfflineReady() { 27 | console.log('onOfflineReady message should not appear') 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /examples/vanilla-js-custom-sw/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { VitePWA } from 'vite-plugin-pwa' 3 | 4 | export default defineConfig({ 5 | mode: 'development', 6 | logLevel: 'info', 7 | define: { 8 | __DATE__: `'${new Date().toISOString()}'`, 9 | }, 10 | build: { 11 | sourcemap: true, 12 | }, 13 | plugins: [ 14 | VitePWA({ 15 | mode: 'development', 16 | base: '/', 17 | strategies: 'injectManifest', 18 | srcDir: './service-worker', 19 | filename: 'sw.js', 20 | includeAssets: ['favicon.svg'], 21 | injectRegister: false, 22 | injectManifest: { 23 | minify: false, 24 | enableWorkboxModulesLogs: true, 25 | }, 26 | manifest: { 27 | name: 'PWA Router', 28 | short_name: 'PWA Router', 29 | theme_color: '#ffffff', 30 | icons: [ 31 | { 32 | src: 'pwa-192x192.png', // <== don't add slash, for testing 33 | sizes: '192x192', 34 | type: 'image/png', 35 | }, 36 | { 37 | src: '/pwa-512x512.png', // <== don't remove slash, for testing 38 | sizes: '512x512', 39 | type: 'image/png', 40 | }, 41 | { 42 | src: 'pwa-512x512.png', // <== don't add slash, for testing 43 | sizes: '512x512', 44 | type: 'image/png', 45 | purpose: 'any maskable', 46 | }, 47 | ], 48 | }, 49 | devOptions: { 50 | enabled: true, 51 | /* when using generateSW the PWA plugin will switch to classic */ 52 | navigateFallback: 'index.html', 53 | suppressWarnings: true, 54 | type: 'module', 55 | }, 56 | }), 57 | ], 58 | }) 59 | -------------------------------------------------------------------------------- /examples/vanilla-ts-dev-options/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | This example relies on [https-localhost](https://github.com/daquinoaldo/https-localhost) to serve the dist files on https://localhost/. Please refer to it's docs for the steps to setup your local environment. 4 | 5 | ```bash 6 | npm run start 7 | ``` 8 | 9 | Open up https://localhost/, then restart the server, you will see a notification ask you to restart reload the offline content. 10 | -------------------------------------------------------------------------------- /examples/vanilla-ts-dev-options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Vite App 12 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/vanilla-ts-dev-options/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-ts-dev-options", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev-auto": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW_DEV=true SW_INLINE=auto vite --force", 8 | "dev-inline": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW_DEV=true SW_INLINE=inline vite --force", 9 | "dev-script": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW_DEV=true SW_INLINE=script vite --force", 10 | "dev-script-defer": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW_DEV=true SW_INLINE=script-defer vite --force", 11 | "dev-auto-destroy": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW_DEV=true SW_DESTROY=true SW_INLINE=auto vite --force", 12 | "dev-inline-destroy": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW_DEV=true SW_DESTROY=true SW_INLINE=inline vite --force", 13 | "dev-script-destroy": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW_DEV=true SW_DESTROY=true SW_INLINE=script vite --force", 14 | "dev-script-defer-destroy": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW_DEV=true SW_DESTROY=true SW_INLINE=script-defer vite --force", 15 | "build": "DEBUG=vite-plugin-pwa vite build", 16 | "build-auto": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW_DEV=true SW_INLINE=auto vite build --force", 17 | "build-inline": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW_DEV=true SW_INLINE=inline vite build --force", 18 | "build-script": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW_DEV=true SW_INLINE=script vite build --force", 19 | "build-script-defer": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW_DEV=true SW_INLINE=script-defer vite build --force", 20 | "serve": "serve dist" 21 | }, 22 | "devDependencies": { 23 | "rimraf": "^5.0.5", 24 | "typescript": "^5.3.3", 25 | "vite": "^5.0.10", 26 | "vite-plugin-pwa": "workspace:*" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/vanilla-ts-dev-options/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/vanilla-ts-dev-options/public/pwa-192x192.png -------------------------------------------------------------------------------- /examples/vanilla-ts-dev-options/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/vanilla-ts-dev-options/public/pwa-512x512.png -------------------------------------------------------------------------------- /examples/vanilla-ts-dev-options/src/main.ts: -------------------------------------------------------------------------------- 1 | import { pwaInfo } from 'virtual:pwa-info' 2 | 3 | console.log(pwaInfo) 4 | 5 | document.querySelector('#app')!.innerHTML = ` 6 |
7 | PWA Logo 8 |

Vite + TypeScript

9 |

Testing SW with injectRegister=auto,inline,script,script-defer

10 |
11 | ` 12 | -------------------------------------------------------------------------------- /examples/vanilla-ts-dev-options/src/vite-env.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /examples/vanilla-ts-dev-options/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "types": ["vite/client", "vite-plugin-pwa/vanillajs", "vite-plugin-pwa/info"], 12 | "allowImportingTsExtensions": true, 13 | 14 | /* Linting */ 15 | "strict": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noEmit": true, 20 | "isolatedModules": true, 21 | "skipLibCheck": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/vanilla-ts-dev-options/vite.config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { defineConfig } from 'vite' 3 | import { VitePWA } from 'vite-plugin-pwa' 4 | 5 | const injectRegister = (process.env.SW_INLINE ?? 'auto') as 'inline' | 'auto' | 'script' | 'script-defer' 6 | const selfDestroying = process.env.SW_DESTROY === 'true' 7 | 8 | export default defineConfig({ 9 | build: { 10 | sourcemap: process.env.SOURCE_MAP === 'true', 11 | }, 12 | plugins: [ 13 | VitePWA({ 14 | mode: 'development', 15 | base: '/', 16 | /* buildBase: '/test-build-base/', */ 17 | includeAssets: ['favicon.svg'], 18 | injectRegister, 19 | selfDestroying, 20 | manifest: { 21 | name: 'PWA Router', 22 | short_name: 'PWA Router', 23 | theme_color: '#ffffff', 24 | icons: [ 25 | { 26 | src: 'pwa-192x192.png', // <== don't add slash, for testing 27 | sizes: '192x192', 28 | type: 'image/png', 29 | }, 30 | { 31 | src: '/pwa-512x512.png', // <== don't remove slash, for testing 32 | sizes: '512x512', 33 | type: 'image/png', 34 | }, 35 | { 36 | src: 'pwa-512x512.png', // <== don't add slash, for testing 37 | sizes: '512x512', 38 | type: 'image/png', 39 | purpose: 'any maskable', 40 | }, 41 | ], 42 | }, 43 | devOptions: { 44 | enabled: process.env.SW_DEV === 'true', 45 | /* when using generateSW the PWA plugin will switch to classic */ 46 | navigateFallback: 'index.html', 47 | suppressWarnings: true, 48 | }, 49 | }), 50 | ], 51 | }) 52 | -------------------------------------------------------------------------------- /examples/vanilla-ts-no-ip/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | This example relies on [https-localhost](https://github.com/daquinoaldo/https-localhost) to serve the dist files on https://localhost/. Please refer to it's docs for the steps to setup your local environment. 4 | 5 | ```bash 6 | npm run start-sw 7 | ``` 8 | 9 | Open up https://localhost/ to check the PWA is working. 10 | -------------------------------------------------------------------------------- /examples/vanilla-ts-no-ip/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Vite App 12 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/vanilla-ts-no-ip/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-ts-no-ip", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW_DEV=true vite --force", 8 | "dev-custom": "rimraf dev-dist && DEBUG=vite-plugin-pwa SW=true SW_DEV=true vite --force", 9 | "run-build-sw": "DEBUG=vite-plugin-pwa BASE_URL=/ SOURCE_MAP=true vite build", 10 | "run-build-custom-sw": "DEBUG=vite-plugin-pwa BASE_URL=/ SOURCE_MAP=true SW=true vite build", 11 | "start-sw": "nr run-build-sw && nr serve", 12 | "serve": "serve dist", 13 | "start-preview": "vite preview --port=4173", 14 | "test-custom-sw": "nr run-build-custom-sw && SW=true vitest run && SW=true playwright test", 15 | "test-generate-sw": "nr run-build-sw && vitest run && playwright test", 16 | "test": "nr test-generate-sw && nr test-custom-sw" 17 | }, 18 | "devDependencies": { 19 | "lodash-es": "^4.17.21", 20 | "rimraf": "^5.0.5", 21 | "typescript": "^5.3.3", 22 | "vite": "^5.0.10", 23 | "vite-plugin-pwa": "workspace:*", 24 | "workbox-cacheable-response": "^7.1.0", 25 | "workbox-core": "^7.3.0", 26 | "workbox-expiration": "^7.3.0", 27 | "workbox-routing": "^7.3.0", 28 | "workbox-strategies": "^7.3.0", 29 | "workbox-window": "^7.3.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/vanilla-ts-no-ip/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/vanilla-ts-no-ip/public/pwa-192x192.png -------------------------------------------------------------------------------- /examples/vanilla-ts-no-ip/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/vanilla-ts-no-ip/public/pwa-512x512.png -------------------------------------------------------------------------------- /examples/vanilla-ts-no-ip/src/main.ts: -------------------------------------------------------------------------------- 1 | import { pwaInfo } from 'virtual:pwa-info' 2 | import { registerSW } from 'virtual:pwa-register' 3 | 4 | const date = __DATE__ 5 | 6 | console.log(pwaInfo) 7 | 8 | const app = document.querySelector('#app')! 9 | 10 | app.innerHTML = ` 11 |
12 | PWA Logo 13 |

Vite + TypeScript

14 |

Testing SW without Injection Point (self.__WB_MANIFEST)

15 |
16 |

${date}

17 |
18 |
19 | ` 20 | 21 | registerSW({ 22 | immediate: true, 23 | onNeedRefresh() { 24 | console.log('onNeedRefresh message should not appear') 25 | }, 26 | onOfflineReady() { 27 | console.log('onOfflineReady message should not appear') 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /examples/vanilla-ts-no-ip/src/vite-env.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | declare const __DATE__: string 6 | -------------------------------------------------------------------------------- /examples/vanilla-ts-no-ip/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ES2020", "DOM", "DOM.Iterable", "WebWorker"], 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "resolveJsonModule": true, 11 | "types": ["vite/client", "vite-plugin-pwa/vanillajs", "vite-plugin-pwa/info"], 12 | "allowImportingTsExtensions": true, 13 | 14 | /* Linting */ 15 | "strict": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noEmit": true, 20 | "isolatedModules": true, 21 | "skipLibCheck": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/vanilla-ts-no-ip/vite.config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { defineConfig } from 'vite' 3 | import { VitePWA } from 'vite-plugin-pwa' 4 | 5 | const customSW = process.env.SW === 'true' 6 | 7 | export default defineConfig({ 8 | mode: 'development', 9 | logLevel: 'info', 10 | define: { 11 | __DATE__: `'${new Date().toISOString()}'`, 12 | }, 13 | build: { 14 | sourcemap: process.env.SOURCE_MAP === 'true', 15 | }, 16 | plugins: [ 17 | VitePWA({ 18 | mode: 'development', 19 | base: '/', 20 | /* buildBase: '/test-build-base/', */ 21 | strategies: customSW ? 'injectManifest' : 'generateSW', 22 | registerType: 'autoUpdate', 23 | includeAssets: ['favicon.svg'], 24 | filename: customSW ? 'custom-sw.ts' : undefined, 25 | srcDir: 'src', 26 | manifest: { 27 | name: 'PWA Router', 28 | short_name: 'PWA Router', 29 | theme_color: '#ffffff', 30 | icons: [ 31 | { 32 | src: 'pwa-192x192.png', // <== don't add slash, for testing 33 | sizes: '192x192', 34 | type: 'image/png', 35 | }, 36 | { 37 | src: '/pwa-512x512.png', // <== don't remove slash, for testing 38 | sizes: '512x512', 39 | type: 'image/png', 40 | }, 41 | { 42 | src: 'pwa-512x512.png', // <== don't add slash, for testing 43 | sizes: '512x512', 44 | type: 'image/png', 45 | purpose: 'any maskable', 46 | }, 47 | ], 48 | }, 49 | workbox: { 50 | cleanupOutdatedCaches: true, 51 | clientsClaim: true, 52 | skipWaiting: true, 53 | }, 54 | injectManifest: { 55 | minify: false, 56 | enableWorkboxModulesLogs: true, 57 | injectionPoint: undefined, 58 | }, 59 | devOptions: { 60 | enabled: process.env.SW_DEV === 'true', 61 | type: 'module', 62 | }, 63 | }), 64 | ], 65 | }) 66 | -------------------------------------------------------------------------------- /examples/vanilla-ts-no-ip/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/vue-basic-cdn/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | This example relies on [https-localhost](https://github.com/daquinoaldo/https-localhost) to serve the dist files on https://localhost/. Please refer to it's docs for the steps to setup your local environment. 4 | 5 | ```bash 6 | npm run start 7 | ``` 8 | 9 | Open up https://localhost/, then restart the server, you will see a notification ask you to restart reload the offline content. 10 | -------------------------------------------------------------------------------- /examples/vue-basic-cdn/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vite App 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/vue-basic-cdn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-vue-basic", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "nr run-build && nr serve", 8 | "dev": "DEBUG=vite-plugin-pwa vite", 9 | "build": "DEBUG=vite-plugin-pwa vite build", 10 | "run-build": "DEBUG=vite-plugin-pwa BASE_URL=/ SOURCE_MAP=true vite build", 11 | "serve": "serve dist" 12 | }, 13 | "dependencies": { 14 | "vue": "^3.3.8" 15 | }, 16 | "devDependencies": { 17 | "@rollup/plugin-replace": "^5.0.5", 18 | "@vitejs/plugin-vue": "^4.3.4", 19 | "@vueuse/core": "^10.6.1", 20 | "https-localhost": "^4.7.1", 21 | "typescript": "^5.3.3", 22 | "vite": "^5.0.10", 23 | "vite-plugin-pwa": "workspace:*" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/vue-basic-cdn/src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 14 | -------------------------------------------------------------------------------- /examples/vue-basic-cdn/src/ReloadPrompt.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 42 | 43 | 67 | -------------------------------------------------------------------------------- /examples/vue-basic-cdn/src/index.css: -------------------------------------------------------------------------------- 1 | #app { 2 | font-family: Avenir, Helvetica, Arial, sans-serif; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | text-align: center; 6 | margin-top: 60px; 7 | } 8 | -------------------------------------------------------------------------------- /examples/vue-basic-cdn/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import './index.css' 4 | 5 | createApp(App).mount('#app') 6 | -------------------------------------------------------------------------------- /examples/vue-basic-cdn/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { defineComponent } from 'vue' 3 | 4 | const Component: ReturnType 5 | export default Component 6 | } 7 | -------------------------------------------------------------------------------- /examples/vue-basic-cdn/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "jsx": "preserve", 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "useDefineForClassFields": true, 7 | "module": "ESNext", 8 | "moduleResolution": "bundler", 9 | "resolveJsonModule": true, 10 | "allowImportingTsExtensions": true, 11 | "strict": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noEmit": true, 16 | "isolatedModules": true, 17 | "skipLibCheck": true 18 | }, 19 | "references": [{ "path": "./tsconfig.node.json" }], 20 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/vue-basic-cdn/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/vue-basic-cdn/vite.config.ts: -------------------------------------------------------------------------------- 1 | import type { UserConfig } from 'vite' 2 | import process from 'node:process' 3 | import replace from '@rollup/plugin-replace' 4 | import Vue from '@vitejs/plugin-vue' 5 | import { VitePWA } from 'vite-plugin-pwa' 6 | 7 | const config: UserConfig = { 8 | base: 'https://cdn.com/', 9 | build: { 10 | sourcemap: process.env.SOURCE_MAP === 'true', 11 | }, 12 | plugins: [ 13 | Vue(), 14 | VitePWA({ 15 | mode: 'development', 16 | }), 17 | replace({ 18 | __DATE__: new Date().toISOString(), 19 | }), 20 | ], 21 | } 22 | 23 | export default config 24 | -------------------------------------------------------------------------------- /examples/vue-router/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | This example relies on [https-localhost](https://github.com/daquinoaldo/https-localhost) to serve the dist files on https://localhost/. Please refer to it's docs for the steps to setup your local environment. 4 | 5 | ```bash 6 | npm run start 7 | ``` 8 | 9 | Open up https://localhost/, then restart the server, you will see a notification ask you to restart reload the offline content. 10 | -------------------------------------------------------------------------------- /examples/vue-router/client-test/offline.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | const injectManifest = process.env.SW === 'true' 4 | const swName = `${injectManifest ? 'claims-sw.js' : 'sw.js'}` 5 | 6 | test('Vue3: Test offline', 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 | 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 any 17 | return registration.active?.scriptURL 18 | }) 19 | expect(offlineSwURL).toBe(`http://localhost:4173/${swName}`) 20 | await context.setOffline(true) 21 | const aboutAnchor = offlinePage.getByRole('button', { name: 'About' }) 22 | expect(await aboutAnchor.getAttribute('type')).toBe('button') 23 | await aboutAnchor.click({ noWaitAfter: false }) 24 | const url = await offlinePage.evaluate(async () => { 25 | await new Promise(resolve => setTimeout(resolve, 3000)) 26 | return location.href 27 | }) 28 | expect(url).toBe('http://localhost:4173/about') 29 | expect(offlinePage.locator('button').getByText('Home')).toBeTruthy() 30 | await offlinePage.reload({ waitUntil: 'load' }) 31 | expect(offlinePage.url()).toBe('http://localhost:4173/about') 32 | expect(offlinePage.locator('button').getByText('Home')).toBeTruthy() 33 | // Dispose context once it's no longer needed. 34 | await context.close() 35 | }) 36 | -------------------------------------------------------------------------------- /examples/vue-router/client-test/sw.spec.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { expect, test } from '@playwright/test' 3 | 4 | const injectManifest = process.env.SW === 'true' 5 | const swName = `${injectManifest ? 'claims-sw.js' : 'sw.js'}` 6 | 7 | test('Vue3: The service worker is registered and cache storage is present', async ({ page }) => { 8 | await page.goto('/') 9 | 10 | const swURL = await page.evaluate(async () => { 11 | const registration = await Promise.race([ 12 | navigator.serviceWorker.ready, 13 | new Promise((_resolve, reject) => setTimeout(() => reject(new Error('Service worker registration failed: time out')), 10000)), 14 | ]) 15 | // @ts-expect-error registration is of type any 16 | return registration.active?.scriptURL 17 | }) 18 | expect(swURL).toBe(`http://localhost:4173/${swName}`) 19 | 20 | const cacheContents = await page.evaluate(async () => { 21 | const cacheState: Record> = {} 22 | for (const cacheName of await caches.keys()) { 23 | const cache = await caches.open(cacheName) 24 | cacheState[cacheName] = (await cache.keys()).map(req => req.url) 25 | } 26 | return cacheState 27 | }) 28 | 29 | expect(Object.keys(cacheContents).length).toEqual(1) 30 | 31 | const key = 'workbox-precache-v2-http://localhost:4173/' 32 | 33 | expect(Object.keys(cacheContents)[0]).toEqual(key) 34 | 35 | const urls = cacheContents[key].map(url => url.slice('http://localhost:4173/'.length)) 36 | 37 | /* 38 | 'http://localhost:4173/index.html?__WB_REVISION__=073370aa3804305a787b01180cd6b8aa', 39 | 'http://localhost:4173/manifest.webmanifest?__WB_REVISION__=27df2fa4f35d014b42361148a2207da3' 40 | */ 41 | expect(urls.some(url => url.startsWith('manifest.webmanifest?__WB_REVISION__='))).toEqual(true) 42 | expect(urls.some(url => url.startsWith('index.html?__WB_REVISION__='))).toEqual(true) 43 | expect(urls.some(url => url.startsWith(`${swName}`))).toEqual(false) 44 | }) 45 | -------------------------------------------------------------------------------- /examples/vue-router/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Vite App 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/vue-router/public/pwa-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/vue-router/public/pwa-192x192.png -------------------------------------------------------------------------------- /examples/vue-router/public/pwa-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vite-pwa/vite-plugin-pwa/92eca1b6ce047071b35e9e9c7ed2825745a7a269/examples/vue-router/public/pwa-512x512.png -------------------------------------------------------------------------------- /examples/vue-router/src/App.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 54 | -------------------------------------------------------------------------------- /examples/vue-router/src/ReloadPrompt.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 58 | 59 | 83 | -------------------------------------------------------------------------------- /examples/vue-router/src/claims-sw.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'virtual:message' 2 | import { clientsClaim } from 'workbox-core' 3 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 4 | import { NavigationRoute, registerRoute } from 'workbox-routing' 5 | 6 | declare let self: ServiceWorkerGlobalScope 7 | 8 | console.log(message) 9 | 10 | // self.__WB_MANIFEST is default injection point 11 | precacheAndRoute(self.__WB_MANIFEST) 12 | 13 | // clean old assets 14 | cleanupOutdatedCaches() 15 | 16 | let allowlist: undefined | RegExp[] 17 | if (import.meta.env.DEV) 18 | allowlist = [/^\/$/] 19 | 20 | // to allow work offline 21 | registerRoute(new NavigationRoute( 22 | createHandlerBoundToURL('index.html'), 23 | { allowlist }, 24 | )) 25 | 26 | self.skipWaiting() 27 | clientsClaim() 28 | -------------------------------------------------------------------------------- /examples/vue-router/src/index.css: -------------------------------------------------------------------------------- 1 | #app { 2 | font-family: Avenir, Helvetica, Arial, sans-serif; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | text-align: center; 6 | margin-top: 60px; 7 | } 8 | -------------------------------------------------------------------------------- /examples/vue-router/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { createRouter, createWebHistory } from 'vue-router' 3 | import App from './App.vue' 4 | import './index.css' 5 | 6 | const router = createRouter({ 7 | history: createWebHistory(), 8 | routes: [ 9 | { path: '/', component: () => import('./pages/home.vue') }, 10 | { path: '/about', component: () => import('./pages/about.vue') }, 11 | { path: '/hi/:name', component: () => import('./pages/hi/[name].vue'), props: true }, 12 | ], 13 | }) 14 | 15 | createApp(App).use(router).mount('#app') 16 | -------------------------------------------------------------------------------- /examples/vue-router/src/my-worker.js: -------------------------------------------------------------------------------- 1 | import { mode, msg } from './workerImport' 2 | 3 | let counter = 1 4 | 5 | self.onmessage = (e) => { 6 | if (e.data === 'ping') { 7 | self.postMessage({ msg: `${msg} - ${counter++}`, mode }) 8 | } 9 | else if (e.data === 'clear') { 10 | counter = 1 11 | 12 | self.postMessage({ msg: null, mode: null }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/vue-router/src/pages/about.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /examples/vue-router/src/pages/hi/[name].vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 32 | -------------------------------------------------------------------------------- /examples/vue-router/src/pages/home.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 34 | -------------------------------------------------------------------------------- /examples/vue-router/src/prompt-sw.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'virtual:message' 2 | import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching' 3 | import { NavigationRoute, registerRoute } from 'workbox-routing' 4 | 5 | declare let self: ServiceWorkerGlobalScope 6 | 7 | console.log(message) 8 | 9 | self.addEventListener('message', (event) => { 10 | if (event.data && event.data.type === 'SKIP_WAITING') 11 | self.skipWaiting() 12 | }) 13 | 14 | // self.__WB_MANIFEST is default injection point 15 | precacheAndRoute(self.__WB_MANIFEST) 16 | 17 | // clean old assets 18 | cleanupOutdatedCaches() 19 | 20 | // to allow work offline 21 | registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'))) 22 | -------------------------------------------------------------------------------- /examples/vue-router/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { defineComponent } from 'vue' 3 | 4 | const Component: ReturnType 5 | export default Component 6 | } 7 | 8 | declare module 'virtual:message' { 9 | const message: string 10 | export { message } 11 | } 12 | -------------------------------------------------------------------------------- /examples/vue-router/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | -------------------------------------------------------------------------------- /examples/vue-router/src/workerImport.js: -------------------------------------------------------------------------------- 1 | export const msg = 'pong' 2 | export const mode = process.env.NODE_ENV 3 | -------------------------------------------------------------------------------- /examples/vue-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "jsx": "preserve", 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "useDefineForClassFields": true, 7 | "module": "ESNext", 8 | "moduleResolution": "bundler", 9 | "resolveJsonModule": true, 10 | "allowImportingTsExtensions": true, 11 | "strict": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noEmit": true, 16 | "isolatedModules": true, 17 | "skipLibCheck": true 18 | }, 19 | "references": [{ "path": "./tsconfig.node.json" }], 20 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] 21 | } 22 | -------------------------------------------------------------------------------- /examples/vue-router/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/vue-router/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['test/*.test.ts'], 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist' 2 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NODE_VERSION = "18" 3 | 4 | [build] 5 | publish = "docs/.vitepress/dist" 6 | command = "pnpm run deploy" 7 | 8 | [[redirects]] 9 | from = "/*" 10 | to = "/index.html" 11 | status = 200 12 | 13 | [[headers]] 14 | for = "/manifest.webmanifest" 15 | [headers.values] 16 | Content-Type = "application/manifest+json" 17 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - docs/ 3 | - examples/* 4 | onlyBuiltDependencies: 5 | - sharp 6 | -------------------------------------------------------------------------------- /preact.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'virtual:pwa-register/preact' { 2 | // eslint-disable-next-line ts/ban-ts-comment 3 | // @ts-ignore ignore when preact/hooks is not installed 4 | import type { StateUpdater } from 'preact/hooks' 5 | import type { RegisterSWOptions } from 'vite-plugin-pwa/types' 6 | 7 | export type { RegisterSWOptions } 8 | 9 | export function useRegisterSW(options?: RegisterSWOptions): { 10 | needRefresh: [boolean, StateUpdater] 11 | offlineReady: [boolean, StateUpdater] 12 | /** 13 | * Reloads the current window to allow the service worker take the control. 14 | * 15 | * @param reloadPage From version 0.13.2+ this param is not used anymore. 16 | */ 17 | updateServiceWorker: (reloadPage?: boolean) => Promise 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pwa-assets.d.ts: -------------------------------------------------------------------------------- 1 | /* WARNING: these virtual modules are meant to be used by integrations and not being consumed directly from applications */ 2 | 3 | declare module 'virtual:pwa-assets/head' { 4 | export interface PWAAssetHeadLink { 5 | id?: string 6 | rel: 'apple-touch-startup-image' | 'apple-touch-icon' | 'icon' 7 | href: string 8 | media?: string 9 | sizes?: string 10 | type?: string 11 | } 12 | 13 | export interface ColorSchemeMeta { 14 | name: string 15 | content: string 16 | } 17 | 18 | export interface PWAAssetsHead { 19 | links: PWAAssetHeadLink[] 20 | themeColor?: ColorSchemeMeta 21 | } 22 | 23 | export const pwaAssetsHead: PWAAssetsHead 24 | } 25 | 26 | declare module 'virtual:pwa-assets/icons' { 27 | import type { AppleSplashScreenLink, FaviconLink, HtmlLink, IconAsset } from '@vite-pwa/assets-generator/api' 28 | 29 | export type PWAAssetIcon = Omit, 'buffer'> 30 | 31 | export interface PWAAssetsIcons { 32 | favicon: Record> 33 | transparent: Record> 34 | maskable: Record> 35 | apple: Record> 36 | appleSplashScreen: Record> 37 | } 38 | 39 | export const pwaAssetsIcons: PWAAssetsIcons 40 | } 41 | -------------------------------------------------------------------------------- /react.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'virtual:pwa-register/react' { 2 | // eslint-disable-next-line ts/ban-ts-comment 3 | // @ts-ignore ignore when react is not installed 4 | import type { Dispatch, SetStateAction } from 'react' 5 | import type { RegisterSWOptions } from 'vite-plugin-pwa/types' 6 | 7 | export type { RegisterSWOptions } 8 | 9 | export function useRegisterSW(options?: RegisterSWOptions): { 10 | needRefresh: [boolean, Dispatch>] 11 | offlineReady: [boolean, Dispatch>] 12 | /** 13 | * Reloads the current window to allow the service worker take the control. 14 | * 15 | * @param reloadPage From version 0.13.2+ this param is not used anymore. 16 | */ 17 | updateServiceWorker: (reloadPage?: boolean) => Promise 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scripts/build.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process' 2 | import { commands } from './commands' 3 | 4 | for (const command of commands) 5 | execSync(command, { stdio: 'inherit' }) 6 | -------------------------------------------------------------------------------- /scripts/commands.ts: -------------------------------------------------------------------------------- 1 | export const commands = [ 2 | 'npx tsup src/index.ts --dts --target esnext --format cjs,esm', 3 | 'npx tsup src/client/build/register.ts --target esnext --format esm -d dist/client/build', 4 | 'npx tsup src/client/build/vue.ts --external vue --target esnext --format esm -d dist/client/build', 5 | 'npx tsup src/client/build/svelte.ts --external svelte/store --target esnext --format esm -d dist/client/build', 6 | 'npx tsup src/client/build/react.ts --external react --target esnext --format esm -d dist/client/build', 7 | 'npx tsup src/client/build/preact.ts --external preact/hooks --target esnext --format esm -d dist/client/build', 8 | 'npx tsup src/client/build/solid.ts --external solid-js --target esnext --format esm -d dist/client/build', 9 | 'npx tsup src/client/dev/register.ts --target esnext --format esm -d dist/client/dev', 10 | 'npx tsup src/client/dev/vue.ts --external vue --target esnext --format esm -d dist/client/dev', 11 | 'npx tsup src/client/dev/svelte.ts --external svelte/store --target esnext --format esm -d dist/client/dev', 12 | 'npx tsup src/client/dev/react.ts --external react --target esnext --format esm -d dist/client/dev', 13 | 'npx tsup src/client/dev/preact.ts --external preact/hooks --target esnext --format esm -d dist/client/dev', 14 | 'npx tsup src/client/dev/solid.ts --external solid-js --target esnext --format esm -d dist/client/dev', 15 | ] 16 | -------------------------------------------------------------------------------- /scripts/dev.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from 'node:child_process' 2 | import { commands } from './commands' 3 | 4 | for (const command of commands) 5 | spawn('npx', [...command.split(' ').slice(1), '--watch', '--ignore-watch', 'dist'], { stdio: 'inherit' }) 6 | -------------------------------------------------------------------------------- /solid.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'virtual:pwa-register/solid' { 2 | // eslint-disable-next-line ts/ban-ts-comment 3 | // @ts-ignore ignore when solid-js is not installed 4 | import type { Accessor, Setter } from 'solid-js' 5 | import type { RegisterSWOptions } from 'vite-plugin-pwa/types' 6 | 7 | export type { RegisterSWOptions } 8 | 9 | export function useRegisterSW(options?: RegisterSWOptions): { 10 | needRefresh: [Accessor, Setter] 11 | offlineReady: [Accessor, Setter] 12 | /** 13 | * Reloads the current window to allow the service worker take the control. 14 | * 15 | * @param reloadPage From version 0.13.2+ this param is not used anymore. 16 | */ 17 | updateServiceWorker: (reloadPage?: boolean) => Promise 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/client/build/preact.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'preact/hooks' 2 | import type { RegisterSWOptions } from '../type' 3 | import { registerSW } from './register' 4 | 5 | export type { RegisterSWOptions } 6 | 7 | export function useRegisterSW(options: RegisterSWOptions = {}) { 8 | const { 9 | immediate = true, 10 | onNeedRefresh, 11 | onOfflineReady, 12 | onRegistered, 13 | onRegisteredSW, 14 | onRegisterError, 15 | } = options 16 | 17 | const [needRefresh, setNeedRefresh] = useState(false) 18 | const [offlineReady, setOfflineReady] = useState(false) 19 | 20 | const [updateServiceWorker] = useState(() => { 21 | return registerSW({ 22 | immediate, 23 | onOfflineReady() { 24 | setOfflineReady(true) 25 | onOfflineReady?.() 26 | }, 27 | onNeedRefresh() { 28 | setNeedRefresh(true) 29 | onNeedRefresh?.() 30 | }, 31 | onRegistered, 32 | onRegisteredSW, 33 | onRegisterError, 34 | }) 35 | }) 36 | 37 | return { 38 | needRefresh: [needRefresh, setNeedRefresh], 39 | offlineReady: [offlineReady, setOfflineReady], 40 | updateServiceWorker, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/client/build/react.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import type { RegisterSWOptions } from '../type' 3 | import { registerSW } from './register' 4 | 5 | export type { RegisterSWOptions } 6 | 7 | export function useRegisterSW(options: RegisterSWOptions = {}) { 8 | const { 9 | immediate = true, 10 | onNeedRefresh, 11 | onOfflineReady, 12 | onRegistered, 13 | onRegisteredSW, 14 | onRegisterError, 15 | } = options 16 | 17 | const [needRefresh, setNeedRefresh] = useState(false) 18 | const [offlineReady, setOfflineReady] = useState(false) 19 | 20 | const [updateServiceWorker] = useState(() => { 21 | return registerSW({ 22 | immediate, 23 | onOfflineReady() { 24 | setOfflineReady(true) 25 | onOfflineReady?.() 26 | }, 27 | onNeedRefresh() { 28 | setNeedRefresh(true) 29 | onNeedRefresh?.() 30 | }, 31 | onRegistered, 32 | onRegisteredSW, 33 | onRegisterError, 34 | }) 35 | }) 36 | 37 | return { 38 | needRefresh: [needRefresh, setNeedRefresh], 39 | offlineReady: [offlineReady, setOfflineReady], 40 | updateServiceWorker, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/client/build/solid.ts: -------------------------------------------------------------------------------- 1 | import { createSignal } from 'solid-js' 2 | import type { RegisterSWOptions } from '../type' 3 | import { registerSW } from './register' 4 | 5 | export type { RegisterSWOptions } 6 | 7 | export function useRegisterSW(options: RegisterSWOptions = {}) { 8 | const { 9 | immediate = true, 10 | onNeedRefresh, 11 | onOfflineReady, 12 | onRegistered, 13 | onRegisteredSW, 14 | onRegisterError, 15 | } = options 16 | 17 | const [needRefresh, setNeedRefresh] = createSignal(false) 18 | const [offlineReady, setOfflineReady] = createSignal(false) 19 | 20 | const updateServiceWorker = registerSW({ 21 | immediate, 22 | onOfflineReady() { 23 | setOfflineReady(true) 24 | onOfflineReady?.() 25 | }, 26 | onNeedRefresh() { 27 | setNeedRefresh(true) 28 | onNeedRefresh?.() 29 | }, 30 | onRegistered, 31 | onRegisteredSW, 32 | onRegisterError, 33 | }) 34 | 35 | return { 36 | needRefresh: [needRefresh, setNeedRefresh], 37 | offlineReady: [offlineReady, setOfflineReady], 38 | updateServiceWorker, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/client/build/svelte.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | import type { RegisterSWOptions } from '../type' 3 | import { registerSW } from './register' 4 | 5 | export type { RegisterSWOptions } 6 | 7 | export function useRegisterSW(options: RegisterSWOptions = {}) { 8 | const { 9 | immediate = true, 10 | onNeedRefresh, 11 | onOfflineReady, 12 | onRegistered, 13 | onRegisteredSW, 14 | onRegisterError, 15 | } = options 16 | 17 | const needRefresh = writable(false) 18 | const offlineReady = writable(false) 19 | 20 | const updateServiceWorker = registerSW({ 21 | immediate, 22 | onOfflineReady() { 23 | offlineReady.set(true) 24 | onOfflineReady?.() 25 | }, 26 | onNeedRefresh() { 27 | needRefresh.set(true) 28 | onNeedRefresh?.() 29 | }, 30 | onRegistered, 31 | onRegisteredSW, 32 | onRegisterError, 33 | }) 34 | 35 | return { 36 | needRefresh, 37 | offlineReady, 38 | updateServiceWorker, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/client/build/vue.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import type { RegisterSWOptions } from '../type' 3 | import { registerSW } from './register' 4 | 5 | export type { RegisterSWOptions } 6 | 7 | export function useRegisterSW(options: RegisterSWOptions = {}) { 8 | const { 9 | immediate = true, 10 | onNeedRefresh, 11 | onOfflineReady, 12 | onRegistered, 13 | onRegisteredSW, 14 | onRegisterError, 15 | } = options 16 | 17 | const needRefresh = ref(false) 18 | const offlineReady = ref(false) 19 | 20 | const updateServiceWorker = registerSW({ 21 | immediate, 22 | onNeedRefresh() { 23 | needRefresh.value = true 24 | onNeedRefresh?.() 25 | }, 26 | onOfflineReady() { 27 | offlineReady.value = true 28 | onOfflineReady?.() 29 | }, 30 | onRegistered, 31 | onRegisteredSW, 32 | onRegisterError, 33 | }) 34 | 35 | return { 36 | updateServiceWorker, 37 | offlineReady, 38 | needRefresh, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/client/dev/preact.ts: -------------------------------------------------------------------------------- 1 | import type { RegisterSWOptions } from '../type' 2 | import { useState } from 'preact/hooks' 3 | 4 | export type { RegisterSWOptions } 5 | 6 | export function useRegisterSW(_options: RegisterSWOptions = {}) { 7 | const needRefresh = useState(false) 8 | const offlineReady = useState(false) 9 | 10 | const updateServiceWorker = (_reloadPage?: boolean) => {} 11 | 12 | return { 13 | needRefresh, 14 | offlineReady, 15 | updateServiceWorker, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/dev/react.ts: -------------------------------------------------------------------------------- 1 | import type { RegisterSWOptions } from '../type' 2 | import { useState } from 'react' 3 | 4 | export type { RegisterSWOptions } 5 | 6 | export function useRegisterSW(_options: RegisterSWOptions = {}) { 7 | const needRefresh = useState(false) 8 | const offlineReady = useState(false) 9 | 10 | const updateServiceWorker = (_reloadPage?: boolean) => {} 11 | 12 | return { 13 | needRefresh, 14 | offlineReady, 15 | updateServiceWorker, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/dev/register.ts: -------------------------------------------------------------------------------- 1 | import type { RegisterSWOptions } from '../type' 2 | 3 | export type { RegisterSWOptions } 4 | 5 | export function registerSW(_options: RegisterSWOptions = {}) { 6 | return async (_reloadPage = true) => {} 7 | } 8 | -------------------------------------------------------------------------------- /src/client/dev/solid.ts: -------------------------------------------------------------------------------- 1 | import type { RegisterSWOptions } from '../type' 2 | import { createSignal } from 'solid-js' 3 | 4 | export type { RegisterSWOptions } 5 | 6 | export function useRegisterSW(_options: RegisterSWOptions = {}) { 7 | const needRefresh = createSignal(false) 8 | const offlineReady = createSignal(false) 9 | 10 | const updateServiceWorker = (_reloadPage?: boolean) => {} 11 | 12 | return { 13 | needRefresh, 14 | offlineReady, 15 | updateServiceWorker, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/dev/svelte.ts: -------------------------------------------------------------------------------- 1 | import type { RegisterSWOptions } from '../type' 2 | import { writable } from 'svelte/store' 3 | 4 | export type { RegisterSWOptions } 5 | 6 | export function useRegisterSW(_options: RegisterSWOptions = {}) { 7 | const needRefresh = writable(false) 8 | const offlineReady = writable(false) 9 | 10 | const updateServiceWorker = (_reloadPage?: boolean) => {} 11 | 12 | return { 13 | needRefresh, 14 | offlineReady, 15 | updateServiceWorker, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/dev/vue.ts: -------------------------------------------------------------------------------- 1 | import type { RegisterSWOptions } from '../type' 2 | import { ref } from 'vue' 3 | 4 | export type { RegisterSWOptions } 5 | 6 | export function useRegisterSW(_options: RegisterSWOptions = {}) { 7 | const needRefresh = ref(false) 8 | const offlineReady = ref(false) 9 | 10 | const updateServiceWorker = (_reloadPage?: boolean) => {} 11 | 12 | return { 13 | updateServiceWorker, 14 | offlineReady, 15 | needRefresh, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/client/type.d.ts: -------------------------------------------------------------------------------- 1 | export interface RegisterSWOptions { 2 | immediate?: boolean 3 | onNeedRefresh?: () => void 4 | onOfflineReady?: () => void 5 | /** 6 | * Called only if `onRegisteredSW` is not provided. 7 | * 8 | * @deprecated Use `onRegisteredSW` instead. 9 | * @param registration The service worker registration if available. 10 | */ 11 | onRegistered?: (registration: ServiceWorkerRegistration | undefined) => void 12 | /** 13 | * Called once the service worker is registered (requires version `0.12.8+`). 14 | * 15 | * @param swScriptUrl The service worker script url. 16 | * @param registration The service worker registration if available. 17 | */ 18 | onRegisteredSW?: (swScriptUrl: string, registration: ServiceWorkerRegistration | undefined) => void 19 | onRegisterError?: (error: any) => void 20 | } 21 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const FILE_SW_REGISTER = 'registerSW.js' 2 | 3 | export const VIRTUAL_MODULES_MAP: Record = { 4 | 'virtual:pwa-register': 'register', 5 | 'virtual:pwa-register/vue': 'vue', 6 | 'virtual:pwa-register/svelte': 'svelte', 7 | 'virtual:pwa-register/react': 'react', 8 | 'virtual:pwa-register/preact': 'preact', 9 | 'virtual:pwa-register/solid': 'solid', 10 | } 11 | export const VIRTUAL_MODULES_RESOLVE_PREFIX = '/@vite-plugin-pwa/' 12 | export const VIRTUAL_MODULES = Object.keys(VIRTUAL_MODULES_MAP) 13 | export const defaultInjectManifestVitePlugins = [ 14 | 'alias', 15 | 'commonjs', 16 | 'vite:resolve', 17 | 'vite:esbuild', 18 | 'replace', 19 | 'vite:define', 20 | 'rollup-plugin-dynamic-import-variables', 21 | 'vite:esbuild-transpile', 22 | 'vite:json', 23 | 'vite:terser', 24 | ] 25 | 26 | export const PWA_INFO_VIRTUAL = 'virtual:pwa-info' 27 | export const RESOLVED_PWA_INFO_VIRTUAL = `\0${PWA_INFO_VIRTUAL}` 28 | export const PWA_ASSETS_HEAD_VIRTUAL = 'virtual:pwa-assets/head' 29 | export const RESOLVED_PWA_ASSETS_HEAD_VIRTUAL = `\0${PWA_ASSETS_HEAD_VIRTUAL}` 30 | export const PWA_ASSETS_ICONS_VIRTUAL = 'virtual:pwa-assets/icons' 31 | export const RESOLVED_PWA_ASSETS_ICONS_VIRTUAL = `\0${PWA_ASSETS_ICONS_VIRTUAL}` 32 | 33 | export const DEV_SW_NAME = 'dev-sw.js?dev-sw' 34 | export const DEV_SW_VIRTUAL = `${VIRTUAL_MODULES_RESOLVE_PREFIX}pwa-entry-point-loaded` 35 | export const RESOLVED_DEV_SW_VIRTUAL = `\0${DEV_SW_VIRTUAL}` 36 | export const DEV_READY_NAME = 'vite-pwa-plugin:dev-ready' 37 | export const DEV_REGISTER_SW_NAME = 'vite-plugin-pwa:register-sw' 38 | export const DEV_PWA_ASSETS_NAME = 'vite-plugin-pwa:pwa-assets' 39 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import type { ResolvedConfig } from 'vite' 2 | import type { PWAAssetsGenerator } from './pwa-assets/types' 3 | import type { ResolvedVitePWAOptions, VitePWAOptions } from './types' 4 | import { readFileSync } from 'node:fs' 5 | import { dirname, resolve } from 'node:path' 6 | import { fileURLToPath } from 'node:url' 7 | 8 | export interface PWAPluginContext { 9 | version: string 10 | viteConfig: ResolvedConfig 11 | userOptions: Partial 12 | options: ResolvedVitePWAOptions 13 | useImportRegister: boolean 14 | devEnvironment: boolean 15 | pwaAssetsGenerator: Promise 16 | } 17 | 18 | export function createContext(userOptions: Partial): PWAPluginContext { 19 | const _dirname = typeof __dirname !== 'undefined' 20 | ? __dirname 21 | : dirname(fileURLToPath(import.meta.url)) 22 | const { version } = JSON.parse( 23 | readFileSync(resolve(_dirname, '../package.json'), 'utf-8'), 24 | ) 25 | return { 26 | version, 27 | userOptions, 28 | options: undefined!, 29 | viteConfig: undefined!, 30 | useImportRegister: false, 31 | devEnvironment: false, 32 | pwaAssetsGenerator: Promise.resolve(undefined), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | import type { VitePWAOptions } from './types' 3 | import { createAPI } from './api' 4 | import { createContext } from './context' 5 | import { BuildPlugin } from './plugins/build' 6 | import { DevPlugin } from './plugins/dev' 7 | import { InfoPlugin } from './plugins/info' 8 | import { MainPlugin } from './plugins/main' 9 | import { AssetsPlugin } from './plugins/pwa-assets' 10 | 11 | export function VitePWA(userOptions: Partial = {}): Plugin[] { 12 | const ctx = createContext(userOptions) 13 | const api = createAPI(ctx) 14 | return [ 15 | MainPlugin(ctx, api), 16 | InfoPlugin(ctx, api), 17 | BuildPlugin(ctx), 18 | DevPlugin(ctx), 19 | AssetsPlugin(ctx), 20 | ] 21 | } 22 | 23 | export { cachePreset } from './cache' 24 | export { defaultInjectManifestVitePlugins } from './constants' 25 | export * from './types' 26 | export type { VitePWAOptions as Options } 27 | -------------------------------------------------------------------------------- /src/plugins/build.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | import type { PWAPluginContext } from '../context' 3 | import { _generateBundle, _generateSW } from '../api' 4 | import { injectServiceWorker } from '../html' 5 | 6 | export function BuildPlugin(ctx: PWAPluginContext) { 7 | const transformIndexHtmlHandler = (html: string) => { 8 | const { options, useImportRegister } = ctx 9 | if (options.disable) 10 | return html 11 | 12 | // if virtual register is requested, do not inject. 13 | if (options.injectRegister === 'auto') 14 | options.injectRegister = useImportRegister ? null : 'script' 15 | 16 | return injectServiceWorker(html, options, false) 17 | } 18 | 19 | return { 20 | name: 'vite-plugin-pwa:build', 21 | enforce: 'post', 22 | apply: 'build', 23 | transformIndexHtml: { 24 | order: 'post', 25 | handler(html) { 26 | return transformIndexHtmlHandler(html) 27 | }, 28 | enforce: 'post', // deprecated since Vite 4 29 | transform(html) { // deprecated since Vite 4 30 | return transformIndexHtmlHandler(html) 31 | }, 32 | }, 33 | async generateBundle(_, bundle) { 34 | const pwaAssetsGenerator = await ctx.pwaAssetsGenerator 35 | if (pwaAssetsGenerator) 36 | pwaAssetsGenerator.injectManifestIcons() 37 | 38 | return _generateBundle(ctx, bundle, this) 39 | }, 40 | closeBundle: { 41 | sequential: true, 42 | order: ctx.userOptions?.integration?.closeBundleOrder, 43 | async handler() { 44 | if (!ctx.viteConfig.build.ssr) { 45 | const pwaAssetsGenerator = await ctx.pwaAssetsGenerator 46 | if (pwaAssetsGenerator) 47 | await pwaAssetsGenerator.generate() 48 | 49 | if (!ctx.options.disable) 50 | await _generateSW(ctx) 51 | } 52 | }, 53 | }, 54 | async buildEnd(error) { 55 | if (error) 56 | throw error 57 | }, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/pwa-assets/build.ts: -------------------------------------------------------------------------------- 1 | import type { AssetsGeneratorContext } from './types' 2 | import { mkdir } from 'node:fs/promises' 3 | import { generateAssets } from '@vite-pwa/assets-generator/api/generate-assets' 4 | 5 | export async function generate( 6 | assetsGeneratorContext: AssetsGeneratorContext, 7 | ) { 8 | await mkdir(assetsGeneratorContext.imageOutDir, { recursive: true }) 9 | await generateAssets(assetsGeneratorContext.assetsInstructions, true, assetsGeneratorContext.imageOutDir) 10 | } 11 | -------------------------------------------------------------------------------- /src/pwa-assets/dev.ts: -------------------------------------------------------------------------------- 1 | import type { PWAPluginContext } from '../context' 2 | import type { AssetsGeneratorContext, ResolvedIconAsset } from './types' 3 | import { loadAssetsGeneratorContext } from './config' 4 | 5 | export async function findIconAsset( 6 | path: string, 7 | { assetsInstructions, cache, lastModified }: AssetsGeneratorContext, 8 | ) { 9 | let resolved = cache.get(path) 10 | if (resolved) { 11 | resolved.age = Date.now() - lastModified 12 | return resolved 13 | } 14 | 15 | const iconAsset = assetsInstructions.transparent[path] 16 | ?? assetsInstructions.maskable[path] 17 | ?? assetsInstructions.apple[path] 18 | ?? assetsInstructions.favicon[path] 19 | ?? assetsInstructions.appleSplashScreen[path] 20 | 21 | if (!iconAsset) 22 | return 23 | 24 | if (iconAsset) { 25 | resolved = { 26 | path, 27 | mimeType: iconAsset.mimeType, 28 | buffer: iconAsset.buffer(), 29 | lastModified: Date.now(), 30 | age: 0, 31 | } satisfies ResolvedIconAsset 32 | cache.set(path, resolved) 33 | return resolved 34 | } 35 | } 36 | 37 | export async function checkHotUpdate( 38 | file: string, 39 | ctx: PWAPluginContext, 40 | assetsGeneratorContext: AssetsGeneratorContext, 41 | ) { 42 | // watch pwa assets configuration file 43 | const result = assetsGeneratorContext.sources.includes(file) 44 | if (result) 45 | await loadAssetsGeneratorContext(ctx, assetsGeneratorContext) 46 | 47 | return result 48 | } 49 | -------------------------------------------------------------------------------- /src/pwa-assets/generator.ts: -------------------------------------------------------------------------------- 1 | import type { PWAPluginContext } from '../context' 2 | import type { PWAAssetsGenerator } from './types' 3 | import { generate } from './build' 4 | import { loadAssetsGeneratorContext } from './config' 5 | import { checkHotUpdate, findIconAsset } from './dev' 6 | import { resolveHtmlAssets, transformIndexHtml } from './html' 7 | import { injectManifestIcons } from './manifest' 8 | import { extractIcons } from './utils' 9 | 10 | export async function loadInstructions(ctx: PWAPluginContext) { 11 | const assetsGeneratorContext = await loadAssetsGeneratorContext(ctx) 12 | if (!assetsGeneratorContext) 13 | return 14 | 15 | return { 16 | generate: () => generate(assetsGeneratorContext), 17 | findIconAsset: (path: string) => findIconAsset(path, assetsGeneratorContext), 18 | resolveHtmlAssets: () => resolveHtmlAssets(ctx, assetsGeneratorContext), 19 | transformIndexHtml: (html: string) => transformIndexHtml(html, ctx, assetsGeneratorContext), 20 | injectManifestIcons: () => injectManifestIcons(ctx, assetsGeneratorContext), 21 | instructions: () => assetsGeneratorContext.assetsInstructions, 22 | icons: () => extractIcons(assetsGeneratorContext.assetsInstructions), 23 | checkHotUpdate: file => checkHotUpdate(file, ctx, assetsGeneratorContext), 24 | } satisfies PWAAssetsGenerator 25 | } 26 | -------------------------------------------------------------------------------- /src/pwa-assets/manifest.ts: -------------------------------------------------------------------------------- 1 | import type { PWAPluginContext } from '../context' 2 | import type { AssetsGeneratorContext } from './types' 3 | import { generateManifestIconsEntry } from '@vite-pwa/assets-generator/api/generate-manifest-icons-entry' 4 | 5 | export function injectManifestIcons( 6 | ctx: PWAPluginContext, 7 | assetsGeneratorContext: AssetsGeneratorContext, 8 | ) { 9 | if (!assetsGeneratorContext.overrideManifestIcons) 10 | return 11 | 12 | const manifest = ctx.options.manifest 13 | if (manifest) { 14 | manifest.icons = generateManifestIconsEntry( 15 | 'object', 16 | assetsGeneratorContext.assetsInstructions, 17 | ).icons 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pwa-assets/options.ts: -------------------------------------------------------------------------------- 1 | import type { PWAAssetsOptions, ResolvedPWAAssetsOptions } from '../types' 2 | 3 | export function resolvePWAAssetsOptions( 4 | options?: PWAAssetsOptions, 5 | ) { 6 | if (!options) 7 | return false 8 | 9 | const { 10 | disabled, 11 | preset = 'minimal-2023', 12 | image = 'public/favicon.svg', 13 | htmlPreset = '2023', 14 | overrideManifestIcons = false, 15 | includeHtmlHeadLinks = true, 16 | injectThemeColor = true, 17 | integration, 18 | } = options ?? {} 19 | 20 | const resolvedConfiguration: ResolvedPWAAssetsOptions = { 21 | disabled: true, 22 | config: false, 23 | preset: false, 24 | images: [image], 25 | htmlPreset, 26 | overrideManifestIcons, 27 | includeHtmlHeadLinks, 28 | injectThemeColor, 29 | integration, 30 | } 31 | 32 | if (disabled === true) 33 | return resolvedConfiguration 34 | 35 | if ('config' in options && !!options.config) { 36 | resolvedConfiguration.disabled = false 37 | resolvedConfiguration.config = options.config 38 | return resolvedConfiguration 39 | } 40 | 41 | if (preset === false) 42 | return resolvedConfiguration 43 | 44 | resolvedConfiguration.disabled = false 45 | resolvedConfiguration.preset = preset 46 | 47 | return resolvedConfiguration 48 | } 49 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function slash(str: string) { 2 | return str.replace(/\\/g, '/') 3 | } 4 | 5 | export function resolveBasePath(base: string) { 6 | if (isAbsolute(base)) 7 | return base 8 | return (!base.startsWith('/') && !base.startsWith('./')) 9 | ? `/${base}` 10 | : base 11 | } 12 | 13 | export function isAbsolute(url: string) { 14 | return url.match(/^(?:[a-z]+:)?\/\//i) 15 | } 16 | 17 | export function normalizePath(path: string) { 18 | return path.replace(/\\/g, '/') 19 | } 20 | -------------------------------------------------------------------------------- /svelte.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'virtual:pwa-register/svelte' { 2 | // eslint-disable-next-line ts/ban-ts-comment 3 | // @ts-ignore ignore when svelte is not installed 4 | import type { Writable } from 'svelte/store' 5 | import type { RegisterSWOptions } from 'vite-plugin-pwa/types' 6 | 7 | export type { RegisterSWOptions } 8 | 9 | export function useRegisterSW(options?: RegisterSWOptions): { 10 | needRefresh: Writable 11 | offlineReady: Writable 12 | /** 13 | * Reloads the current window to allow the service worker take the control. 14 | * 15 | * @param reloadPage From version 0.13.2+ this param is not used anymore. 16 | */ 17 | updateServiceWorker: (reloadPage?: boolean) => Promise 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["ESNext", "DOM", "WebWorker"], 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "resolveJsonModule": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "noUnusedLocals": true, 11 | "esModuleInterop": true 12 | }, 13 | "exclude": [ 14 | "dist", 15 | "node_modules", 16 | "test" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface RegisterSWOptions { 2 | immediate?: boolean 3 | onNeedRefresh?: () => void 4 | onOfflineReady?: () => void 5 | /** 6 | * Called only if `onRegisteredSW` is not provided. 7 | * 8 | * @deprecated Use `onRegisteredSW` instead. 9 | * @param registration The service worker registration if available. 10 | */ 11 | onRegistered?: (registration: ServiceWorkerRegistration | undefined) => void 12 | /** 13 | * Called once the service worker is registered (requires version `0.12.8+`). 14 | * 15 | * @param swScriptUrl The service worker script url. 16 | * @param registration The service worker registration if available. 17 | */ 18 | onRegisteredSW?: (swScriptUrl: string, registration: ServiceWorkerRegistration | undefined) => void 19 | onRegisterError?: (error: any) => void 20 | } 21 | -------------------------------------------------------------------------------- /types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /vanillajs.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'virtual:pwa-register' { 2 | import type { RegisterSWOptions } from 'vite-plugin-pwa/types' 3 | 4 | export type { RegisterSWOptions } 5 | 6 | /** 7 | * Registers the service worker returning a callback to reload the current page when an update is found. 8 | * 9 | * @param options the options to register the service worker. 10 | * @return (reloadPage?: boolean) => Promise From version 0.13.2+ `reloadPage` param is not used anymore. 11 | */ 12 | export function registerSW(options?: RegisterSWOptions): (reloadPage?: boolean) => Promise 13 | } 14 | -------------------------------------------------------------------------------- /vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'virtual:pwa-register/vue' { 2 | import type { RegisterSWOptions } from 'vite-plugin-pwa/types' 3 | // eslint-disable-next-line ts/ban-ts-comment 4 | // @ts-ignore ignore when vue is not installed 5 | import type { Ref } from 'vue' 6 | 7 | export type { RegisterSWOptions } 8 | 9 | export function useRegisterSW(options?: RegisterSWOptions): { 10 | needRefresh: Ref 11 | offlineReady: Ref 12 | /** 13 | * Reloads the current window to allow the service worker take the control. 14 | * 15 | * @param reloadPage From version 0.13.2+ this param is not used anymore. 16 | */ 17 | updateServiceWorker: (reloadPage?: boolean) => Promise 18 | } 19 | } 20 | --------------------------------------------------------------------------------