提交 3fa4c162 编写于 作者: View Design's avatar View Design

Auto Commit

上级 99586ea7
root = true
[*]
indent_size = 2
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
dev-dist
dist
node_modules
templates
{
"extends": ["@antfu"]
}
name: Release
permissions:
contents: write
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npx changelogithub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shamefully-hoist=true
strict-peer-dependencies=false
shell-emulator=true
typescript.includeWorkspace=true
typescript.tsConfig.exclude[]=../playground
# Contributing Guide
Hi! We are really excited that you are interested in contributing to `@vite-pwa/nuxt`. Before submitting your contribution, please make sure to take a moment and read through the following guide.
Refer also to https://github.com/antfu/contribute.
## Set up your local development environment
The `@vite-pwa/nuxt` repo is a monorepo using pnpm workspaces. The package manager used to install and link dependencies must be [pnpm](https://pnpm.io/).
To develop and test the `@vite-pwa/nuxt` package:
1. Fork the `@vite-pwa/nuxt` repository to your own GitHub account and then clone it to your local device.
2. `@vite-pwa/nuxt` uses pnpm v8. If you are working on multiple projects with different versions of pnpm, it's recommend to enable [Corepack](https://github.com/nodejs/corepack) by running `corepack enable`.
3. Check out a branch where you can work and commit your changes:
```shell
git checkout -b my-new-branch
```
5. Run `pnpm install` in `@vite-pwa/nuxt`'s root folder
6. Run `nr dev:prepare` in `@vite-pwa/nuxt`'s root folder.
7. Run `nr dev` in `@vite-pwa/nuxt`'s root folder.
## Running tests
Before running tests, you'll need to install [Playwright](https://playwright.dev/) Chromium browser: `pnpm playwright install chromium`.
Run `nr test` in `@vite-pwa/nuxt`'s root folder.
MIT License
Copyright (c) 2023-PRESENT Anthony Fu <https://github.com/antfu>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
<p align='center'>
<img src='https://raw.githubusercontent.com/vite-pwa/nuxt/main/hero.png' alt="@vite-pwa/nuxt - Zero-config PWA for Nuxt 3"><br>
Zero-config PWA Plugin for Nuxt 3
</p>
<p align='center'>
<a href='https://www.npmjs.com/package/@vite-pwa/nuxt' target="__blank">
<img src='https://img.shields.io/npm/v/@vite-pwa/nuxt?color=33A6B8&label=' alt="NPM version">
</a>
<a href="https://www.npmjs.com/package/@vite-pwa/nuxt" target="__blank">
<img alt="NPM Downloads" src="https://img.shields.io/npm/dm/@vite-pwa/nuxt?color=476582&label=">
</a>
<a href="https://vite-pwa-org.netlify.app/frameworks/nuxt" target="__blank">
<img src="https://img.shields.io/static/v1?label=&message=docs%20%26%20guides&color=2e859c" alt="Docs & Guides">
</a>
<br>
<a href="https://github.com/vite-pwa/nuxt" target="__blank">
<img alt="GitHub stars" src="https://img.shields.io/github/stars/vite-pwa/nuxt?style=social">
</a>
</p>
<br>
<p align="center">
<a href="https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg">
<img src='https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg'/>
</a>
</p>
## 🚀 Features
- 📖 [**Documentation & guides**](https://vite-pwa-org.netlify.app/)
- 👌 **Zero-Config**: sensible built-in default configs for common use cases
- 🔩 **Extensible**: expose the full ability to customize the behavior of the plugin
- 🦾 **Type Strong**: written in [TypeScript](https://www.typescriptlang.org/)
- 🔌 **Offline Support**: generate service worker with offline support (via Workbox)
-**Fully tree shakable**: auto inject Web App Manifest
- 💬 **Prompt for new content**: built-in support for Vanilla JavaScript, Vue 3, React, Svelte, SolidJS and Preact
- ⚙️ **Stale-while-revalidate**: automatic reload when new content is available
-**Static assets handling**: configure static assets for offline support
- 🐞 **Development Support**: debug your custom service worker logic as you develop your application
- 🛠️ **Versatile**: integration with meta frameworks: [îles](https://github.com/ElMassimo/iles), [SvelteKit](https://github.com/sveltejs/kit), [VitePress](https://github.com/vuejs/vitepress), [Astro](https://github.com/withastro/astro), [Nuxt 3](https://github.com/nuxt/nuxt) and [Remix](https://github.com/remix-run/remix)
- 💥 **PWA Assets Generator**: generate all the PWA assets from a single command and a single source image
- 🚀 **PWA Assets Integration**: serving, generating and injecting PWA Assets on the fly in your application
## 📦 Install
> From v0.4.0, `@vite-pwa/nuxt` requires Vite 5 and Nuxt 3.9.0+.
> For older versions, `@vite-pwa/nuxt` requires Vite 3.2.0+ and Nuxt 3.0.0+.
```bash
npx nuxi@latest module add @vite-pwa/nuxt
```
## 🦄 Usage
Add `@vite-pwa/nuxt` module to `nuxt.config.ts` and configure it:
```ts
// nuxt.config.ts
import { defineNuxtConfig } from 'nuxt/config'
export default defineNuxtConfig({
modules: [
'@vite-pwa/nuxt'
],
pwa: {
/* PWA options */
}
})
```
Read the [📖 documentation](https://vite-pwa-org.netlify.app/frameworks/nuxt) for a complete guide on how to configure and use
this plugin.
## ⚡️ Examples
You need to stop the dev server once started and then to see the PWA in action run:
- `nr dev:preview:build`: Nuxt build command + start server
- `nr dev:preview:generate`: Nuxt generate command + start server
<table>
<thead>
<tr>
<th>Example</th>
<th>Source</th>
<th>Playground</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Auto Update PWA</code></td>
<td><a href="https://github.com/vite-pwa/nuxt/tree/main/playground">GitHub</a></td>
<td>
<a href="https://stackblitz.com/fork/github/vite-pwa/nuxt" target="_blank" rel="noopener noreferrer">
<img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open in StackBlitz" width="162" height="32">
</a>
</td>
</tr>
</tbody>
</table>
## 👀 Full config
Check out the type declaration [src/types.ts](./src/types.ts) and the following links for more details.
- [Web app manifests](https://developer.mozilla.org/en-US/docs/Web/Manifest)
- [Workbox](https://developers.google.com/web/tools/workbox)
## 📄 License
[MIT](./LICENSE) License &copy; 2023-PRESENT [Anthony Fu](https://github.com/antfu)
import { expect, test } from '@playwright/test'
test('The service worker is registered and cache storage is present', async ({ page }) => {
await page.goto('/')
const swURL = await page.evaluate(async () => {
const registration = await Promise.race([
navigator.serviceWorker.ready,
new Promise((_resolve, reject) => setTimeout(() => reject(new Error('Service worker registration failed: time out')), 10000)),
])
// @ts-expect-error TS18046: 'registration' is of type 'unknown'.
return registration.active?.scriptURL
})
const swName = 'sw.js'
expect(swURL).toBe(`http://localhost:4173/${swName}`)
const cacheContents = await page.evaluate(async () => {
const cacheState: Record<string, Array<string>> = {}
for (const cacheName of await caches.keys()) {
const cache = await caches.open(cacheName)
cacheState[cacheName] = (await cache.keys()).map(req => req.url)
}
return cacheState
})
expect(Object.keys(cacheContents).length).toEqual(1)
const key = 'workbox-precache-v2-http://localhost:4173/'
expect(Object.keys(cacheContents)[0]).toEqual(key)
const urls = cacheContents[key].map(url => url.slice('http://localhost:4173/'.length))
/*
'http://localhost:4173/about?__WB_REVISION__=38251751d310c9b683a1426c22c135a2',
'http://localhost:4173/?__WB_REVISION__=073370aa3804305a787b01180cd6b8aa',
'http://localhost:4173/manifest.webmanifest?__WB_REVISION__=27df2fa4f35d014b42361148a2207da3'
*/
expect(urls.some(url => url.startsWith('manifest.webmanifest?__WB_REVISION__='))).toEqual(true)
expect(urls.some(url => url.startsWith('?__WB_REVISION__='))).toEqual(true)
expect(urls.some(url => url.startsWith('about?__WB_REVISION__='))).toEqual(true)
// dontCacheBustURLsMatching: any asset in _nuxt folder shouldn't have a revision (?__WB_REVISION__=)
expect(urls.some(url => url.startsWith('_nuxt/') && url.endsWith('.css'))).toEqual(true)
expect(urls.some(url => url.startsWith('_nuxt/') && url.endsWith('.js'))).toEqual(true)
expect(urls.some(url => url.includes('_payload.json?__WB_REVISION__='))).toEqual(true)
expect(urls.some(url => url.startsWith('_nuxt/builds/') && url.includes('.json'))).toEqual(true)
})
declare module 'virtual:nuxt-pwa-configuration' {
export const enabled: boolean
export const display: 'fullscreen' | 'standalone' | 'minimal-ui' | 'browser'
export const installPrompt: string | undefined
export const periodicSyncForUpdates: number
}
{
"name": "@vite-pwa/nuxt",
"type": "module",
"version": "0.7.0",
"packageManager": "pnpm@9.0.6",
"description": "Zero-config PWA for Nuxt 3",
"author": "antfu <anthonyfu117@hotmail.com>",
"license": "MIT",
"funding": "https://github.com/sponsors/antfu",
"homepage": "https://github.com/vite-pwa/nuxt#readme",
"repository": {
"type": "git",
"url": "https://github.com/vite-pwa/nuxt.git"
},
"bugs": "https://github.com/vite-pwa/nuxt/issues",
"keywords": [
"nuxt",
"pwa",
"workbox",
"vite-plugin-pwa",
"nuxt-module"
],
"exports": {
".": {
"types": "./dist/types.d.mts",
"default": "./dist/module.mjs"
},
"./configuration": {
"types": "./configuration.d.ts"
},
"./*": "./*"
},
"main": "./dist/module.mjs",
"types": "./dist/types.d.ts",
"files": [
"dist",
"*.d.ts"
],
"scripts": {
"prepack": "nuxt-module-build prepare && nuxt-module-build build",
"dev": "nuxi dev playground",
"dev:generate": "nuxi generate playground",
"dev:generate:netlify": "NITRO_PRESET=netlify nuxi generate playground",
"dev:generate:vercel": "NITRO_PRESET=vercel nuxi generate playground",
"dev:build": "nuxi build playground",
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
"dev:preview:build": "nr dev:build && node playground/.output/server/index.mjs",
"dev:preview:generate": "nr dev:generate && serve playground/dist",
"release": "bumpp && npm publish",
"lint": "eslint .",
"lint-fix": "nr lint --fix",
"test:build:serve": "PORT=4173 node playground/.output/server/index.mjs",
"test:generate:serve": "PORT=4173 serve playground/dist",
"test:build": "nr dev:build && TEST_BUILD=true vitest run && TEST_BUILD=true playwright test",
"test:generate": "nr dev:generate && vitest run && playwright test",
"test": "nr test:build && nr test:generate",
"test:with-build": "nr dev:prepare && nr prepack && nr test"
},
"dependencies": {
"@nuxt/kit": "^3.9.0",
"pathe": "^1.1.1",
"ufo": "^1.3.2",
"vite-plugin-pwa": ">=0.20.0 <1"
},
"devDependencies": {
"@antfu/eslint-config": "^0.43.1",
"@antfu/ni": "^0.21.10",
"@nuxt/module-builder": "^0.5.5",
"@nuxt/schema": "^3.10.1",
"@nuxt/test-utils": "^3.11.0",
"@playwright/test": "^1.40.1",
"@types/node": "^18",
"bumpp": "^9.2.0",
"eslint": "^8.54.0",
"node-fetch-native": "^1.4.1",
"nuxt": "^3.10.1",
"publint": "^0.2.5",
"rimraf": "^5.0.5",
"serve": "^14.2.1",
"typescript": "^5.4.5",
"vitest": "^1.1.0",
"vue-tsc": "^1.8.27"
},
"resolutions": {
"@nuxt/kit": "^3.10.1"
},
"peerDependencies": {
"@vite-pwa/assets-generator": "^0.2.4"
},
"peerDependenciesMeta": {
"@vite-pwa/assets-generator": {
"optional": true
}
},
"build": {
"externals": [
"node:child_process",
"node:fs",
"consola",
"esbuild",
"pathe",
"rollup",
"ufo",
"vite",
"vite-plugin-pwa"
]
},
"stackblitz": {
"startCommand": "nr prepack && nr dev:prepare && nr dev"
}
}
<template>
<NuxtLoadingIndicator />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>
<template>
<div>
<NuxtPwaAssets />
<slot />
</div>
</template>
import process from 'node:process'
const sw = process.env.SW === 'true'
export default defineNuxtConfig({
/* ssr: false, */
// typescript,
modules: ['@vite-pwa/nuxt'],
future: {
typescriptBundlerResolution: true,
},
experimental: {
payloadExtraction: true,
watcher: 'parcel',
},
nitro: {
esbuild: {
options: {
target: 'esnext',
},
},
prerender: {
routes: ['/', '/about'],
},
},
imports: {
autoImport: true,
},
appConfig: {
// you don't need to include this: only for testing purposes
buildDate: new Date().toISOString(),
},
vite: {
logLevel: 'info',
},
pwa: {
mode: 'development',
strategies: 'generateSW',
registerType: 'autoUpdate',
manifest: {
name: 'Nuxt Vite PWA',
short_name: 'NuxtVitePWA',
theme_color: '#ffffff',
},
pwaAssets: {
config: true,
},
workbox: {
globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
},
client: {
installPrompt: true,
},
devOptions: {
enabled: true,
suppressWarnings: true,
navigateFallback: '/',
navigateFallbackAllowlist: [/^\/$/],
},
},
})
{
"name": "playground-assets",
"type": "module",
"private": true,
"scripts": {
"dev": "nuxi dev",
"build": "nuxi build",
"generate": "nuxi generate"
},
"devDependencies": {
"@vite-pwa/assets-generator": "^0.2.4",
"@vite-pwa/nuxt": "workspace:*",
"nuxt": "^3.10.1"
}
}
<script setup lang="ts">
</script>
<template>
<div>
<h1>Nuxt Vite PWA</h1>
<ClientOnly>
PWA Installed: {{ $pwa?.isPWAInstalled }}
</ClientOnly>
<br>
<NuxtLink to="/">
Home
</NuxtLink>
<br>
<PwaTransparentImage image="pwa-192x192.png" alt="PWA Icon" />
</div>
</template>
<script setup lang="ts">
</script>
<template>
<div>
<h1>Nuxt Vite PWA</h1>
<ClientOnly>
PWA Installed: {{ $pwa?.isPWAInstalled }}
</ClientOnly>
<br>
<NuxtLink to="/about">
About
</NuxtLink>
<br>
<PwaTransparentImage image="pwa-64x64.png" alt="PWA Icon" />
</div>
</template>
<svg width="155" height="155" viewBox="0 0 155 155" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
class="a"
d="M50.6853 104.345C48.3909 100.398 46.0047 96.3602 43.7103 92.4138C43.5268 92.1385 43.435 91.8632 43.5267 91.4043C45.8211 84.4293 48.1155 77.3626 50.4099 70.3876C50.4099 70.2959 50.5017 70.1123 50.5935 69.837C53.255 75.0682 55.8247 80.2076 58.4862 85.4389C58.6697 85.0717 58.7615 84.7964 58.8533 84.5211C62.4326 73.508 66.0118 62.5867 69.5911 51.5736C69.7746 51.1147 69.9582 50.9312 70.417 51.0229C73.6292 51.0229 76.8413 51.0229 80.1453 51.0229C80.6959 51.0229 80.8795 51.2065 81.063 51.6654C84.367 62.5867 87.6709 73.4162 90.9748 84.3375C91.0666 84.6129 91.1584 84.98 91.3419 85.4389C91.8008 84.5211 92.1679 83.6033 92.4432 82.7774C96.9402 72.3149 101.345 61.9443 105.842 51.4818C105.934 51.2065 106.026 51.0229 106.393 51.0229C110.982 51.0229 115.571 51.0229 120.068 51.0229C120.068 51.0229 120.16 51.0229 120.251 51.0229C119.701 52.3078 119.242 53.5009 118.783 54.7857C112.175 71.0301 105.659 87.3661 99.0511 103.61C98.9593 103.794 98.7757 103.978 98.8675 104.253C94.0952 104.253 89.3229 104.253 84.5505 104.253C84.6423 104.069 84.5505 103.794 84.4587 103.61C84.0916 102.417 83.7245 101.224 83.3574 100.031C80.6042 91.3125 77.9426 82.6856 75.1894 73.9669C75.1894 73.7833 75.1894 73.5998 74.9141 73.508C71.5184 83.6951 68.2144 93.974 64.8187 104.161C60.0464 104.345 55.3658 104.345 50.6853 104.345Z"
fill="#35849A" />
<path
class="b"
d="M7 104.345C7 86.8155 7 69.2863 7 51.7571C7 51.2065 7.09178 51.0229 7.73421 51.0229C15.168 51.0229 22.6019 51.0229 30.0357 51.0229C34.1656 51.0229 38.0202 51.9407 41.3241 54.5104C42.7008 55.5199 43.8938 56.8048 44.9034 58.2732C45.0869 58.5485 45.0869 58.7321 44.9952 59.0074C42.2419 67.5426 39.3968 76.1695 36.6436 84.7046C36.5518 85.0717 36.3682 85.1635 36.0011 85.2553C34.0738 85.7142 32.0548 85.9895 30.0357 85.9895C27.0071 85.9895 24.0703 85.9895 21.0417 85.9895C20.6746 85.9895 20.5828 85.9895 20.5828 86.4484C20.5828 92.322 20.5828 98.2874 20.5828 104.161V104.253C16.0858 104.345 11.497 104.345 7 104.345ZM20.5828 68.4603C20.5828 70.6629 20.5828 72.8656 20.5828 75.16C20.5828 75.6188 20.6746 75.7106 21.1335 75.7106C22.2348 75.7106 23.4279 75.7106 24.5292 75.7106C26.0894 75.7106 27.6495 75.7106 29.2097 75.3435C30.5864 74.9764 31.8712 74.5175 32.6972 73.3244C34.441 70.8465 34.6245 68.0932 33.5232 65.34C32.6054 62.862 30.4946 61.8525 28.0166 61.5771C25.7223 61.3018 23.4279 61.4854 21.1335 61.3936C20.6746 61.3936 20.5828 61.5771 20.5828 61.9442C20.5828 64.1469 20.5828 66.3495 20.5828 68.4603Z"
fill="#3E3E3E" />
<path d="M128.167 92.3636H114L133.833 44V71.6364H148L128.167 120V92.3636Z" fill="#F7D94B" />
<style>
@media (prefers-color-scheme: dark) {
.a {
fill: #4AA6C0;
}
.b {
fill: #EEEEEE;
}
}
</style>
</svg>
import {
createAppleSplashScreens,
defineConfig,
minimal2023Preset,
} from '@vite-pwa/assets-generator/config'
export default defineConfig({
headLinkOptions: {
preset: '2023',
},
preset: {
...minimal2023Preset,
appleSplashScreens: createAppleSplashScreens({
padding: 0.3,
resizeOptions: { fit: 'contain', background: 'white' },
darkResizeOptions: { fit: 'contain', background: 'black' },
linkMediaOptions: {
log: true,
addMediaScreen: true,
xhtml: true,
},
}, ['iPad Air 9.7"']),
},
images: 'public/favicon.svg',
})
{
"extends": "./.nuxt/tsconfig.json"
}
typescript.tsConfig.exclude[]=../src/runtime
<template>
<div>
<NuxtPwaManifest />
<NuxtLoadingIndicator />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
<script setup lang="ts">
// you don't need this: only for testing purposes
const date = useAppConfig().buildDate
</script>
<template>
<main>
<slot />
<footer>Built Date: {{ date }}</footer>
<ClientOnly>
<div
v-if="$pwa?.offlineReady || $pwa?.needRefresh"
class="pwa-toast"
role="alert"
>
<div class="message">
<span v-if="$pwa.offlineReady">
App ready to work offline
</span>
<span v-else>
New content available, click on reload button to update.
</span>
</div>
<button
v-if="$pwa.needRefresh"
@click="$pwa.updateServiceWorker()"
>
Reload
</button>
<button @click="$pwa.cancelPrompt()">
Close
</button>
</div>
<div
v-if="$pwa?.showInstallPrompt && !$pwa?.offlineReady && !$pwa?.needRefresh"
class="pwa-toast"
role="alert"
>
<div class="message">
<span>
Install PWA
</span>
</div>
<button @click="$pwa.install()">
Install
</button>
<button @click="$pwa.cancelInstall()">
Cancel
</button>
</div>
</ClientOnly>
</main>
</template>
<style>
.pwa-toast {
position: fixed;
right: 0;
bottom: 0;
margin: 16px;
padding: 12px;
border: 1px solid #8885;
border-radius: 4px;
z-index: 1;
text-align: left;
box-shadow: 3px 4px 5px 0 #8885;
}
.pwa-toast .message {
margin-bottom: 8px;
}
.pwa-toast button {
border: 1px solid #8885;
outline: none;
margin-right: 5px;
border-radius: 2px;
padding: 3px 10px;
}
</style>
import process from 'node:process'
const sw = process.env.SW === 'true'
export default defineNuxtConfig({
/* ssr: false, */
// typescript,
modules: ['@vite-pwa/nuxt'],
future: {
typescriptBundlerResolution: true,
},
experimental: {
payloadExtraction: true,
watcher: 'parcel',
},
nitro: {
esbuild: {
options: {
target: 'esnext',
},
},
prerender: {
routes: ['/', '/about'],
},
},
imports: {
autoImport: true,
},
appConfig: {
// you don't need to include this: only for testing purposes
buildDate: new Date().toISOString(),
},
vite: {
logLevel: 'info',
},
pwa: {
strategies: sw ? 'injectManifest' : 'generateSW',
srcDir: sw ? 'service-worker' : undefined,
filename: sw ? 'sw.ts' : undefined,
registerType: 'autoUpdate',
manifest: {
name: 'Nuxt Vite PWA',
short_name: 'NuxtVitePWA',
theme_color: '#ffffff',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable',
},
],
},
workbox: {
globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
},
injectManifest: {
globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
},
client: {
installPrompt: true,
// you don't need to include this: only for testing purposes
// if enabling periodic sync for update use 1 hour or so (periodicSyncForUpdates: 3600)
periodicSyncForUpdates: 20,
},
devOptions: {
enabled: true,
suppressWarnings: true,
navigateFallback: '/',
navigateFallbackAllowlist: [/^\/$/],
type: 'module',
},
},
})
{
"name": "playground",
"type": "module",
"private": true,
"scripts": {
"dev": "nuxi dev",
"dev-sw": "SW=true nuxi dev",
"build": "nuxi build",
"build-sw": "SW=true nuxi build",
"generate": "nuxi generate"
},
"devDependencies": {
"@vite-pwa/nuxt": "workspace:*",
"nuxt": "^3.10.1"
}
}
<script setup lang="ts">
</script>
<template>
<div>
<h1>Nuxt Vite PWA</h1>
<ClientOnly>
PWA Installed: {{ $pwa?.isPWAInstalled }}
</ClientOnly>
<NuxtLink to="/">
Home 1
</NuxtLink>
</div>
</template>
<template>
<div>
<h1>Nuxt Vite PWA</h1>
<ClientOnly>
PWA Installed: {{ $pwa?.isPWAInstalled }}
</ClientOnly>
<NuxtLink to="/about">
About
</NuxtLink>
</div>
</template>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" id="svg20" width="410" height="404" fill="none" version="1.1" viewBox="0 0 410 404"><metadata id="metadata24"/><path id="path2" fill="url(#paint0_linear)" d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z"/><defs id="defs18"><linearGradient id="paint0_linear" x1="6" x2="235" y1="33" y2="344" gradientUnits="userSpaceOnUse"><stop id="stop6" stop-color="#41D1FF"/><stop id="stop8" offset="1" stop-color="#BD34FE"/></linearGradient><linearGradient id="paint1_linear" x1="194.651" x2="236.076" y1="8.818" y2="292.989" gradientUnits="userSpaceOnUse"><stop id="stop11" stop-color="#FFEA83"/><stop id="stop13" offset=".083" stop-color="#FFDD35"/><stop id="stop15" offset="1" stop-color="#FFA800"/></linearGradient></defs><path id="path4" fill="url(#paint1_linear)" d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z"/><g id="layer1"><g id="g8" transform="matrix(0.15789659,0,0,0.15890333,54.892928,275.21638)"><path id="path2-1" fill="#3d3d3d" fill-opacity="1" stroke-linejoin="round" stroke-width=".2" d="m 1436.62,603.304 56.39,-142.599 h 162.82 L 1578.56,244.39 1675.2,5.28336e-4 1952,734.933 h -204.13 l -47.3,-131.629 z" style="fill:#3e3e3e;fill-opacity:1"/><path id="path4-4" fill="#5a0fc8" fill-opacity="1" stroke-linejoin="round" stroke-width=".2" d="M 1262.47,734.935 1558.79,0.00156593 1362.34,0.0025425 1159.64,474.933 1015.5,0.00351906 H 864.499 L 709.731,474.933 600.585,258.517 501.812,562.819 602.096,734.935 h 193.331 l 139.857,-425.91 133.346,425.91 z" style="fill:#2e859c;fill-opacity:1"/><path id="path6" fill="#3d3d3d" fill-opacity="1" stroke-linejoin="round" stroke-width=".2" d="m 186.476,482.643 h 121.003 c 36.654,0 69.293,-4.091 97.917,-12.273 l 31.293,-96.408 87.459,-269.446 C 517.484,93.9535 509.876,83.9667 501.324,74.5569 456.419,24.852 390.719,4.06265e-4 304.222,4.06265e-4 H -3.8147e-6 V 734.933 H 186.476 Z M 346.642,169.079 c 17.54,17.653 26.309,41.276 26.309,70.871 0,29.822 -7.713,53.474 -23.138,70.956 -16.91,19.425 -48.047,29.137 -93.409,29.137 H 186.476 V 142.598 h 70.442 c 42.277,0 72.185,8.827 89.724,26.481 z" style="fill:#3e3e3e;fill-opacity:1"/></g></g></svg>
\ No newline at end of file
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
/// <reference lib="WebWorker" />
/// <reference types="vite/client" />
import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching'
import { clientsClaim } from 'workbox-core'
import { NavigationRoute, registerRoute } from 'workbox-routing'
declare let self: ServiceWorkerGlobalScope
// self.__WB_MANIFEST is default injection point
precacheAndRoute(self.__WB_MANIFEST)
// clean old assets
cleanupOutdatedCaches()
let allowlist: undefined | RegExp[]
if (import.meta.env.DEV)
allowlist = [/^\/$/]
// to allow work offline
registerRoute(new NavigationRoute(
createHandlerBoundToURL('/'),
{ allowlist },
))
self.skipWaiting()
clientsClaim()
{
"extends": "./.nuxt/tsconfig.json"
}
import process from 'node:process'
import { defineConfig, devices } from '@playwright/test'
const url = 'http://localhost:4173'
const build = process.env.TEST_BUILD === 'true'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './client-test',
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
outputDir: 'test-results/',
timeout: 5 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 1000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'line',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: url,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ..devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: `pnpm run test:${build ? 'build' : 'generate'}:serve`,
url,
reuseExistingServer: !process.env.CI,
},
})
因为 它太大了无法显示 source diff 。你可以改为 查看blob
packages:
- playground/
- playground-assets/
import {
defineNuxtModule,
} from '@nuxt/kit'
import { version } from '../package.json'
import type { PwaModuleOptions } from './types'
import { doSetup } from './utils/module'
export * from './types'
export default defineNuxtModule<PwaModuleOptions>({
meta: {
name: 'pwa',
configKey: 'pwa',
compatibility: {
nuxt: '^3.6.5',
bridge: false,
},
version,
},
defaults: nuxt => ({
base: nuxt.options.app.baseURL,
scope: nuxt.options.app.baseURL,
injectRegister: false,
includeManifestIcons: false,
registerPlugin: true,
writePlugin: false,
client: {
registerPlugin: true,
installPrompt: false,
periodicSyncForUpdates: 0,
},
}),
async setup(options, nuxt) {
await doSetup(options, nuxt)
},
})
export interface ModuleOptions extends PwaModuleOptions {}
declare module '@nuxt/schema' {
interface NuxtConfig {
['pwa']?: Partial<ModuleOptions>
}
interface NuxtOptions {
['pwa']?: ModuleOptions
}
}
import type { MetaObject } from '@nuxt/schema'
import { defineComponent, ref } from 'vue'
import { pwaInfo } from 'virtual:pwa-info'
import { pwaAssetsHead } from 'virtual:pwa-assets/head'
import { useHead } from '#imports'
export default defineComponent({
setup() {
const meta = ref<MetaObject>({ link: [] })
useHead(meta)
if (pwaAssetsHead.themeColor)
meta.value.meta = [{ name: 'theme-color', content: pwaAssetsHead.themeColor.content }]
if (pwaAssetsHead.links.length)
// @ts-expect-error: links are fine
meta.value.link!.push(...pwaAssetsHead.links)
if (pwaInfo) {
const { webManifest } = pwaInfo
if (webManifest) {
const { href, useCredentials } = webManifest
if (useCredentials) {
meta.value.link!.push({
rel: 'manifest',
href,
crossorigin: 'use-credentials',
})
}
else {
meta.value.link!.push({
rel: 'manifest',
href,
})
}
}
}
return () => null
},
})
<script setup lang="ts">
import { useApplePwaIcon } from '#pwa'
import type { PwaAppleImageProps } from '#build/pwa-icons/PwaAppleImage'
const props = defineProps<PwaAppleImageProps>()
const { icon } = useApplePwaIcon(props)
</script>
<template>
<img v-if="icon" v-bind="icon">
</template>
const _default: typeof import('#build/pwa-icons/PwaAppleImage')['default']
export default _default
<script setup lang="ts">
import { useAppleSplashScreenPwaIcon } from '#pwa'
import type { PwaAppleSplashScreenImageProps } from '#build/pwa-icons/PwaAppleSplashScreenImage'
const props = defineProps<PwaAppleSplashScreenImageProps>()
const { icon } = useAppleSplashScreenPwaIcon(props)
</script>
<template>
<img v-if="icon" v-bind="icon">
</template>
const _default: typeof import('#build/pwa-icons/PwaAppleSplashScreenImage')['default']
export default _default
<script setup lang="ts">
import { useFaviconPwaIcon } from '#pwa'
import type { PwaFaviconImageProps } from '#build/pwa-icons/PwaFaviconImage'
const props = defineProps<PwaFaviconImageProps>()
const { icon } = useFaviconPwaIcon(props)
</script>
<template>
<img v-if="icon" v-bind="icon">
</template>
const _default: typeof import('#build/pwa-icons/PwaFaviconImage')['default']
export default _default
<script setup lang="ts">
import { useMaskablePwaIcon } from '#pwa'
import type { PwaMaskableImageProps } from '#build/pwa-icons/PwaMaskableImage'
const props = defineProps<PwaMaskableImageProps>()
const { icon } = useMaskablePwaIcon(props)
</script>
<template>
<img v-if="icon" v-bind="icon">
</template>
const _default: typeof import('#build/pwa-icons/PwaMaskableImage')['default']
export default _default
<script setup lang="ts">
import { useTransparentPwaIcon } from '#pwa'
import type { PwaTransparentImageProps } from '#build/pwa-icons/PwaTransparentImage'
const props = defineProps<PwaTransparentImageProps>()
const { icon } = useTransparentPwaIcon(props)
</script>
<template>
<img v-if="icon" v-bind="icon">
</template>
const _default: typeof import('#build/pwa-icons/PwaTransparentImage')['default']
export default _default
import type { MetaObject } from '@nuxt/schema'
import { defineComponent, ref } from 'vue'
import { pwaInfo } from 'virtual:pwa-info'
import { useHead } from '#imports'
export default defineComponent({
async setup() {
if (pwaInfo) {
const meta = ref<MetaObject>({ link: [] })
useHead(meta)
const { webManifest } = pwaInfo
if (webManifest) {
const { href, useCredentials } = webManifest
if (useCredentials) {
meta.value.link!.push({
rel: 'manifest',
href,
crossorigin: 'use-credentials',
})
}
else {
meta.value.link!.push({
rel: 'manifest',
href,
})
}
}
}
return () => null
},
})
import { computed, toValue } from 'vue'
import type { MaybeRef } from 'vue'
import { useNuxtApp } from '#imports'
import type { PwaTransparentImageProps } from '#build/pwa-icons/PwaTransparentImage'
import type { PwaMaskableImageProps } from '#build/pwa-icons/PwaMaskableImage'
import type { PwaFaviconImageProps } from '#build/pwa-icons/PwaFaviconImage'
import type { PwaAppleImageProps } from '#build/pwa-icons/PwaAppleImage'
import type { PwaAppleSplashScreenImageProps } from '#build/pwa-icons/PwaAppleSplashScreenImage'
export interface PWAImage {
image: string
alt?: string
width?: number
height?: number
crossorigin?: '' | 'anonymous' | 'use-credentials'
loading?: 'lazy' | 'eager'
decoding?: 'async' | 'auto' | 'sync'
nonce?: string
[key: string]: any
}
export interface PWAIcon {
src: string
key: any
alt?: string
width?: number
height?: number
crossorigin?: '' | 'anonymous' | 'use-credentials'
loading?: 'lazy' | 'eager'
decoding?: 'async' | 'auto' | 'sync'
nonce?: string
[key: string]: any
}
type PWAImageType<T> = T extends 'transparent'
? PwaTransparentImageProps['image'] | (Omit<PWAImage, 'image'> & { image: PwaTransparentImageProps['image'] })
: T extends 'maskable'
? PwaMaskableImageProps['image'] | Omit<PWAImage, 'image'> & { image: PwaMaskableImageProps['image'] }
: T extends 'favicon'
? PwaFaviconImageProps['image'] | Omit<PWAImage, 'image'> & { image: PwaFaviconImageProps['image'] }
: T extends 'apple'
? PwaAppleImageProps['image'] | Omit<PWAImage, 'image'> & { image: PwaAppleImageProps['image'] }
: T extends 'appleSplashScreen'
? PwaAppleSplashScreenImageProps['image'] | Omit<PWAImage, 'image'> & { image: PwaAppleSplashScreenImageProps['image'] }
: never
export type TransparentImageType = MaybeRef<PWAImageType<'transparent'>>
export type MaskableImageType = MaybeRef<PWAImageType<'maskable'>>
export type FaviconImageType = MaybeRef<PWAImageType<'favicon'>>
export type AppleImageType = MaybeRef<PWAImageType<'apple'>>
export type AppleSplashScreenImageType = MaybeRef<PWAImageType<'appleSplashScreen'>>
export function useTransparentPwaIcon(image: TransparentImageType) {
return usePWAIcon('transparent', image)
}
export function useMaskablePwaIcon(image: MaskableImageType) {
return usePWAIcon('maskable', image)
}
export function useFaviconPwaIcon(image: FaviconImageType) {
return usePWAIcon('favicon', image)
}
export function useApplePwaIcon(image: AppleImageType) {
return usePWAIcon('apple', image)
}
export function useAppleSplashScreenPwaIcon(image: AppleSplashScreenImageType) {
return usePWAIcon('appleSplashScreen', image)
}
function usePWAIcon(
type: 'transparent' | 'maskable' | 'favicon' | 'apple' | 'appleSplashScreen',
pwaImage: MaybeRef<string | PWAImage>,
) {
const pwaIcons = useNuxtApp().$pwaIcons
const icon = computed(() => {
const pwaIcon = toValue(pwaImage)
const iconName = typeof pwaIcon === 'object' ? pwaIcon.image : pwaIcon
const image = pwaIcons?.[type]?.[iconName]?.asImage
if (!image)
return
if (typeof pwaIcon === 'string') {
return <PWAIcon>{
width: image.width,
height: image.height,
key: image.key,
src: image.src,
}
}
const {
alt,
width,
height,
crossorigin,
loading,
decoding,
nonce,
image: _image,
...rest
} = pwaIcon
return <PWAIcon>{
alt,
width: width ?? image.width,
height: height ?? image.height,
crossorigin,
loading,
decoding,
nonce,
...rest,
key: image.key,
src: image.src,
}
})
return { icon }
}
import { defineNuxtPlugin } from '#imports'
import { pwaAssetsIcons } from 'virtual:pwa-assets/icons'
export default defineNuxtPlugin(() => {
const pwaIcons = {}
configureEntries(pwaIcons, 'transparent')
configureEntries(pwaIcons, 'maskable')
configureEntries(pwaIcons, 'favicon')
configureEntries(pwaIcons, 'apple')
configureEntries(pwaIcons, 'appleSplashScreen')
return {
provide: {
pwaIcons,
},
}
})
function configureEntries(pwaIcons, key) {
pwaIcons[key] = Object.values(pwaAssetsIcons[key] ?? {}).reduce((acc, icon) => {
const entry = {
...icon,
asImage: {
src: icon.url,
key: `${key}-${icon.name}`,
}
}
if (icon.width && icon.height) {
entry.asImage.width = icon.width
entry.asImage.height = icon.height
}
acc[icon.name] = entry
return acc
}, {})
}
import { nextTick, reactive, ref } from 'vue'
import type { UnwrapNestedRefs } from 'vue'
import { useRegisterSW } from 'virtual:pwa-register/vue'
import { display, installPrompt, periodicSyncForUpdates } from 'virtual:nuxt-pwa-configuration'
import type { PwaInjection } from './types'
import { defineNuxtPlugin } from '#imports'
import type { Plugin } from '#app'
const plugin: Plugin<{
pwa?: UnwrapNestedRefs<PwaInjection>
}> = defineNuxtPlugin(() => {
const registrationError = ref(false)
const swActivated = ref(false)
const showInstallPrompt = ref(false)
const hideInstall = ref(!installPrompt ? true : localStorage.getItem(installPrompt) === 'true')
// https://thomashunter.name/posts/2021-12-11-detecting-if-pwa-twa-is-installed
const ua = navigator.userAgent
const ios = ua.match(/iPhone|iPad|iPod/)
const useDisplay = display === 'standalone' || display === 'minimal-ui' ? `${display}` : 'standalone'
const standalone = window.matchMedia(`(display-mode: ${useDisplay})`).matches
const isInstalled = ref(!!(standalone || (ios && !ua.match(/Safari/))))
const isPWAInstalled = ref(isInstalled.value)
window.matchMedia(`(display-mode: ${useDisplay})`).addEventListener('change', (e) => {
// PWA on fullscreen mode will not match standalone nor minimal-ui
if (!isPWAInstalled.value && e.matches)
isPWAInstalled.value = true
})
let swRegistration: ServiceWorkerRegistration | undefined
const getSWRegistration = () => swRegistration
const registerPeriodicSync = (swUrl: string, r: ServiceWorkerRegistration, timeout: number) => {
setInterval(async () => {
if (('connection' in navigator) && !navigator.onLine)
return
const resp = await fetch(swUrl, {
cache: 'no-store',
headers: {
'cache': 'no-store',
'cache-control': 'no-cache',
},
})
if (resp?.status === 200)
await r.update()
}, timeout)
}
const {
offlineReady, needRefresh, updateServiceWorker,
} = useRegisterSW({
immediate: true,
onRegisterError() {
registrationError.value = true
},
onRegisteredSW(swUrl, r) {
swRegistration = r
const timeout = periodicSyncForUpdates
if (timeout > 0) {
// should add support in pwa plugin
if (r?.active?.state === 'activated') {
swActivated.value = true
registerPeriodicSync(swUrl, r, timeout * 1000)
}
else if (r?.installing) {
r.installing.addEventListener('statechange', (e) => {
const sw = e.target as ServiceWorker
swActivated.value = sw.state === 'activated'
if (swActivated.value)
registerPeriodicSync(swUrl, r, timeout * 1000)
})
}
}
},
})
const cancelPrompt = async () => {
offlineReady.value = false
needRefresh.value = false
}
let install: () => Promise<void> = () => Promise.resolve()
let cancelInstall: () => void = () => {}
if (!hideInstall.value) {
type InstallPromptEvent = Event & {
prompt: () => void
userChoice: Promise<{ outcome: 'dismissed' | 'accepted' }>
}
let deferredPrompt: InstallPromptEvent | undefined
const beforeInstallPrompt = (e: Event) => {
e.preventDefault()
deferredPrompt = e as InstallPromptEvent
showInstallPrompt.value = true
}
window.addEventListener('beforeinstallprompt', beforeInstallPrompt)
window.addEventListener('appinstalled', () => {
deferredPrompt = undefined
showInstallPrompt.value = false
})
cancelInstall = () => {
deferredPrompt = undefined
showInstallPrompt.value = false
window.removeEventListener('beforeinstallprompt', beforeInstallPrompt)
hideInstall.value = true
localStorage.setItem(installPrompt!, 'true')
}
install = async () => {
if (!showInstallPrompt.value || !deferredPrompt) {
showInstallPrompt.value = false
return
}
showInstallPrompt.value = false
await nextTick()
deferredPrompt.prompt()
await deferredPrompt.userChoice
}
}
return {
provide: {
pwa: reactive({
isInstalled,
isPWAInstalled,
showInstallPrompt,
cancelInstall,
install,
swActivated,
registrationError,
offlineReady,
needRefresh,
updateServiceWorker,
cancelPrompt,
getSWRegistration,
}) satisfies UnwrapNestedRefs<PwaInjection>,
},
}
})
export default plugin
import type { Ref } from 'vue'
import type { UnwrapNestedRefs } from 'vue'
export interface PwaInjection {
/**
* @deprecated use `isPWAInstalled` instead
*/
isInstalled: boolean
isPWAInstalled: Ref<boolean>
showInstallPrompt: Ref<boolean>
cancelInstall: () => void
install: () => Promise<void>
swActivated: Ref<boolean>
registrationError: Ref<boolean>
offlineReady: Ref<boolean>
needRefresh: Ref<boolean>
updateServiceWorker: (reloadPage?: boolean | undefined) => Promise<void>
cancelPrompt: () => Promise<void>
getSWRegistration: () => ServiceWorkerRegistration | undefined
}
declare module '#app' {
interface NuxtApp {
$pwa?: UnwrapNestedRefs<PwaInjection>
}
}
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$pwa?: UnwrapNestedRefs<PwaInjection>
}
}
export {}
import type { VitePWAOptions } from 'vite-plugin-pwa'
export interface ClientOptions {
/**
* Exposes the plugin: defaults to true.
*/
registerPlugin?: boolean
/**
* Registers a periodic sync for updates interval: value in seconds.
*/
periodicSyncForUpdates?: number
/**
* Will prevent showing native PWA install prompt: defaults to false.
*
* When set to true or no empty string, the native PWA install prompt will be prevented.
*
* When set to a string, it will be used as the key in `localStorage` to prevent show the PWA install prompt widget.
*
* When set to true, the key used will be `vite-pwa:hide-install`.
*/
installPrompt?: boolean | string
}
export interface PwaModuleOptions extends Partial<VitePWAOptions> {
registerWebManifestInRouteRules?: boolean
/**
* Writes the plugin to disk: defaults to false (debug).
*/
writePlugin?: boolean
/**
* Options for plugin.
*/
client?: ClientOptions
}
import { lstat } from 'node:fs/promises'
import { createHash } from 'node:crypto'
import { createReadStream } from 'node:fs'
import type { Nuxt } from '@nuxt/schema'
import { resolve } from 'pathe'
import type { NitroConfig } from 'nitropack'
import type { PwaModuleOptions } from '../types'
export function configurePWAOptions(
nuxt3_8: boolean,
options: PwaModuleOptions,
nuxt: Nuxt,
nitroConfig: NitroConfig,
) {
if (!options.outDir) {
const publicDir = nitroConfig.output?.publicDir ?? nuxt.options.nitro?.output?.publicDir
options.outDir = publicDir ? resolve(publicDir) : resolve(nuxt.options.buildDir, '../.output/public')
}
// generate dev sw in .nuxt folder: we don't need to remove it
if (options.devOptions?.enabled)
options.devOptions.resolveTempFolder = () => resolve(nuxt.options.buildDir, 'dev-sw-dist')
let config: Partial<
import('workbox-build').BasePartial
& import('workbox-build').GlobPartial
& import('workbox-build').RequiredGlobDirectoryPartial
>
if (options.strategies === 'injectManifest') {
options.injectManifest = options.injectManifest ?? {}
config = options.injectManifest
}
else {
options.workbox = options.workbox ?? {}
if (options.registerType === 'autoUpdate' && (options.client?.registerPlugin || options.injectRegister === 'script' || options.injectRegister === 'inline')) {
options.workbox.clientsClaim = true
options.workbox.skipWaiting = true
}
if (nuxt.options.dev) {
// on dev force always to use the root
options.workbox.navigateFallback = options.workbox.navigateFallback ?? nuxt.options.app.baseURL ?? '/'
if (options.devOptions?.enabled && !options.devOptions.navigateFallbackAllowlist)
options.devOptions.navigateFallbackAllowlist = [nuxt.options.app.baseURL ? new RegExp(nuxt.options.app.baseURL) : /\//]
}
// the user may want to disable offline support
if (!('navigateFallback' in options.workbox))
options.workbox.navigateFallback = nuxt.options.app.baseURL ?? '/'
config = options.workbox
}
let buildAssetsDir = nuxt.options.app.buildAssetsDir ?? '_nuxt/'
if (buildAssetsDir[0] === '/')
buildAssetsDir = buildAssetsDir.slice(1)
if (buildAssetsDir[buildAssetsDir.length - 1] !== '/')
buildAssetsDir += '/'
// Vite 5 support: allow override dontCacheBustURLsMatching
if (!('dontCacheBustURLsMatching' in config))
config.dontCacheBustURLsMatching = new RegExp(buildAssetsDir)
// handle payload extraction
if (nuxt.options.experimental.payloadExtraction) {
const enableGlobPatterns = nuxt.options._generate
|| (
!!nitroConfig.prerender?.routes?.length
|| Object.values(nitroConfig.routeRules ?? {}).some(r => r.prerender)
)
if (enableGlobPatterns) {
config.globPatterns = config.globPatterns ?? []
config.globPatterns.push('**/_payload.json')
}
}
// handle Nuxt App Manifest
let appManifestFolder: string | undefined
if (nuxt3_8 && nuxt.options.experimental.appManifest) {
config.globPatterns = config.globPatterns ?? []
appManifestFolder = `${buildAssetsDir}builds/`
config.globPatterns.push(`${appManifestFolder}**/*.json`)
}
// allow override manifestTransforms
if (!nuxt.options.dev && !config.manifestTransforms)
config.manifestTransforms = [createManifestTransform(nuxt.options.app.baseURL ?? '/', options.outDir, appManifestFolder)]
if (options.pwaAssets) {
options.pwaAssets.integration = {
baseUrl: nuxt.options.app.baseURL ?? '/',
publicDir: nuxt.options.dir.public,
outDir: options.outDir,
}
}
}
function createManifestTransform(
base: string,
publicFolder: string,
appManifestFolder?: string,
): import('workbox-build').ManifestTransform {
return async (entries) => {
entries.filter(e => e.url.endsWith('.html')).forEach((e) => {
const url = e.url.startsWith('/') ? e.url.slice(1) : e.url
if (url === 'index.html') {
e.url = base
}
else {
const parts = url.split('/')
parts[parts.length - 1] = parts[parts.length - 1].replace(/\.html$/, '')
e.url = parts.length > 1 ? parts.slice(0, parts.length - 1).join('/') : parts[0]
}
})
if (appManifestFolder) {
// this shouldn't be necessary, since we are using dontCacheBustURLsMatching
const regExp = /(\/)?[0-9a-f]{8}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{12}\.json$/i
// we need to remove the revision from the sw prechaing manifest, UUID is enough:
// we don't use dontCacheBustURLsMatching, single regex
entries.filter(e => e.url.startsWith(appManifestFolder) && regExp.test(e.url)).forEach((e) => {
e.revision = null
})
// add revision to latest.json file: we are excluding `_nuxt/` assets from dontCacheBustURLsMatching
const latest = `${appManifestFolder}latest.json`
const latestJson = resolve(publicFolder, latest)
const data = await lstat(latestJson).catch(() => undefined)
if (data?.isFile()) {
const revision = await new Promise<string>((resolve, reject) => {
const cHash = createHash('MD5')
const stream = createReadStream(latestJson)
stream.on('error', (err) => {
reject(err)
})
stream.on('data', chunk => cHash.update(chunk))
stream.on('end', () => {
resolve(cHash.digest('hex'))
})
})
const latestEntry = entries.find(e => e.url === latest)
if (latestEntry)
latestEntry.revision = revision
else
entries.push({ url: latest, revision, size: data.size })
}
else {
entries = entries.filter(e => e.url !== latest)
}
}
return { manifest: entries, warnings: [] }
}
}
import { join } from 'node:path'
import { mkdir } from 'node:fs/promises'
import { addComponent, addPlugin, createResolver, extendWebpackConfig, getNuxtVersion } from '@nuxt/kit'
import type { Plugin } from 'vite'
import type { Nuxt } from '@nuxt/schema'
import type { VitePluginPWAAPI } from 'vite-plugin-pwa'
import { VitePWA } from 'vite-plugin-pwa'
import type { PwaModuleOptions } from '../types'
import { configurePWAOptions } from './config'
import { regeneratePWA, writeWebManifest } from './utils'
import { registerPwaIconsTypes } from './pwa-icons-types'
export async function doSetup(options: PwaModuleOptions, nuxt: Nuxt) {
const resolver = createResolver(import.meta.url)
const nuxtVersion = (getNuxtVersion(nuxt) as string).split('.').map(v => Number.parseInt(v))
const nuxt3_8 = nuxtVersion.length > 1 && (nuxtVersion[0] > 3 || (nuxtVersion[0] === 3 && nuxtVersion[1] >= 8))
let vitePwaClientPlugin: Plugin | undefined
const resolveVitePluginPWAAPI = (): VitePluginPWAAPI | undefined => {
return vitePwaClientPlugin?.api
}
const client = options.client ?? { registerPlugin: true, installPrompt: false, periodicSyncForUpdates: 0 }
/* if (client.registerPlugin) {
addPluginTemplate({
src: resolver.resolve('../templates/pwa.client.ts'),
write: nuxt.options.dev || options.writePlugin,
options: {
periodicSyncForUpdates: typeof client.periodicSyncForUpdates === 'number' ? client.periodicSyncForUpdates : 0,
installPrompt: (typeof client.installPrompt === 'undefined' || client.installPrompt === false)
? undefined
: (client.installPrompt === true || client.installPrompt.trim() === '')
? 'vite-pwa:hide-install'
: client.installPrompt.trim(),
},
})
} */
const runtimeDir = resolver.resolve('../runtime')
if (!nuxt.options.ssr)
nuxt.options.build.transpile.push(runtimeDir)
if (client.registerPlugin) {
addPlugin({
src: resolver.resolve(runtimeDir, 'plugins/pwa.client'),
mode: 'client',
})
}
addPlugin({
src: resolver.resolve(runtimeDir, 'plugins/pwa-icons.mjs'),
})
await Promise.all([
addComponent({
name: 'VitePwaManifest',
filePath: resolver.resolve(runtimeDir, 'components/VitePwaManifest'),
}),
addComponent({
name: 'NuxtPwaManifest',
filePath: resolver.resolve(runtimeDir, 'components/VitePwaManifest'),
}),
addComponent({
name: 'NuxtPwaAssets',
filePath: resolver.resolve(runtimeDir, 'components/NuxtPwaAssets'),
}),
addComponent({
name: 'PwaAppleImage',
filePath: resolver.resolve(runtimeDir, 'components/PwaAppleImage.vue'),
}),
addComponent({
name: 'PwaAppleSplashScreenImage',
filePath: resolver.resolve(runtimeDir, 'components/PwaAppleSplashScreenImage.vue'),
}),
addComponent({
name: 'PwaFaviconImage',
filePath: resolver.resolve(runtimeDir, 'components/PwaFaviconImage.vue'),
}),
addComponent({
name: 'PwaMaskableImage',
filePath: resolver.resolve(runtimeDir, 'components/PwaMaskableImage.vue'),
}),
addComponent({
name: 'PwaTransparentImage',
filePath: resolver.resolve(runtimeDir, 'components/PwaTransparentImage.vue'),
}),
])
nuxt.hook('prepare:types', ({ references }) => {
references.push({ path: resolver.resolve(runtimeDir, 'plugins/types') })
references.push({ types: '@vite-pwa/nuxt/configuration' })
references.push({ types: 'vite-plugin-pwa/vue' })
references.push({ types: 'vite-plugin-pwa/info' })
references.push({ types: 'vite-plugin-pwa/pwa-assets' })
})
const pwaAssets = await registerPwaIconsTypes(options, nuxt, resolver, runtimeDir)
const manifestDir = join(nuxt.options.buildDir, 'manifests')
nuxt.options.nitro.publicAssets = nuxt.options.nitro.publicAssets || []
nuxt.options.nitro.publicAssets.push({
dir: manifestDir,
maxAge: 0,
})
nuxt.hook('nitro:init', (nitro) => {
configurePWAOptions(nuxt3_8, options, nuxt, nitro.options)
})
nuxt.hook('vite:extend', ({ config }) => {
const plugin = config.plugins?.find(p => p && typeof p === 'object' && 'name' in p && p.name === 'vite-plugin-pwa')
if (plugin)
throw new Error('Remove vite-plugin-pwa plugin from Vite Plugins entry in Nuxt config file!')
})
nuxt.hook('vite:extendConfig', async (viteInlineConfig, { isClient }) => {
viteInlineConfig.plugins = viteInlineConfig.plugins || []
const plugin = viteInlineConfig.plugins.find(p => p && typeof p === 'object' && 'name' in p && p.name === 'vite-plugin-pwa')
if (plugin)
throw new Error('Remove vite-plugin-pwa plugin from Vite Plugins entry in Nuxt config file!')
if (options.manifest && isClient) {
viteInlineConfig.plugins.push({
name: 'vite-pwa-nuxt:webmanifest:build',
apply: 'build',
async writeBundle(_options, bundle) {
if (options.disable || !bundle)
return
const api = resolveVitePluginPWAAPI()
if (api) {
await mkdir(manifestDir, { recursive: true })
await writeWebManifest(manifestDir, options.manifestFilename || 'manifest.webmanifest', api, pwaAssets)
}
},
})
}
if (isClient) {
viteInlineConfig.plugins = viteInlineConfig.plugins || []
const configuration = 'virtual:nuxt-pwa-configuration'
const resolvedConfiguration = `\0${configuration}`
viteInlineConfig.plugins.push({
name: 'vite-pwa-nuxt:configuration',
enforce: 'pre',
resolveId(id) {
if (id === configuration)
return resolvedConfiguration
},
load(id) {
if (id === resolvedConfiguration) {
const display = typeof options.manifest !== 'boolean' ? options.manifest?.display ?? 'standalone' : 'standalone'
const installPrompt = (typeof client.installPrompt === 'undefined' || client.installPrompt === false)
? undefined
: (client.installPrompt === true || client.installPrompt.trim() === '')
? 'vite-pwa:hide-install'
: client.installPrompt.trim()
return `export const enabled = ${client.registerPlugin}
export const display = '${display}'
export const installPrompt = ${JSON.stringify(installPrompt)}
export const periodicSyncForUpdates = ${typeof client.periodicSyncForUpdates === 'number' ? client.periodicSyncForUpdates : 0}
`
}
},
})
}
// remove vite plugin pwa build plugin
const plugins = [...VitePWA(options).filter(p => p.name !== 'vite-plugin-pwa:build')]
viteInlineConfig.plugins.push(plugins)
if (isClient)
vitePwaClientPlugin = plugins.find(p => p.name === 'vite-plugin-pwa') as Plugin
})
extendWebpackConfig(() => {
throw new Error('Webpack is not supported: @vite-pwa/nuxt module can only be used with Vite!')
})
if (nuxt.options.dev) {
const webManifest = `${nuxt.options.app.baseURL}${options.devOptions?.webManifestUrl ?? options.manifestFilename ?? 'manifest.webmanifest'}`
const devSw = `${nuxt.options.app.baseURL}dev-sw.js?dev-sw`
const workbox = `${nuxt.options.app.baseURL}workbox-`
// @ts-expect-error just ignore
const emptyHandle = (_req, _res, next) => {
next()
}
nuxt.hook('vite:serverCreated', (viteServer, { isServer }) => {
if (isServer)
return
viteServer.middlewares.stack.push({ route: webManifest, handle: emptyHandle })
viteServer.middlewares.stack.push({ route: devSw, handle: emptyHandle })
if (options.pwaAssets) {
viteServer.middlewares.stack.push({
route: '',
// @ts-expect-error just ignore
handle: async (req, res, next) => {
const url = req.url
if (!url)
return next()
if (!/\.(ico|png|svg|webp)$/.test(url))
return next()
const pwaAssetsGenerator = await resolveVitePluginPWAAPI()?.pwaAssetsGenerator()
if (!pwaAssetsGenerator)
return next()
const icon = await pwaAssetsGenerator.findIconAsset(url)
if (!icon)
return next()
if (icon.age > 0) {
const ifModifiedSince = req.headers['if-modified-since'] ?? req.headers['If-Modified-Since']
const useIfModifiedSince = ifModifiedSince ? Array.isArray(ifModifiedSince) ? ifModifiedSince[0] : ifModifiedSince : undefined
if (useIfModifiedSince && new Date(icon.lastModified).getTime() / 1000 >= new Date(useIfModifiedSince).getTime() / 1000) {
res.statusCode = 304
res.end()
return
}
}
const buffer = await icon.buffer
res.setHeader('Age', icon.age / 1000)
res.setHeader('Content-Type', icon.mimeType)
res.setHeader('Content-Length', buffer.length)
res.setHeader('Last-Modified', new Date(icon.lastModified).toUTCString())
res.statusCode = 200
res.end(buffer)
},
})
}
})
if (!options.strategies || options.strategies === 'generateSW') {
nuxt.hook('vite:serverCreated', (viteServer, { isServer }) => {
if (isServer)
return
viteServer.middlewares.stack.push({ route: workbox, handle: emptyHandle })
})
if (options.devOptions?.suppressWarnings) {
const suppressWarnings = `${nuxt.options.app.baseURL}suppress-warnings.js`
nuxt.hook('vite:serverCreated', (viteServer, { isServer }) => {
if (isServer)
return
viteServer.middlewares.stack.push({ route: suppressWarnings, handle: emptyHandle })
})
}
}
}
else {
if (!options.disable && options.registerWebManifestInRouteRules) {
nuxt.hook('nitro:config', async (nitroConfig) => {
nitroConfig.routeRules = nitroConfig.routeRules || {}
let swName = options.filename || 'sw.js'
if (options.strategies === 'injectManifest' && swName.endsWith('.ts'))
swName = swName.replace(/\.ts$/, '.js')
nitroConfig.routeRules[`${nuxt.options.app.baseURL}${swName}`] = {
headers: {
'Cache-Control': 'public, max-age=0, must-revalidate',
},
}
// if provided by the user, we don't know web manifest name
if (options.manifest) {
nitroConfig.routeRules[`${nuxt.options.app.baseURL}${options.manifestFilename ?? 'manifest.webmanifest'}`] = {
headers: {
'Content-Type': 'application/manifest+json',
'Cache-Control': 'public, max-age=0, must-revalidate',
},
}
}
})
}
if (nuxt3_8) {
nuxt.hook('nitro:build:public-assets', async () => {
await regeneratePWA(
options.outDir!,
pwaAssets,
resolveVitePluginPWAAPI(),
)
})
}
else {
nuxt.hook('nitro:init', (nitro) => {
nitro.hooks.hook('rollup:before', async () => {
await regeneratePWA(
options.outDir!,
pwaAssets,
resolveVitePluginPWAAPI(),
)
})
})
if (nuxt.options._generate) {
nuxt.hook('close', async () => {
await regeneratePWA(
options.outDir!,
pwaAssets,
resolveVitePluginPWAAPI(),
)
})
}
}
}
}
import { addTypeTemplate } from '@nuxt/kit'
export interface DtsInfo {
dts?: string
transparent?: string
maskable?: string
favicon?: string
apple?: string
appleSplashScreen?: string
}
export interface PwaIconsTypes {
transparent?: string[]
maskable?: string[]
favicon?: string[]
apple?: string[]
appleSplashScreen?: string[]
}
export function addPwaTypeTemplate(
filename: string,
content?: string,
) {
if (content?.length) {
addTypeTemplate({
write: true,
filename: `pwa-icons/${filename}.d.ts`,
getContents: () => content,
})
}
else {
addTypeTemplate({
write: true,
getContents: () => generatePwaImageType(filename),
filename: `pwa-icons/${filename}.d.ts`,
})
}
}
export function pwaIcons(types?: PwaIconsTypes) {
return `// Generated by @vite-pwa/nuxt
import type { AppleSplashScreenLink, FaviconLink, HtmlLink, IconAsset } from '@vite-pwa/assets-generator/api'
export interface PWAAssetIconImage {
width?: number
height?: number
key: string
src: string
}
export type PWAAssetIcon<T extends HtmlLink> = Omit<IconAsset<T>, 'buffer'> & {
asImage: PWAAssetIconImage
}
export interface PWAIcons {
transparent: Record<${generateTypes(types?.transparent)}, PWAAssetIcon<HtmlLink>>
maskable: Record<${generateTypes(types?.maskable)}, PWAAssetIcon<HtmlLink>>
favicon: Record<${generateTypes(types?.favicon)}, PWAAssetIcon<FaviconLink>>
apple: Record<${generateTypes(types?.apple)}, PWAAssetIcon<HtmlLink>>
appleSplashScreen: Record<${generateTypes(types?.appleSplashScreen)}, PWAAssetIcon<AppleSplashScreenLink>>
}
declare module '#app' {
interface NuxtApp {
$pwaIcons?: PWAIcons
}
}
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$pwaIcons?: PWAIcons
}
}
export {}
`
}
export function generatePwaImageType(filename: string, names?: string[]) {
const propsName = `${filename}Props`
return `// Generated by @vite-pwa/nuxt
export interface ${propsName} {
image: ${generateTypes(names)}
alt?: string
width?: number
height?: number
crossorigin?: '' | 'anonymous' | 'use-credentials'
loading?: 'lazy' | 'eager'
decoding?: 'async' | 'auto' | 'sync'
nonce?: string
}
type __VLS_NonUndefinedable<T> = T extends undefined ? never : T
type __VLS_TypePropsToRuntimeProps<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? {
type: import('vue').PropType<__VLS_NonUndefinedable<T[K]>>
} : {
type: import('vue').PropType<T[K]>
required: true
}
}
declare const _default: import('vue').DefineComponent<__VLS_TypePropsToRuntimeProps<${propsName}>, {}, unknown, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('vue').ExtractPropTypes<__VLS_TypePropsToRuntimeProps<${propsName}>>>, {}, {}>
export default _default
`
}
function generateTypes(types?: string[]) {
return types?.length ? types.map(name => `'${name}'`).join(' | ') : 'string'
}
import type { Nuxt } from '@nuxt/schema'
import { addImports, addTypeTemplate } from '@nuxt/kit'
import type { Resolver } from '@nuxt/kit'
import type { PwaModuleOptions } from '../types'
import type { DtsInfo } from './pwa-icons-helper'
import { addPwaTypeTemplate, pwaIcons } from './pwa-icons-helper'
export async function registerPwaIconsTypes(
options: PwaModuleOptions,
nuxt: Nuxt,
resolver: Resolver,
runtimeDir: string,
) {
const pwaAssets = options.pwaAssets && !options.pwaAssets.disabled && (options.pwaAssets.config === true || !!options.pwaAssets.preset)
let dts: DtsInfo | undefined
if (pwaAssets) {
try {
const { preparePWAIconTypes } = await import('./pwa-icons')
dts = await preparePWAIconTypes(options, nuxt)
}
catch {
dts = undefined
}
}
nuxt.options.alias['#pwa'] = resolver.resolve(runtimeDir, 'composables/index.mjs')
nuxt.options.build.transpile.push('#pwa')
addImports([
'useTransparentPwaIcon',
'useMaskablePwaIcon',
'useFaviconPwaIcon',
'useApplePwaIcon',
'useAppleSplashScreenPwaIcon',
].map(key => ({
name: key,
as: key,
from: resolver.resolve(runtimeDir, 'composables/index'),
})))
const dtsContent = dts?.dts
if (dtsContent) {
addTypeTemplate({
write: true,
filename: 'pwa-icons/pwa-icons.d.ts',
getContents: () => dtsContent,
})
}
else {
addTypeTemplate({
write: true,
filename: 'pwa-icons/pwa-icons.d.ts',
getContents: () => pwaIcons(),
})
}
addPwaTypeTemplate('PwaTransparentImage', dts?.transparent)
addPwaTypeTemplate('PwaMaskableImage', dts?.maskable)
addPwaTypeTemplate('PwaFaviconImage', dts?.favicon)
addPwaTypeTemplate('PwaAppleImage', dts?.apple)
addPwaTypeTemplate('PwaAppleSplashScreenImage', dts?.appleSplashScreen)
return !!dts
}
import process from 'node:process'
import { basename, relative, resolve } from 'node:path'
import { readFile } from 'node:fs/promises'
import type { Nuxt } from '@nuxt/schema'
import { instructions } from '@vite-pwa/assets-generator/api/instructions'
import type { UserConfig } from '@vite-pwa/assets-generator/config'
import { loadConfig } from '@vite-pwa/assets-generator/config'
import type { ResolvedPWAAssetsOptions } from 'vite-plugin-pwa'
import type { PwaModuleOptions } from '../types'
import { type DtsInfo, generatePwaImageType, pwaIcons } from './pwa-icons-helper'
export async function preparePWAIconTypes(
options: PwaModuleOptions,
nuxt: Nuxt,
) {
if (!options.pwaAssets || options.pwaAssets.disabled)
return
const configuration = resolvePWAAssetsOptions(options)
if (!configuration || configuration.disabled)
return
const root = nuxt.options.rootDir ?? process.cwd()
const { config, sources } = await loadConfiguration(root, configuration)
if (!config.preset)
return
const {
preset,
images,
headLinkOptions: userHeadLinkOptions,
} = config
if (!images)
return
if (Array.isArray(images) && (!images.length || images.length > 1))
return
const useImage = Array.isArray(images) ? images[0] : images
const imageFile = resolve(root, useImage)
const publicDir = resolve(root, nuxt.options.dir.public ?? 'public')
const imageName = relative(publicDir, imageFile)
const xhtml = userHeadLinkOptions?.xhtml === true
const includeId = userHeadLinkOptions?.includeId === true
const assetsInstructions = await instructions({
imageResolver: () => readFile(resolve(root, useImage)),
imageName,
preset,
faviconPreset: userHeadLinkOptions?.preset,
htmlLinks: { xhtml, includeId },
basePath: nuxt.options.app.baseURL ?? '/',
resolveSvgName: userHeadLinkOptions?.resolveSvgName ?? (name => basename(name)),
})
const transparentNames = Object.values(assetsInstructions.transparent).map(({ name }) => name)
const maskableNames = Object.values(assetsInstructions.maskable).map(({ name }) => name)
const faviconNames = Object.values(assetsInstructions.favicon).map(({ name }) => name)
const appleNames = Object.values(assetsInstructions.apple).map(({ name }) => name)
const appleSplashScreenNames = Object.values(assetsInstructions.appleSplashScreen).map(({ name }) => name)
const dts = {
dts: pwaIcons({
transparent: transparentNames,
maskable: maskableNames,
favicon: faviconNames,
apple: appleNames,
appleSplashScreen: appleSplashScreenNames,
}),
transparent: generatePwaImageType('PwaTransparentImage', transparentNames),
maskable: generatePwaImageType('PwaMaskableImage', maskableNames),
favicon: generatePwaImageType('PwaFaviconImage', faviconNames),
apple: generatePwaImageType('PwaAppleImage', appleNames),
appleSplashScreen: generatePwaImageType('PwaAppleSplashScreenImage', appleSplashScreenNames),
} satisfies DtsInfo
if (nuxt.options.dev && nuxt.options.ssr) {
// restart nuxt dev server when the configuration files change
sources.forEach(source => nuxt.options.watch.push(source.replace(/\\/g, '/')))
}
return dts
}
function resolvePWAAssetsOptions(options: PwaModuleOptions) {
if (!options.pwaAssets)
return
const {
disabled: useDisabled,
config,
preset,
image = 'public/favicon.svg',
htmlPreset = '2023',
overrideManifestIcons = false,
includeHtmlHeadLinks = true,
injectThemeColor = true,
integration,
} = options.pwaAssets ?? {}
const disabled = useDisabled || (!config && !preset)
return <ResolvedPWAAssetsOptions>{
disabled,
config: disabled || !config ? false : config,
preset: disabled || config ? false : preset ?? 'minimal-2023',
images: [image],
htmlPreset,
overrideManifestIcons,
includeHtmlHeadLinks,
injectThemeColor,
integration,
}
}
async function loadConfiguration(root: string, pwaAssets: ResolvedPWAAssetsOptions) {
if (pwaAssets.config === false) {
return await loadConfig<UserConfig>(root, {
config: false,
preset: pwaAssets.preset as UserConfig['preset'],
images: pwaAssets.images,
logLevel: 'silent',
})
}
return await loadConfig<UserConfig>(
root,
typeof pwaAssets.config === 'boolean'
? root
: { config: pwaAssets.config },
)
}
import { writeFile } from 'node:fs/promises'
import type { VitePluginPWAAPI } from 'vite-plugin-pwa'
import { resolve } from 'pathe'
export async function regeneratePWA(_dir: string, pwaAssets: boolean, api?: VitePluginPWAAPI) {
if (pwaAssets) {
const pwaAssetsGenerator = await api?.pwaAssetsGenerator()
if (pwaAssetsGenerator)
await pwaAssetsGenerator.generate()
}
if (!api || api.disabled)
return
await api.generateSW()
}
export async function writeWebManifest(dir: string, path: string, api: VitePluginPWAAPI, pwaAssets: boolean) {
if (pwaAssets) {
const pwaAssetsGenerator = await api.pwaAssetsGenerator()
if (pwaAssetsGenerator)
pwaAssetsGenerator.injectManifestIcons()
}
const manifest = api.generateBundle({})?.[path]
if (manifest && 'source' in manifest)
await writeFile(resolve(dir, path), manifest.source, 'utf-8')
}
import { existsSync, readFileSync } from 'node:fs'
import process from 'node:process'
import { describe, expect, it } from 'vitest'
const build = process.env.TEST_BUILD === 'true'
describe(`test-${build ? 'build' : 'generate'}`, () => {
it('service worker is generated: ', () => {
const swName = build
? './playground/.output/public/sw.js'
: './playground/dist/sw.js'
const webManifest = build
? './playground/.output/public/manifest.webmanifest'
: './playground/dist/manifest.webmanifest'
expect(existsSync(swName), `${swName} doesn't exist`).toBeTruthy()
expect(existsSync(webManifest), `${webManifest} doesn't exist`).toBeTruthy()
const swContent = readFileSync(swName, 'utf-8')
let match: RegExpMatchArray | null
match = swContent.match(/define\(\["\.\/(workbox-\w+)"/)
expect(match && match.length === 2, `workbox-***.js entry not found in ${swName}`).toBeTruthy()
const workboxName = `./playground/${build ? '.output/public' : 'dist'}/${match?.[1]}.js`
expect(existsSync(workboxName), `${workboxName} doesn't exist`).toBeTruthy()
match = swContent.match(/url:\s*"manifest\.webmanifest"/)
expect(match && match.length === 1, 'missing manifest.webmanifest in sw precache manifest').toBeTruthy()
match = swContent.match(/url:\s*"\/"/)
expect(match && match.length === 1, 'missing entry point route (/) in sw precache manifest').toBeTruthy()
match = swContent.match(/url:\s*"about"/)
expect(match && match.length === 1, 'missing about route (/about) in sw precache manifest').toBeTruthy()
match = swContent.match(/url:\s*"_nuxt\/.*\.(css|js)"/)
expect(match && match.length > 0, 'missing _nuxt/**.(css|js) in sw precache manifest').toBeTruthy()
match = swContent.match(/url:\s*"(.*\/)?_payload.json"/)
expect(match && match.length === 2, 'missing _payload.json and about/_payload.json entries in sw precache manifest').toBeTruthy()
match = swContent.match(/url:\s*"_nuxt\/builds\/.*\.json"/)
expect(match && match.length > 0, 'missing App Manifest json entries in sw precache manifest').toBeTruthy()
if (build) {
match = swContent.match(/url:\s*"server\//)
expect(match === null, 'found server/ entries in sw precache manifest').toBeTruthy()
}
})
})
{
"extends": "./.nuxt/tsconfig.json"
}
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['test/*.test.ts'],
},
})
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册