├── test
├── fixture
│ ├── static
│ │ ├── .gitkeep
│ │ ├── precache.js
│ │ ├── OneSignalSDKUpdaterWorker.js
│ │ ├── icon.png
│ │ ├── offline.png
│ │ ├── OneSignalSDKWorker.js
│ │ ├── offline.html
│ │ └── custom-sw.js
│ ├── .gitignore
│ ├── sw
│ │ ├── caching.js
│ │ ├── routing.js
│ │ └── workbox.js
│ ├── pages
│ │ ├── _page.vue
│ │ └── index.vue
│ ├── tsconfig.json
│ ├── layouts
│ │ └── default.vue
│ └── nuxt.config.ts
├── pwa.test.js
└── __snapshots__
│ └── pwa.test.js.snap
├── .github
├── ISSUE_TEMPLATE.md
└── workflows
│ └── test.yml
├── docs
├── .gitignore
├── static
│ ├── icon.png
│ ├── preview.png
│ ├── preview-dark.png
│ ├── logo-dark.svg
│ └── logo-light.svg
├── content
│ ├── en
│ │ ├── onesignal.md
│ │ ├── index.md
│ │ ├── setup.md
│ │ ├── icon.md
│ │ ├── meta.md
│ │ ├── manifest.md
│ │ └── workbox.md
│ └── settings.json
├── nuxt.config.js
├── package.json
├── tailwind.config.js
└── README.md
├── templates
├── meta.json
├── meta.plugin.js
├── icon.plugin.js
└── workbox
│ ├── sw.unregister.js
│ ├── workbox.js
│ ├── workbox.unregister.js
│ └── sw.js
├── renovate.json
├── jest.config.js
├── .eslintrc
├── .gitignore
├── .eslintignore
├── types
├── index.d.ts
├── pwa.d.ts
├── manifest.d.ts
├── icon.d.ts
├── meta.d.ts
└── workbox.d.ts
├── .editorconfig
├── tsconfig.json
├── netlify.toml
├── lib
├── meta.utils.js
└── resize.js
├── LICENSE
├── README.md
├── src
├── workbox
│ ├── defaults.ts
│ ├── index.ts
│ └── options.ts
├── manifest.ts
├── pwa.ts
├── utils.ts
├── icon.ts
└── meta.ts
├── package.json
├── CODE_OF_CONDUCT.md
└── CHANGELOG.md
/test/fixture/static/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixture/static/precache.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixture/.gitignore:
--------------------------------------------------------------------------------
1 | sw.*
2 | workbox-*
3 |
--------------------------------------------------------------------------------
/test/fixture/sw/caching.js:
--------------------------------------------------------------------------------
1 | // Caching Extension
2 |
--------------------------------------------------------------------------------
/test/fixture/sw/routing.js:
--------------------------------------------------------------------------------
1 | // Routing Extension
2 |
--------------------------------------------------------------------------------
/test/fixture/sw/workbox.js:
--------------------------------------------------------------------------------
1 | // Workbox Extension
2 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .nuxt
4 | sw.*
5 |
--------------------------------------------------------------------------------
/templates/meta.json:
--------------------------------------------------------------------------------
1 | <%= JSON.stringify(options.head, null, 2) %>
2 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "@nuxtjs"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: '@nuxt/test-utils'
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "@nuxtjs/eslint-config-typescript"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/docs/static/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nuxt-community/pwa-module/main/docs/static/icon.png
--------------------------------------------------------------------------------
/docs/static/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nuxt-community/pwa-module/main/docs/static/preview.png
--------------------------------------------------------------------------------
/docs/static/preview-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nuxt-community/pwa-module/main/docs/static/preview-dark.png
--------------------------------------------------------------------------------
/test/fixture/static/OneSignalSDKUpdaterWorker.js:
--------------------------------------------------------------------------------
1 | importScripts('https://cdn.onesignal.com/sdks/OneSignalSDK.js')
2 |
--------------------------------------------------------------------------------
/test/fixture/static/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nuxt-community/pwa-module/main/test/fixture/static/icon.png
--------------------------------------------------------------------------------
/test/fixture/static/offline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nuxt-community/pwa-module/main/test/fixture/static/offline.png
--------------------------------------------------------------------------------
/test/fixture/pages/_page.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | You are on page: {{ $route.path }}
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.iml
3 | .idea
4 | *.log*
5 | .nuxt
6 | .vscode
7 | .DS_Store
8 | coverage
9 | dist
10 | .cache
11 |
--------------------------------------------------------------------------------
/test/fixture/static/OneSignalSDKWorker.js:
--------------------------------------------------------------------------------
1 | importScripts('/sw.js?1552851140562', 'https://cdn.onesignal.com/sdks/OneSignalSDK.js')
2 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # Common
2 | node_modules
3 | dist
4 | .nuxt
5 | coverage
6 |
7 | # Plugin
8 | templates
9 |
10 | # Fixtures
11 | test/fixture/**/static
12 |
--------------------------------------------------------------------------------
/test/fixture/static/offline.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Offline
4 |
5 |
6 | Website is Offline :(
7 |
8 |
9 |
--------------------------------------------------------------------------------
/templates/meta.plugin.js:
--------------------------------------------------------------------------------
1 | import { mergeMeta } from './meta.utils'
2 | import meta from './meta.json'
3 |
4 | export default function ({ app }) {
5 | mergeMeta(app.head, meta)
6 | }
7 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | export { MetaOptions } from './meta'
2 | export { IconOptions } from './icon'
3 | export { WorkboxOptions } from './workbox'
4 | export { ManifestOptions } from './manifest'
5 | export { PWAContext } from './pwa'
6 |
--------------------------------------------------------------------------------
/docs/content/en/onesignal.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: OneSignal Module
3 | description: OneSignal Module
4 | position: 6
5 | category: Modules
6 | ---
7 |
8 |
9 | Please see [nuxt-community/onesignal-module](https://github.com/nuxt-community/onesignal-module)
10 |
--------------------------------------------------------------------------------
/templates/icon.plugin.js:
--------------------------------------------------------------------------------
1 | export default async function (ctx, inject) {
2 | const icons = <%= JSON.stringify(options.icons) %>
3 | const getIcon = size => icons[size + 'x' + size] || ''
4 | inject('<%= options.pluginName.replace('$', '') %>', getIcon)
5 | }
6 |
--------------------------------------------------------------------------------
/docs/content/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Nuxt PWA",
3 | "url": "https://pwa.nuxtjs.org",
4 | "logo": {
5 | "light": "/logo-light.svg",
6 | "dark": "/logo-dark.svg"
7 | },
8 | "github": "nuxt-community/pwa-module",
9 | "twitter": "nuxt_js"
10 | }
11 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_size = 2
6 | indent_style = space
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/test/fixture/static/custom-sw.js:
--------------------------------------------------------------------------------
1 | console.log('Custom service worker!')
2 |
3 | self.addEventListener('install', function (e) {
4 | console.log('Install event:', e)
5 | })
6 |
7 | self.addEventListener('activate', function (e) {
8 | console.log('Activate event:', e)
9 | })
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "outDir": "dist",
7 | "declaration": true,
8 | "resolveJsonModule": true,
9 | "esModuleInterop": true,
10 | "types": [
11 | "@nuxt/types"
12 | ]
13 | },
14 | "include": [
15 | "src"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/types/pwa.d.ts:
--------------------------------------------------------------------------------
1 | import { MetaOptions } from './meta'
2 | import { IconOptions } from './icon'
3 | import { WorkboxOptions } from './workbox'
4 | import { ManifestOptions } from './manifest'
5 |
6 | export interface PWAContext {
7 | meta?: MetaOptions
8 | icon?: IconOptions
9 | workbox?: WorkboxOptions
10 | manifest?: ManifestOptions
11 |
12 | _manifestMeta: any // vue-meta record
13 | }
14 |
--------------------------------------------------------------------------------
/docs/nuxt.config.js:
--------------------------------------------------------------------------------
1 | import theme from '@nuxt/content-theme-docs'
2 |
3 | export default theme({
4 | loading: { color: '#5A0FC8' },
5 | buildModules: ['nuxt-ackee'],
6 | ackee: {
7 | server: 'https://ackee.nuxtjs.com',
8 | domainId: 'a2998bc2-56dd-47fa-9d94-9781411bd1f9',
9 | detailed: true
10 | },
11 | pwa: {
12 | manifest: {
13 | name: 'Nuxt PWA'
14 | }
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "3.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "nuxt",
7 | "build": "nuxt build",
8 | "start": "nuxt start",
9 | "generate": "nuxt generate"
10 | },
11 | "dependencies": {
12 | "@nuxt/content-theme-docs": "^0.8.2",
13 | "nuxt": "^2.14.12"
14 | },
15 | "devDependencies": {
16 | "nuxt-ackee": "^2.0.0"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/docs/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | theme: {
3 | extend: {
4 | colors: {
5 | primary: {
6 | 100: '#F4EEFB',
7 | 200: '#E2D5F6',
8 | 300: '#D1BCF0',
9 | 400: '#AF89E4',
10 | 500: '#8C57D9',
11 | 600: '#7E4EC3',
12 | 700: '#543482',
13 | 800: '#3F2762',
14 | 900: '#2A1A41'
15 | }
16 | }
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | # Global settings applied to the whole site.
2 | #
3 | # “base” is the directory to change to before starting build. If you set base:
4 | # that is where we will look for package.json/.nvmrc/etc, not repo root!
5 | # “command” is your build command.
6 | # “publish” is the directory to publish (relative to the root of your repo).
7 |
8 | [build]
9 | base = "docs"
10 | command = "yarn generate"
11 | publish = "dist"
12 |
--------------------------------------------------------------------------------
/templates/workbox/sw.unregister.js:
--------------------------------------------------------------------------------
1 | // THIS FILE SHOULD NOT BE VERSION CONTROLLED
2 |
3 | // https://github.com/NekR/self-destroying-sw
4 |
5 | self.addEventListener('install', function (e) {
6 | self.skipWaiting()
7 | })
8 |
9 | self.addEventListener('activate', function (e) {
10 | self.registration.unregister()
11 | .then(function () {
12 | return self.clients.matchAll()
13 | })
14 | .then(function (clients) {
15 | clients.forEach(client => client.navigate(client.url))
16 | })
17 | })
18 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # docs
2 |
3 | ## Setup
4 |
5 | Install dependencies:
6 |
7 | ```bash
8 | yarn install
9 | ```
10 |
11 | ## Development
12 |
13 | ```bash
14 | yarn dev
15 | ```
16 |
17 | ## Static Generation
18 |
19 | This will create the `dist/` directory for publishing to static hosting:
20 |
21 | ```bash
22 | yarn generate
23 | ```
24 |
25 | To preview the static generated app, run `yarn start`
26 |
27 | For detailed explanation on how things work, checkout [nuxt/content](https://content.nuxtjs.org) and [@nuxt/content theme docs](https://content.nuxtjs.org/themes-docs).
28 |
--------------------------------------------------------------------------------
/templates/workbox/workbox.js:
--------------------------------------------------------------------------------
1 | async function register() {
2 | if (!'serviceWorker' in navigator) {
3 | throw new Error('serviceWorker is not supported in current browser!')
4 | }
5 |
6 | const { Workbox } = await import('workbox-cdn/workbox/workbox-window.<%= options.dev ? 'dev' : 'prod' %>.es5.mjs')
7 |
8 | const workbox = new Workbox('<%= options.swURL %>', {
9 | scope: '<%= options.swScope %>'
10 | })
11 |
12 | await workbox.register()
13 |
14 | return workbox
15 | }
16 |
17 | window.$workbox = register()
18 | .catch(error => {<% if (options.dev) { %> console.error('Error registering workbox:', error) <% } %>})
19 |
--------------------------------------------------------------------------------
/test/fixture/pages/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
29 |
30 |
35 |
--------------------------------------------------------------------------------
/templates/workbox/workbox.unregister.js:
--------------------------------------------------------------------------------
1 | if ('serviceWorker' in navigator) {
2 | navigator.serviceWorker.getRegistrations().then((registrations) => {
3 | for (const registration of registrations) {
4 | console.info('[pwa] [workbox] Unregistering service worker:', registration)
5 | registration.unregister()
6 | }
7 | })
8 | }
9 |
10 | if ('caches' in window) {
11 | caches.keys()
12 | .then((keys) => {
13 | if (keys.length) {
14 | console.info('[pwa] [workbox] Cleaning cache for:', keys.join(', '))
15 | for (const key of keys) {
16 | caches.delete(key)
17 | }
18 | }
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/test/fixture/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2018",
4 | "module": "ESNext",
5 | "moduleResolution": "Node",
6 | "lib": [
7 | "ESNext",
8 | "ESNext.AsyncIterable",
9 | "DOM"
10 | ],
11 | "esModuleInterop": true,
12 | "allowJs": true,
13 | "sourceMap": true,
14 | "strict": true,
15 | "noEmit": true,
16 | "baseUrl": ".",
17 | "paths": {
18 | "~/*": [
19 | "./*"
20 | ],
21 | "@/*": [
22 | "./*"
23 | ]
24 | },
25 | "types": [
26 | "@types/node",
27 | "@nuxt/types"
28 | ]
29 | },
30 | "exclude": [
31 | "node_modules"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/docs/content/en/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Introduction
3 | description: 'Supercharge Nuxt with a heavily tested, updated and stable PWA solution'
4 | position: 1
5 | category: ''
6 | features:
7 | - Registers a service worker for offline caching.
8 | - Automatically generate manifest.json file.
9 | - Automatically adds SEO friendly meta data with manifest integration.
10 | - Automatically generates app icons with different sizes.
11 | - Free background push notifications using OneSignal.
12 | ---
13 |
14 |
15 |
16 |
17 | Zero config [PWA](https://developers.google.com/web/progressive-web-apps) solution for [Nuxt.js](https://nuxtjs.org)
18 |
19 | ## Features
20 |
21 |
22 |
23 | Enjoy light and dark mode:
24 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | ci:
13 | runs-on: ${{ matrix.os }}
14 |
15 | strategy:
16 | matrix:
17 | os: [ubuntu-latest]
18 | node: [12]
19 |
20 | steps:
21 | - uses: actions/setup-node@v2
22 | with:
23 | node-version: ${{ matrix.node }}
24 |
25 | - name: checkout
26 | uses: actions/checkout@master
27 |
28 | - name: cache node_modules
29 | uses: actions/cache@v2
30 | with:
31 | path: node_modules
32 | key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
33 |
34 | - name: Install dependencies
35 | if: steps.cache.outputs.cache-hit != 'true'
36 | run: yarn
37 |
38 | - name: Lint
39 | run: yarn lint
40 |
41 | - name: Test
42 | run: yarn jest
43 |
44 | - name: Coverage
45 | uses: codecov/codecov-action@v1
46 |
--------------------------------------------------------------------------------
/lib/meta.utils.js:
--------------------------------------------------------------------------------
1 | export function mergeMeta (to, from) {
2 | if (typeof to === 'function') {
3 | // eslint-disable-next-line no-console
4 | console.warn('Cannot merge meta. Avoid using head as a function!')
5 | return
6 | }
7 |
8 | for (const key in from) {
9 | const value = from[key]
10 | if (Array.isArray(value)) {
11 | to[key] = to[key] || []
12 | for (const item of value) {
13 | // Avoid duplicates
14 | if (
15 | (item.hid && hasMeta(to[key], 'hid', item.hid)) ||
16 | (item.name && hasMeta(to[key], 'name', item.name))
17 | ) {
18 | continue
19 | }
20 | // Add meta
21 | to[key].push(item)
22 | }
23 | } else if (typeof value === 'object') {
24 | to[key] = to[key] || {}
25 | for (const attr in value) {
26 | to[key][attr] = value[attr]
27 | }
28 | } else if (to[key] === undefined) {
29 | to[key] = value
30 | }
31 | }
32 | }
33 |
34 | function hasMeta (arr, key, val) {
35 | return arr.find(obj => val ? obj[key] === val : obj[key])
36 | }
37 |
--------------------------------------------------------------------------------
/test/fixture/layouts/default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
33 |
34 |
61 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Nuxt Community
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 |
--------------------------------------------------------------------------------
/types/manifest.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint camelcase: 0 */
2 |
3 | export interface ManifestOptions {
4 | /**
5 | * Default: _npm_package_name_
6 | */
7 | name: string,
8 | /**
9 | * Default: _npm_package_name_
10 | */
11 | short_name: string,
12 | /**
13 | * Default: _npm_package_description_
14 | */
15 | description: string,
16 | /**
17 | *
18 | */
19 | icons: Record[],
20 | /**
21 | * Default: `routerBase + '?standalone=true'`
22 | */
23 | start_url: string,
24 | /**
25 | * Default: `standalone`
26 | */
27 | display: string,
28 | /**
29 | * Default: `#ffffff`
30 | */
31 | background_color: string,
32 | /**
33 | * Default: undefined
34 | */
35 | theme_color: string,
36 | /**
37 | * Default: `ltr`
38 | */
39 | dir: 'ltr' | 'rtl',
40 | /**
41 | * Default: `en`
42 | */
43 | lang: string,
44 | /**
45 | * Default: `false`
46 | */
47 | useWebmanifestExtension: boolean,
48 | /**
49 | * Default: A combination of `routerBase` and `options.build.publicPath`
50 | */
51 | publicPath: string,
52 |
53 | fileName: string,
54 | crossorigin: boolean
55 | }
56 |
--------------------------------------------------------------------------------
/lib/resize.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const jimp = require('jimp-compact')
3 |
4 | async function resize ({ input, distDir, sizes }) {
5 | const inputFile = await jimp.read(input)
6 |
7 | // Icons
8 | await Promise.all(sizes.map(normalizeSize).map((size) => {
9 | const name = sizeName(size)
10 | const distFile = path.join(distDir, `${name}.png`)
11 | return new Promise((resolve) => {
12 | inputFile.clone().contain(size[0], size[1]).write(distFile, () => resolve())
13 | })
14 | }))
15 | }
16 |
17 | resize(JSON.parse(process.argv[2])).then(() => {
18 | process.exit(0)
19 | }).catch((error) => {
20 | console.error(error) // eslint-disable-line no-console
21 | process.exit(1)
22 | })
23 |
24 | // TODO: Dedup
25 | function sizeName (size) {
26 | size = normalizeSize(size)
27 | const prefix = size[2] ? (size[2] + '_') : ''
28 | return prefix + size[0] + 'x' + size[1]
29 | }
30 |
31 | // TODO: Dedup
32 | function normalizeSize (size) {
33 | if (!Array.isArray(size)) {
34 | size = [size, size]
35 | }
36 | if (size.length === 1) {
37 | size = [size, size]
38 | } else if (size.length === 0) {
39 | size = 64
40 | }
41 | return size
42 | }
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PWA Module
2 |
3 | [![npm version][npm-version-src]][npm-version-href]
4 | [![npm downloads][npm-downloads-src]][npm-downloads-href]
5 | [![Checks][checks-src]][checks-href]
6 | [![Codecov][codecov-src]][codecov-href]
7 |
8 | > Zero config PWA solution for Nuxt.js
9 |
10 | 📖 [**Read Documentation**](https://pwa.nuxtjs.org)
11 |
12 | ## Development
13 |
14 | 1. Clone this repository
15 | 2. Install dependencies using `yarn install` or `npm install`
16 | 3. Start development server using `npm run dev`
17 |
18 | ## License
19 |
20 | [MIT License](./LICENSE)
21 |
22 | Copyright (c) - Nuxt Community
23 |
24 |
25 | [npm-version-src]: https://img.shields.io/npm/v/@nuxtjs/pwa/latest.svg?style=flat-square
26 | [npm-version-href]: https://npmjs.com/package/@nuxtjs/pwa
27 |
28 | [npm-downloads-src]: https://img.shields.io/npm/dm/@nuxtjs/pwa.svg?style=flat-square
29 | [npm-downloads-href]: https://npmjs.com/package/@nuxtjs/pwa
30 |
31 | [checks-src]: https://flat.badgen.net/github/checks/nuxt-community/pwa-module/dev
32 | [checks-href]: https://github.com/nuxt-community/pwa-module/actions
33 |
34 | [codecov-src]: https://img.shields.io/codecov/c/github/nuxt-community/pwa-module.svg?style=flat-square
35 | [codecov-href]: https://codecov.io/gh/nuxt-community/pwa-module
36 |
--------------------------------------------------------------------------------
/src/workbox/defaults.ts:
--------------------------------------------------------------------------------
1 | import { version as workboxVersion } from 'workbox-cdn/package.json'
2 | import type { WorkboxOptions } from '../../types'
3 |
4 | export const defaults: WorkboxOptions = {
5 | // General
6 | workboxVersion,
7 | workboxURL: undefined,
8 | importScripts: [],
9 | autoRegister: true,
10 | enabled: undefined,
11 |
12 | // Config
13 | config: {},
14 | clientsClaim: true,
15 | skipWaiting: true,
16 | offlineAnalytics: false,
17 | workboxExtensions: [],
18 |
19 | // Precache
20 | preCaching: [],
21 | cacheOptions: {
22 | cacheId: undefined,
23 | directoryIndex: '/',
24 | revision: undefined
25 | },
26 | cachingExtensions: [],
27 | cleanupOutdatedCaches: true,
28 |
29 | // Offline
30 | offline: true,
31 | offlineStrategy: 'NetworkFirst',
32 | offlinePage: null,
33 | offlineAssets: [],
34 |
35 | // Runtime Caching
36 | runtimeCaching: [],
37 | routingExtensions: [],
38 | cacheAssets: true,
39 | assetsURLPattern: undefined,
40 | pagesURLPattern: undefined,
41 |
42 | // Sw
43 | swTemplate: undefined,
44 | swURL: undefined,
45 | swScope: undefined,
46 | swDest: undefined,
47 |
48 | // Router
49 | routerBase: undefined,
50 | publicPath: undefined,
51 |
52 | dev: undefined,
53 | cacheNames: undefined
54 | }
55 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@nuxtjs/pwa",
3 | "version": "3.3.5",
4 | "description": "Zero config PWA solution for Nuxt.js",
5 | "repository": "nuxt-community/pwa-module",
6 | "license": "MIT",
7 | "main": "dist/pwa.js",
8 | "types": "./dist/pwa.d.ts",
9 | "files": [
10 | "lib",
11 | "dist",
12 | "templates"
13 | ],
14 | "scripts": {
15 | "build": "siroc build",
16 | "dev": "nuxt-ts test/fixture",
17 | "lint": "eslint --ext .js,.vue,.ts .",
18 | "release": "yarn test && yarn build && standard-version && git push --follow-tags && npm publish",
19 | "test": "yarn lint && jest"
20 | },
21 | "dependencies": {
22 | "clone-deep": "^4.0.1",
23 | "defu": "^3.2.2",
24 | "execa": "^5.0.0",
25 | "fs-extra": "^9.1.0",
26 | "hasha": "^5.2.2",
27 | "jimp-compact": "^0.16.1",
28 | "lodash.template": "^4.5.0",
29 | "serve-static": "^1.14.1",
30 | "workbox-cdn": "^5.1.4"
31 | },
32 | "devDependencies": {
33 | "@babel/preset-typescript": "^7.12.7",
34 | "@nuxt/test-utils": "^0.1.2",
35 | "@nuxt/types": "^2.14.12",
36 | "@nuxt/typescript-build": "^2.0.4",
37 | "@nuxt/typescript-runtime": "^2.0.1",
38 | "@nuxtjs/eslint-config": "latest",
39 | "@nuxtjs/eslint-config-typescript": "^5.0.0",
40 | "@nuxtjs/module-test-utils": "latest",
41 | "@types/workbox-sw": "^4.3.1",
42 | "babel-eslint": "latest",
43 | "codecov": "latest",
44 | "eslint": "latest",
45 | "jest": "latest",
46 | "klaw-sync": "latest",
47 | "node-fetch": "latest",
48 | "nuxt-edge": "latest",
49 | "siroc": "^0.6.3",
50 | "standard-version": "latest",
51 | "typescript": "^4.1.3"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/workbox/index.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { readJSFiles, pick, copyTemplate, PKG_DIR } from '../utils'
3 | import { getOptions } from './options'
4 |
5 | export async function workbox (nuxt, pwa, moduleContainer) {
6 | const options = getOptions(nuxt, pwa)
7 |
8 | // Warning for dev option
9 | if (options.dev) {
10 | // eslint-disable-next-line no-console
11 | console.warn('Workbox is running in development mode')
12 | }
13 |
14 | // Register plugin
15 | if (options.autoRegister) {
16 | moduleContainer.addPlugin({
17 | src: resolve(PKG_DIR, `templates/workbox/workbox${options.enabled ? '' : '.unregister'}.js`),
18 | ssr: false,
19 | fileName: 'workbox.js',
20 | options: {
21 | ...options
22 | }
23 | })
24 | }
25 |
26 | // Add sw.js
27 | if (options.swTemplate) {
28 | copyTemplate({
29 | src: options.swTemplate,
30 | dst: options.swDest,
31 | options: {
32 | dev: nuxt.options.dev,
33 | swOptions: pick(options, [
34 | 'workboxURL',
35 | 'importScripts',
36 | 'config',
37 | 'cacheNames',
38 | 'cacheOptions',
39 | 'clientsClaim',
40 | 'skipWaiting',
41 | 'cleanupOutdatedCaches',
42 | 'offlineAnalytics',
43 | 'preCaching',
44 | 'runtimeCaching',
45 | 'offlinePage',
46 | 'pagesURLPattern',
47 | 'offlineStrategy'
48 | ]),
49 | routingExtensions: await readJSFiles(nuxt, options.routingExtensions),
50 | cachingExtensions: await readJSFiles(nuxt, options.cachingExtensions),
51 | workboxExtensions: await readJSFiles(nuxt, options.workboxExtensions)
52 | }
53 | })
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/docs/content/en/setup.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Setup
3 | description: 'Supercharge Nuxt with a heavily tested, updated and stable PWA solution'
4 | position: 1
5 | category: Guide
6 | ---
7 |
8 | Check the [Nuxt.js documentation](https://nuxtjs.org/api/configuration-modules#the-modules-property) for more information about installing and using modules in Nuxt.js.
9 |
10 | ## Installation
11 |
12 | Add `@nuxtjs/pwa` dependency to your project:
13 |
14 |
15 |
16 |
17 | ```bash
18 | yarn add --dev @nuxtjs/pwa
19 | ```
20 |
21 |
22 |
23 |
24 | ```bash
25 | npm i --save-dev @nuxtjs/pwa
26 | ```
27 |
28 |
29 |
30 |
31 | Edit your `nuxt.config.js` file to add pwa module::
32 |
33 | ```js{}[nuxt.config.js]
34 | {
35 | buildModules: [
36 | '@nuxtjs/pwa',
37 | ]
38 | }
39 | ```
40 |
41 | **NOTE:** If using `ssr: false` with production mode without `nuxt generate`, you have to use `modules` instead of `buildModules`
42 |
43 | ### Add Icon
44 |
45 | Ensure `static` dir exists and optionally create `static/icon.png`. (Recommended to be square png and >= `512x512px`)
46 |
47 | ### Ignore Service Worker
48 |
49 | Create or add this to `.gitignore`:
50 |
51 | ```{}[.gitignore]
52 | sw.*
53 | ```
54 |
55 | ## Configuration
56 |
57 | PWA module is a collection of smaller modules that are designed to magically work out of the box together. To disable each sub-module, you can pass `false` option with its name as key. For example to disable _icon_ module:
58 |
59 | ```js{}[nuxt.config.js]
60 | {
61 | pwa: {
62 | icon: false // disables the icon module
63 | }
64 | }
65 | ```
66 |
67 | Also each sub-module has its own configuration. Continue reading docs for detailed info.
68 |
--------------------------------------------------------------------------------
/docs/content/en/icon.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Icon Module
3 | description: This module automatically generates app icons and favicon with different sizes
4 | position: 2
5 | category: Modules
6 | ---
7 |
8 | This module automatically generates app icons and favicon with different sizes using [jimp](https://github.com/oliver-moran/jimp) and fills `manifest.icons[]` with proper paths to generated assets that is used by manifest module. Source icon is being resized using *cover* method.
9 |
10 |
11 | You can pass options to `pwa.icon` in `nuxt.config.js` to override defaults.
12 |
13 | ```js{}[nuxt.config.js]
14 | pwa: {
15 | icon: {
16 | /* icon options */
17 | }
18 | }
19 | ```
20 |
21 | ## options
22 |
23 | **source**
24 | - Default: `[srcDir]/[staticDir]/icon.png`
25 |
26 | **fileName**
27 | - Default: `icon.png`
28 |
29 | **sizes**
30 | - Default: `[64, 120, 144, 152, 192, 384, 512]`
31 |
32 | Array of sizes to be generated (Square).
33 |
34 | **targetDir**
35 | - Default: `icons`
36 |
37 | **plugin**
38 | - Default: true
39 |
40 | Make icons accessible through `ctx` or Vue instances.
41 |
42 | Example: `ctx.$icon(512)` will return the url for the icon with the size of `512px`.
43 | Will return an empty string when no icon in the given size is available (eg. when the size is not in `sizes` array).
44 |
45 | **pluginName**
46 | - Default: '$icon'
47 |
48 | Name of property for accessible icons.
49 |
50 | **purpose**
51 | - Default: `['any', 'maskable']`
52 |
53 | Array or string of icon purpose.
54 |
55 | Example:
56 |
57 | ```js
58 | purpose: 'maskable'
59 | ```
60 |
61 | More detail of "purpose": [https://w3c.github.io/manifest/#purpose-member](https://w3c.github.io/manifest/#purpose-member)
62 |
63 |
64 | **cacheDir**
65 | - Default: `{rootDir}/node_modules/.cache/pwa/icon`
66 |
67 | Cache dir for generated icons
68 |
--------------------------------------------------------------------------------
/types/icon.d.ts:
--------------------------------------------------------------------------------
1 | export type iOSType = 'ipad' | 'ipadpro9' | 'ipadpro9' | 'ipadpro10' | 'ipadpro12' | 'iphonese' | 'iphone6' | 'iphoneplus' | 'iphonex' | 'iphonexr' | 'iphonexsmax'
2 | export type iOSSize = [number, number, iOSType]
3 |
4 | export interface IconOptions {
5 | /**
6 | * Default: `[srcDir]/[staticDir]/icon.png`
7 | */
8 | source: string,
9 | /**
10 | * Default: `icon.png`
11 | */
12 | fileName: string,
13 | /**
14 | * Array of sizes to be generated (Square).
15 | * Default: `[64, 120, 144, 152, 192, 384, 512]`
16 | */
17 | sizes: number[],
18 |
19 | /**
20 | * Default:
21 | * ```javascript
22 | * [
23 | * [1536, 2048, 'ipad'], // Ipad
24 | * [1536, 2048, 'ipadpro9'], // Ipad Pro 9.7"
25 | * [1668, 2224, 'ipadpro10'], // Ipad Pro 10.5"
26 | * [2048, 2732, 'ipadpro12'], // Ipad Pro 12.9"
27 | * [640, 1136, 'iphonese'], // Iphone SE
28 | * [50, 1334, 'iphone6'], // Iphone 6
29 | * [1080, 1920, 'iphoneplus'], // Iphone Plus
30 | * [1125, 2436, 'iphonex'], // Iphone X
31 | * [828, 1792, 'iphonexr'], // Iphone XR
32 | * [1242, 2688, 'iphonexsmax'] // Iphone XS Max
33 | * ]
34 | * ```
35 | */
36 | iosSizes: iOSSize[],
37 | /**
38 | * Default: `icons`
39 | */
40 | targetDir: string,
41 | /**
42 | * Make icons accessible through `ctx` or Vue instances.
43 | *
44 | * Default: `true`
45 | */
46 | plugin: boolean,
47 | /**
48 | * Name of property for accessible icons.
49 | *
50 | * Default: `$icon`
51 | */
52 | pluginName: string,
53 | /**
54 | * Array or string of icon purpose.
55 | *
56 | * Default: `['any', 'maskable']`
57 | */
58 | purpose: string[] | string,
59 | /**
60 | * Cache dir for generated icons
61 | *
62 | * Default: `{rootDir}/node_modules/.cache/icon`
63 | */
64 | cacheDir: string,
65 |
66 | publicPath: string
67 | }
68 |
--------------------------------------------------------------------------------
/test/fixture/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | import pwaModule from '../../src/pwa'
2 |
3 | export default {
4 | target: 'static',
5 |
6 | generate: {
7 | staticAssets: {
8 | version: 'default'
9 | }
10 | },
11 |
12 | buildModules: [
13 | pwaModule
14 | ],
15 |
16 | manifest: {
17 | name: 'Test Project Name',
18 | description: 'Test Project Description',
19 |
20 | useWebmanifestExtension: true,
21 | fileName: 'manifest_test.[ext]?[hash]',
22 | orientation: 'portrait'
23 | },
24 |
25 | meta: {
26 | nativeUI: true
27 | },
28 |
29 | workbox: {
30 | offlineAnalytics: true,
31 | dev: true,
32 | config: {
33 | debug: true
34 | },
35 | cacheNames: {
36 | prefix: 'test',
37 | googleAnalytics: 'test-ga'
38 | },
39 | cacheOptions: {
40 | revision: 'test-rev'
41 | },
42 | importScripts: [
43 | 'custom-sw.js'
44 | ],
45 | workboxExtensions: [
46 | '~/sw/workbox'
47 | ],
48 | cachingExtensions: [
49 | '~/sw/caching'
50 | ],
51 | routingExtensions: [
52 | '~/sw/routing'
53 | ],
54 | preCaching: [
55 | 'precache.js'
56 | ],
57 | // offlinePage: '/offline.html',
58 | // offlineAssets: [
59 | // '/offline.png'
60 | // ],
61 | runtimeCaching: [
62 | {
63 | urlPattern: 'https://google.com/.*',
64 | handler: 'cacheFirst',
65 | method: 'GET'
66 | },
67 | {
68 | urlPattern: 'https://pwa.nuxtjs.org/.*',
69 | handler: 'CacheFirst',
70 | method: 'GET',
71 | strategyOptions: {
72 | cacheName: 'nuxt-pwa'
73 | },
74 | strategyPlugins: [
75 | {
76 | use: 'Expiration',
77 | config: {
78 | maxEntries: 10,
79 | maxAgeSeconds: 300
80 | }
81 | }
82 | ]
83 | }
84 | ]
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/manifest.ts:
--------------------------------------------------------------------------------
1 | import hasha from 'hasha'
2 | import type { ManifestOptions, PWAContext } from '../types'
3 | import { joinUrl, getRouteParams, emitAsset } from './utils'
4 |
5 | export function manifest (nuxt, pwa: PWAContext) {
6 | const { routerBase, publicPath } = getRouteParams(nuxt.options)
7 |
8 | // Combine sources
9 | const defaults: ManifestOptions = {
10 | name: process.env.npm_package_name,
11 | short_name: process.env.npm_package_name,
12 | description: process.env.npm_package_description,
13 | publicPath,
14 | icons: [],
15 | start_url: routerBase + '?standalone=true',
16 | display: 'standalone',
17 | background_color: '#ffffff',
18 | theme_color: pwa.meta.theme_color,
19 | lang: 'en',
20 | useWebmanifestExtension: false,
21 | fileName: 'manifest.[hash].[ext]',
22 | dir: undefined,
23 | crossorigin: undefined
24 | }
25 |
26 | const options: ManifestOptions = { ...defaults, ...pwa.manifest }
27 |
28 | // Remove extra fields from manifest
29 | const manifest = { ...options }
30 | // @ts-ignore
31 | delete manifest.src
32 | delete manifest.publicPath
33 | delete manifest.useWebmanifestExtension
34 | delete manifest.fileName
35 |
36 | // Generate file name
37 | const manifestFileName = options.fileName
38 | .replace('[hash]', hasha(JSON.stringify(manifest)).substr(0, 8))
39 | .replace('[ext]', options.useWebmanifestExtension ? 'webmanifest' : 'json')
40 |
41 | // Merge final manifest into options.manifest for other modules
42 | if (!nuxt.options.manifest) {
43 | nuxt.options.manifest = {}
44 | }
45 | Object.assign(nuxt.options.manifest, manifest)
46 | Object.assign(pwa.manifest, manifest)
47 |
48 | // Register webpack plugin to emit manifest
49 | const manifestSource = JSON.stringify(manifest, null, 2)
50 | emitAsset(nuxt, manifestFileName, manifestSource)
51 |
52 | // Add manifest meta
53 | const manifestMeta = { rel: 'manifest', href: joinUrl(options.publicPath, manifestFileName), hid: 'manifest' } as any
54 | if (manifest.crossorigin) {
55 | manifestMeta.crossorigin = manifest.crossorigin
56 | }
57 | pwa._manifestMeta = manifestMeta
58 | }
59 |
--------------------------------------------------------------------------------
/test/pwa.test.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import { setupTest, getNuxt } from '@nuxt/test-utils'
3 | import klawSync from 'klaw-sync'
4 | import fs from 'fs-extra'
5 |
6 | const getRelativePath = fileObj => path.relative(__dirname, fileObj.path)
7 | const noJS = item => !/\.js/.test(item)
8 |
9 | console.warn = jest.fn() // eslint-disable-line no-console
10 |
11 | describe('pwa', () => {
12 | setupTest({
13 | testDir: __dirname,
14 | fixture: 'fixture',
15 | configFile: 'nuxt.config.ts',
16 | build: true,
17 | generate: true,
18 | config: {}
19 | })
20 |
21 | test('workbox dev warning', () => {
22 | expect(console.warn).toHaveBeenCalledWith('Workbox is running in development mode') // eslint-disable-line no-console
23 | })
24 |
25 | test('generate files (dist)', () => {
26 | const nuxt = getNuxt()
27 | const generateFiles = klawSync(nuxt.options.generate.dir).map(getRelativePath)
28 |
29 | expect(generateFiles.filter(noJS)).toMatchSnapshot()
30 | })
31 |
32 | test('accessible icons', async () => {
33 | const nuxt = getNuxt()
34 | const { html } = await nuxt.renderRoute('/')
35 | expect(html).toContain('/_nuxt/icons/icon_512x512.b8f3a1.png')
36 | })
37 |
38 | test('icons purpose', () => {
39 | const nuxt = getNuxt()
40 | const assetDir = path.join(nuxt.options.generate.dir, '_nuxt')
41 | const manifestFileName = fs.readdirSync(assetDir).find(item => item.match(/^manifest./i))
42 | const manifestContent = JSON.parse(fs.readFileSync(path.join(assetDir, manifestFileName.split('?')[0])))
43 | expect(manifestContent.icons).toEqual(
44 | expect.arrayContaining([
45 | expect.objectContaining({
46 | purpose: expect.stringMatching(/( ?(any|maskable|badge))+/)
47 | })
48 | ])
49 | )
50 | })
51 |
52 | test('sw.js', async () => {
53 | const nuxt = getNuxt()
54 | const swContents = await fs.readFile(path.resolve(nuxt.options.generate.dir, 'sw.js'), 'utf-8')
55 |
56 | expect(swContents.replace(/@[^/]*/, '')).toMatchSnapshot()
57 | })
58 |
59 | test('manifest.json', async () => {
60 | const nuxt = getNuxt()
61 | const manifestContents = await fs.readFile(path.resolve(nuxt.options.generate.dir, '_nuxt/manifest_test.webmanifest'), 'utf-8')
62 |
63 | expect(manifestContents).toMatchSnapshot()
64 | })
65 | })
66 |
--------------------------------------------------------------------------------
/src/pwa.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import serveStatic from 'serve-static'
3 | import type { MetaOptions, ManifestOptions, IconOptions, PWAContext, WorkboxOptions } from '../types'
4 | import { PKG } from './utils'
5 | import { icon } from './icon'
6 | import { manifest } from './manifest'
7 | import { meta, metaRuntime } from './meta'
8 | import { workbox } from './workbox'
9 |
10 | interface PWAOptions {
11 | meta?: Partial | false
12 | icon?: Partial | false
13 | workbox?: Partial | false
14 | manifest?: Partial | false
15 | }
16 |
17 | export default async function pwa (moduleOptions: PWAOptions) {
18 | const { nuxt } = this
19 | const moduleContainer = this // TODO: remove dependency when module-utils
20 |
21 | const isBuild = nuxt.options._build
22 | const isGenerate = nuxt.options.target === 'static' && !nuxt.options.dev
23 | const isRuntime = !isBuild && !isGenerate
24 |
25 | if (isRuntime) {
26 | // Load meta.json for SPA renderer
27 | metaRuntime(nuxt)
28 | return
29 | }
30 |
31 | const modules = { icon, manifest, meta, workbox }
32 |
33 | // Shared options context
34 | nuxt.options.pwa = { ...(nuxt.options.pwa || {}), ...(moduleOptions || {}) }
35 | const pwa: PWAContext = nuxt.options.pwa
36 |
37 | // Normalize options
38 | for (const name in modules) {
39 | // Skip disabled modules
40 | if (pwa[name] === false || nuxt.options[name] === false) {
41 | continue
42 | }
43 | // Ensure options are an object
44 | if (pwa[name] === undefined) {
45 | pwa[name] = {}
46 | }
47 | // Backward compatibility for top-level options
48 | if (nuxt.options[name] !== undefined) {
49 | pwa[name] = { ...nuxt.options[name], ...pwa[name] }
50 | }
51 | }
52 |
53 | // Execute modules in sequence
54 | for (const name in modules) {
55 | if (pwa[name] === false) {
56 | continue
57 | }
58 | await modules[name](nuxt, pwa, moduleContainer)
59 | }
60 |
61 | // Serve dist from disk
62 | if (nuxt.options.dev) {
63 | const clientDir = resolve(nuxt.options.buildDir, 'dist/client')
64 | nuxt.options.serverMiddleware.push({
65 | path: nuxt.options.build.publicPath,
66 | handler: serveStatic(clientDir)
67 | })
68 | }
69 | }
70 |
71 | declare module '@nuxt/types/config/index' {
72 | interface NuxtOptions {
73 | pwa?: Partial
74 | meta?: Partial | false
75 | icon?: Partial | false
76 | workbox?: Partial | false
77 | manifest?: Partial | false
78 | }
79 | }
80 |
81 | pwa.meta = PKG
82 |
--------------------------------------------------------------------------------
/types/meta.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint camelcase: 0 */
2 | import { ManifestOptions } from './manifest'
3 |
4 | export type OgImageObject = {
5 | path?: string,
6 | width?: number,
7 | height?: number,
8 | type?: string
9 | }
10 |
11 | export interface MetaOptions extends Partial {
12 | /**
13 | * Default: `utf-8`
14 | */
15 | charset: string,
16 | /**
17 | * Default: `width=device-width, initial-scale=1`
18 | *
19 | * Meta: `viewport`
20 | */
21 | viewport: string,
22 | /**
23 | * Default: `true`
24 | *
25 | * Meta: `mobile-web-app-capable`
26 | */
27 | mobileApp: boolean,
28 | /**
29 | * Default: `false`
30 | *
31 | * Meta: `apple-mobile-web-app-capable`
32 | */
33 | mobileAppIOS: boolean,
34 | /**
35 | * Default: `default`
36 | */
37 | appleStatusBarStyle: string,
38 | /**
39 | * Default: `true` (to use options.icons)
40 | *
41 | * Meta: `shortcut icon` + `apple-touch-icon`
42 | */
43 | favicon: boolean,
44 | /**
45 | * Default: _npm_package_name_
46 | *
47 | * Meta: `title`
48 | */
49 | name: string,
50 | /**
51 | * @deprecated use meta.name
52 | */
53 | title?: string,
54 | /**
55 | * Default: _npm_package_author_name_
56 | *
57 | * Meta: `author`
58 | */
59 | author: string,
60 | /**
61 | * Default: _npm_package_description_
62 | *
63 | * Meta: `description`
64 | */
65 | description: string,
66 | /**
67 | * Default: `options.loading.color`
68 | *
69 | * Meta: `description`
70 | */
71 | theme_color: string,
72 | /**
73 | * Default: `en`
74 | *
75 | * Meta: `lang`
76 | */
77 | lang: string,
78 | /**
79 | * Default: `website`
80 | *
81 | * Meta: `og:type`
82 | */
83 | ogType: string,
84 | /**
85 | * Default: _npm_package_name_
86 | *
87 | * Meta: `og:site_name`
88 | */
89 | ogSiteName: string | true,
90 | /**
91 | * Default: _npm_package_name_
92 | *
93 | * Meta: `og:title`
94 | */
95 | ogTitle: string | true,
96 | /**
97 | * Default: _npm_package_description_
98 | *
99 | * Meta: `og:description`
100 | */
101 | ogDescription: string | true,
102 | /**
103 | * Default: `undefined`
104 | *
105 | * Meta: `N/A`
106 | */
107 | ogHost: string | undefined,
108 | /**
109 | * Default: `true`
110 | *
111 | * Meta: `og:image` and sub-tags
112 | */
113 | ogImage: boolean | string | OgImageObject,
114 | /**
115 | * Default: ogHost (if defined)
116 | *
117 | * Meta: `og:url`
118 | */
119 | ogUrl: string | undefined | true,
120 | /**
121 | * Default: `undefined`
122 | *
123 | * Meta: `twitter:card`
124 | */
125 | twitterCard: string | undefined,
126 | /**
127 | * Default: `undefined`
128 | *
129 | * Meta: `twitter:site`
130 | */
131 | twitterSite: string | undefined,
132 | /**
133 | * Default: `undefined`
134 | *
135 | * Meta: `twitter:creator`
136 | */
137 | twitterCreator: string | undefined,
138 | /**
139 | * Default: `false`
140 | */
141 | nativeUI: boolean
142 | }
143 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at pooya@pi0.ir. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { posix, resolve, dirname } from 'path'
2 | import { mkdirp, readFile, existsSync, writeFile } from 'fs-extra'
3 | import template from 'lodash.template'
4 | import { name, version } from '../package.json'
5 |
6 | export const PKG = {
7 | name,
8 | version
9 | }
10 |
11 | export const PKG_DIR = resolve(__dirname, '..')
12 |
13 | export function isUrl (url) {
14 | return url.indexOf('http') === 0 || url.indexOf('//') === 0
15 | }
16 |
17 | export function joinUrl (...args) {
18 | return posix.join(...args).replace(':/', '://')
19 | }
20 |
21 | export function normalizeSize (size) {
22 | if (!Array.isArray(size)) {
23 | size = [size, size]
24 | }
25 | if (size.length === 1) {
26 | size = [size, size]
27 | } else if (size.length === 0) {
28 | size = 64
29 | }
30 | return size
31 | }
32 |
33 | export function sizeName (size) {
34 | size = normalizeSize(size)
35 | const prefix = size[2] ? (size[2] + '_') : ''
36 | return prefix + size[0] + 'x' + size[1]
37 | }
38 |
39 | export function getRouteParams (options) {
40 | // routerBase
41 | const routerBase = options.router.base
42 |
43 | // publicPath
44 | let publicPath
45 | if (isUrl(options.build.publicPath)) {
46 | publicPath = options.build.publicPath
47 | } else {
48 | publicPath = joinUrl(routerBase, options.build.publicPath)
49 | }
50 |
51 | return {
52 | routerBase,
53 | publicPath
54 | }
55 | }
56 |
57 | export function startCase (str) {
58 | return typeof str === 'string' ? str[0].toUpperCase() + str.substr(1) : str
59 | }
60 |
61 | export async function writeData (path, data) {
62 | path = path.split('?')[0]
63 | await mkdirp(dirname(path))
64 | await writeFile(path, await data)
65 | }
66 |
67 | export function emitAsset (nuxt, fileName, data) {
68 | const emitAsset = async () => {
69 | const buildPath = resolve(nuxt.options.buildDir, 'dist/client', fileName)
70 | await writeData(buildPath, data)
71 | }
72 |
73 | nuxt.hook('build:done', () => emitAsset())
74 |
75 | const isGenerate = nuxt.options.target === 'static' && !nuxt.options.dev
76 | if (isGenerate) {
77 | nuxt.hook('modules:done', () => emitAsset())
78 | }
79 | }
80 |
81 | export async function readJSFiles (nuxt, files) {
82 | const contents = []
83 |
84 | for (const file of Array.isArray(files) ? files : [files]) {
85 | const path = nuxt.resolver.resolvePath(file)
86 | if (path && existsSync(path)) {
87 | contents.push(await readFile(path, 'utf8').then(s => s.trim()))
88 | } else {
89 | throw new Error('Can not read ' + path)
90 | }
91 | }
92 |
93 | return contents.join('\n\n')
94 | }
95 |
96 | export function pick (obj, props) {
97 | const newObj = {}
98 | props.forEach((prop) => {
99 | newObj[prop] = obj[prop]
100 | })
101 | return newObj
102 | }
103 |
104 | export async function copyTemplate ({ src, dst, options }) {
105 | const compile = template(await readFile(src, 'utf8'))
106 | await writeFile(dst, compile({ options }))
107 | }
108 |
109 | export function randomString (length) {
110 | const result = []
111 | const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
112 |
113 | for (let i = 0; i < length; i++) {
114 | const char = characters.charAt(Math.floor(Math.random() * characters.length))
115 | result.push(char)
116 | }
117 |
118 | return result.join('')
119 | }
120 |
--------------------------------------------------------------------------------
/templates/workbox/sw.js:
--------------------------------------------------------------------------------
1 | const options = <%= JSON.stringify(options.swOptions) %>
2 |
3 | importScripts(...[options.workboxURL, ...options.importScripts])
4 |
5 | initWorkbox(workbox, options)
6 | workboxExtensions(workbox, options)
7 | precacheAssets(workbox, options)
8 | cachingExtensions(workbox, options)
9 | runtimeCaching(workbox, options)
10 | offlinePage(workbox, options)
11 | routingExtensions(workbox, options)
12 |
13 | function getProp(obj, prop) {
14 | return prop.split('.').reduce((p, c) => p[c], obj)
15 | }
16 |
17 | function initWorkbox(workbox, options) {
18 | if (options.config) {
19 | // Set workbox config
20 | workbox.setConfig(options.config)
21 | }
22 |
23 | if (options.cacheNames) {
24 | // Set workbox cache names
25 | workbox.core.setCacheNameDetails(options.cacheNames)
26 | }
27 |
28 | if (options.clientsClaim) {
29 | // Start controlling any existing clients as soon as it activates
30 | workbox.core.clientsClaim()
31 | }
32 |
33 | if (options.skipWaiting) {
34 | workbox.core.skipWaiting()
35 | }
36 |
37 | if (options.cleanupOutdatedCaches) {
38 | workbox.precaching.cleanupOutdatedCaches()
39 | }
40 |
41 | if (options.offlineAnalytics) {
42 | // Enable offline Google Analytics tracking
43 | workbox.googleAnalytics.initialize()
44 | }
45 | }
46 |
47 | function precacheAssets(workbox, options) {
48 | if (options.preCaching.length) {
49 | workbox.precaching.precacheAndRoute(options.preCaching, options.cacheOptions)
50 | }
51 | }
52 |
53 |
54 | function runtimeCaching(workbox, options) {
55 | const requestInterceptor = {
56 | requestWillFetch({ request }) {
57 | if (request.cache === 'only-if-cached' && request.mode === 'no-cors') {
58 | return new Request(request.url, { ...request, cache: 'default', mode: 'no-cors' })
59 | }
60 | return request
61 | },
62 | fetchDidFail(ctx) {
63 | ctx.error.message =
64 | '[workbox] Network request for ' + ctx.request.url + ' threw an error: ' + ctx.error.message
65 | console.error(ctx.error, 'Details:', ctx)
66 | },
67 | handlerDidError(ctx) {
68 | ctx.error.message =
69 | `[workbox] Network handler threw an error: ` + ctx.error.message
70 | console.error(ctx.error, 'Details:', ctx)
71 | return null
72 | }
73 | }
74 |
75 | for (const entry of options.runtimeCaching) {
76 | const urlPattern = new RegExp(entry.urlPattern)
77 | const method = entry.method || 'GET'
78 |
79 | const plugins = (entry.strategyPlugins || [])
80 | .map(p => new (getProp(workbox, p.use))(...p.config))
81 |
82 | plugins.unshift(requestInterceptor)
83 |
84 | const strategyOptions = { ...entry.strategyOptions, plugins }
85 |
86 | const strategy = new workbox.strategies[entry.handler](strategyOptions)
87 |
88 | workbox.routing.registerRoute(urlPattern, strategy, method)
89 | }
90 | }
91 |
92 | function offlinePage(workbox, options) {
93 | if (options.offlinePage) {
94 | // Register router handler for offlinePage
95 | workbox.routing.registerRoute(new RegExp(options.pagesURLPattern), ({ request, event }) => {
96 | const strategy = new workbox.strategies[options.offlineStrategy]
97 | return strategy
98 | .handle({ request, event })
99 | .catch(() => caches.match(options.offlinePage))
100 | })
101 | }
102 | }
103 |
104 | function workboxExtensions(workbox, options) {
105 | <%= options.workboxExtensions %>
106 | }
107 |
108 | function cachingExtensions(workbox, options) {
109 | <%= options.cachingExtensions %>
110 | }
111 |
112 | function routingExtensions(workbox, options) {
113 | <%= options.routingExtensions %>
114 | }
115 |
--------------------------------------------------------------------------------
/types/workbox.d.ts:
--------------------------------------------------------------------------------
1 | import { HTTPMethod } from 'workbox-routing'
2 | import { Plugin as BackgroundSyncPlugin } from 'workbox-background-sync'
3 | import { Plugin as BroadcastUpdatePlugin } from 'workbox-broadcast-update'
4 | import { Plugin as CacheableResponsePlugin } from 'workbox-cacheable-response'
5 | import { Plugin as ExpirationPlugin } from 'workbox-expiration'
6 | import { Plugin as RangeRequestsPlugin } from 'workbox-range-requests'
7 | import {
8 | StaleWhileRevalidateOptions,
9 | CacheFirstOptions,
10 | NetworkFirstOptions,
11 | NetworkOnlyOptions,
12 | CacheOnlyOptions
13 | } from 'workbox-strategies'
14 |
15 | export type CachingStrategy = 'CacheFirst' | 'CacheOnly' | 'NetworkFirst' | 'NetworkOnly' | 'StaleWhileRevalidate'
16 |
17 | export type StrategyOptions =
18 | Omit
19 |
20 | type StrategyPluginOf = {
21 | use: name
22 | config: ConstructorParameters[0] | ConstructorParameters
23 | }
24 |
25 | export type BackgroundSync = StrategyPluginOf<'BackgroundSync', BackgroundSyncPlugin>
26 | export type BroadcastUpdate = StrategyPluginOf<'BroadcastUpdate', BroadcastUpdatePlugin>
27 | export type CacheableResponse = StrategyPluginOf<'CacheableResponse', CacheableResponsePlugin>
28 | export type Expiration = StrategyPluginOf<'Expiration', ExpirationPlugin>
29 | export type RangeRequests = StrategyPluginOf<'RangeRequests', RangeRequestsPlugin>
30 |
31 | export type StrategyPlugin = BackgroundSync | BroadcastUpdate | CacheableResponse | Expiration | RangeRequests
32 |
33 | export interface RuntimeCaching {
34 | urlPattern: string
35 | handler?: CachingStrategy
36 | method?: HTTPMethod
37 | strategyOptions?: StrategyOptions
38 | strategyPlugins?: StrategyPlugin[]
39 | }
40 |
41 | export interface WorkboxOptions {
42 | dev: boolean,
43 | workboxVersion: string,
44 | workboxURL: string,
45 | importScripts: string[],
46 | /**
47 | * Default: `true`
48 | */
49 | autoRegister: boolean,
50 | /**
51 | * Default: `true` for production mode
52 | */
53 | enabled: boolean,
54 | cacheNames: Record,
55 | config: Record,
56 | /**
57 | * Default: `true`
58 | */
59 | clientsClaim: boolean,
60 | /**
61 | * Default: `true`
62 | */
63 | skipWaiting: boolean,
64 | /**
65 | * Default: `false`
66 | */
67 | offlineAnalytics: boolean,
68 | workboxExtensions: string | string[],
69 | /**
70 | * Default: `[]`
71 | */
72 | preCaching: string[] | {url: string, revision: string}[],
73 | cacheOptions: {
74 | /**
75 | * Default: ` || nuxt`
76 | */
77 | cacheId: string,
78 | /**
79 | * Default: `/`
80 | */
81 | directoryIndex: string,
82 | /**
83 | * Default: `undefined`
84 | */
85 | revision: string | undefined
86 | },
87 | cachingExtensions: string | string[],
88 | cleanupOutdatedCaches: boolean,
89 | /**
90 | * Default: `true`
91 | */
92 | offline: boolean,
93 | /**
94 | * Default: `NetworkFirst`
95 | */
96 | offlineStrategy: CachingStrategy,
97 | offlinePage: string,
98 | offlineAssets: string[],
99 | runtimeCaching: RuntimeCaching[],
100 | /**
101 | * Default: `true`
102 | */
103 | cacheAssets: boolean,
104 | routingExtensions: string | string[],
105 | /**
106 | * Default: `/_nuxt/`
107 | */
108 | assetsURLPattern: string,
109 | /**
110 | * Auto generated based on `router.base`
111 | *
112 | * Default: `/`
113 | */
114 | pagesURLPattern: string,
115 | swTemplate: string,
116 | swURL: string,
117 | swDest: string,
118 | /**
119 | * Default: `routerBase`
120 | */
121 | swScope: string,
122 | /**
123 | * Default: `/`
124 | */
125 | routerBase: string,
126 | /**
127 | * Default: `/_nuxt`
128 | */
129 | publicPath: string
130 | }
131 |
--------------------------------------------------------------------------------
/docs/static/logo-dark.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/docs/static/logo-light.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/docs/content/en/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Meta Module
3 | description: Meta easily adds common meta tags into your project with zero-config needed
4 | position: 3
5 | category: Modules
6 | ---
7 |
8 |
9 | [](https://npmjs.com/package/@nuxtjs/meta)
10 | [](https://npmjs.com/package/@nuxtjs/meta)
11 |
12 | Meta easily adds common meta tags into your project with zero-config needed.
13 | You can optionally override meta using `pwa.meta` in `nuxt.config.js`:
14 |
15 | ```js{}[nuxt.config.js]
16 | pwa: {
17 | meta: {
18 | /* meta options */
19 | }
20 | }
21 | ```
22 |
23 | ## options
24 |
25 | ### `charset`
26 | - Default: `utf-8`
27 |
28 | ### `viewport`
29 |
30 | - Default: `width=device-width, initial-scale=1`
31 | - Meta: `viewport`
32 |
33 | ### `mobileApp`
34 | - Default: `true`
35 | - Meta: `mobile-web-app-capable`
36 |
37 | ### `mobileAppIOS`
38 | - Default: `false`
39 | - Meta: `apple-mobile-web-app-capable`
40 |
41 | Please read this resources before you enable `mobileAppIOS` option:
42 |
43 | - https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html
44 | - https://medium.com/@firt/dont-use-ios-web-app-meta-tag-irresponsibly-in-your-progressive-web-apps-85d70f4438cb
45 |
46 | ### `appleStatusBarStyle`
47 | - Default: `default`
48 | - Meta: `apple-mobile-web-app-status-bar-style`
49 |
50 | There are three options for the status bar style:
51 | 1. `default`: The default status bar style for Safari PWAs; white background with black text and icons.
52 | 2. `black`: Black background with white text and icons.
53 | 3. `black-translucent`: Transparent background with white text and icons. It is [not possible](https://stackoverflow.com/a/40786240/8677167) to have a transparent status bar with black text and icons.
54 |
55 | Note that with `black-translucent`, the web content is displayed on the entire screen, partially obscured by the status bar.
56 |
57 | These articles will help you decide an appropriate value:
58 | - https://medium.com/appscope/changing-the-ios-status-bar-of-your-progressive-web-app-9fc8fbe8e6ab.
59 | - https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html#//apple_ref/doc/uid/TP40008193-SW4
60 |
61 | ### `favicon`
62 | - Default: `true` (to use options.icons)
63 | - Meta: `shortcut icon` + `apple-touch-icon`
64 |
65 | ### `name`
66 | - Default: *npm_package_name*
67 | - Meta: `title`
68 |
69 | ### `author`
70 | - Default: *npm_package_author_name*
71 | - Meta: `author`
72 |
73 | ### `description`
74 | - Default: *npm_package_description*
75 | - Meta: `description`
76 |
77 | ### `theme_color`
78 | - Default: `undefined`
79 | - Meta: `theme-color`
80 |
81 | ### `lang`
82 | - Default: `en`
83 | - Meta: `lang`
84 |
85 | ### `ogType`
86 | - Default: `website`
87 | - Meta: `og:type`
88 |
89 | ### `ogSiteName`
90 | - Default: same as options.name
91 | - Meta: `og:site_name`
92 |
93 | ### `ogTitle`
94 | - Default: same as options.name
95 | - Meta: `og:title`
96 |
97 | ### `ogDescription`
98 | - Default: same as options.description
99 | - Meta: `og:description`
100 |
101 | ### `ogHost`
102 | Specify the domain that the site is hosted. Required for ogImage.
103 | - Default: `undefined`
104 | - Meta: `N/A`
105 |
106 | ### `ogImage`
107 | - Default: `true`
108 | - Meta: `og:image` and sub-tags
109 |
110 | These types are accepted:
111 |
112 | - Boolean: the icons from the `icon` module are used.
113 | - String: the path is used.
114 | - Object:
115 | * `path`: specify the path.
116 | * `width`, `height`: specify the dimensions, respectively.
117 | * `type`: specify the MIME type.
118 |
119 | ### `ogUrl`
120 | - Default: ogHost (if defined)
121 | - Meta: `og:url`
122 |
123 |
124 | ### `twitterCard`
125 | - Default: `undefined`
126 | - Meta: `twitter:card`
127 |
128 | ### `twitterSite`
129 | - Default: `undefined`
130 | - Meta: `twitter:site`
131 |
132 | ### `twitterCreator`
133 | - Default: `undefined`
134 | - Meta: `twitter:creator`
135 |
136 | ### `nativeUI`
137 | - Default: `false`
138 |
139 | By setting `meta.nativeUI` to `true` (Defaults to `false`) `viewport` defaults to `width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, minimal-ui` and `mobileAppIOS` will be enabled if not explicitly set to `false` which is suitable for native looking mobile apps.
140 |
--------------------------------------------------------------------------------
/src/workbox/options.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import cloneDeep from 'clone-deep'
3 | import { joinUrl, getRouteParams, startCase, randomString, PKG_DIR } from '../utils'
4 | import type { WorkboxOptions, PWAContext } from '../../types'
5 | import { defaults } from './defaults'
6 |
7 | export function getOptions (nuxt, pwa: PWAContext): WorkboxOptions {
8 | const options: WorkboxOptions = cloneDeep({ ...defaults, ...pwa.workbox })
9 |
10 | // enabled
11 | if (options.enabled === undefined) {
12 | options.enabled = !nuxt.options.dev || options.dev /* backward compat */
13 | }
14 |
15 | // routerBase
16 | if (!options.routerBase) {
17 | options.routerBase = nuxt.options.router.base
18 | }
19 |
20 | // publicPath
21 | if (!options.publicPath) {
22 | const { publicPath } = getRouteParams(nuxt.options)
23 | options.publicPath = publicPath
24 | }
25 |
26 | // swTemplate
27 | if (!options.swTemplate) {
28 | options.swTemplate = resolve(PKG_DIR, `templates/workbox/sw${options.enabled ? '' : '.unregister'}.js`)
29 | }
30 |
31 | // swDest
32 | if (!options.swDest) {
33 | options.swDest = resolve(nuxt.options.srcDir, nuxt.options.dir.static || 'static', 'sw.js')
34 | }
35 |
36 | // swURL
37 | options.swURL = joinUrl(options.routerBase, options.swURL || 'sw.js')
38 |
39 | // swScope
40 | if (!options.swScope) {
41 | options.swScope = options.routerBase
42 | }
43 |
44 | // Cache all _nuxt resources at runtime
45 | if (!options.assetsURLPattern) {
46 | options.assetsURLPattern = options.publicPath
47 | }
48 | if (options.cacheAssets) {
49 | options.runtimeCaching.push({
50 | urlPattern: options.assetsURLPattern,
51 | handler: nuxt.options.dev ? 'NetworkFirst' : 'CacheFirst'
52 | })
53 | }
54 |
55 | // Optionally cache other routes for offline
56 | if (!options.pagesURLPattern) {
57 | options.pagesURLPattern = options.routerBase
58 | }
59 | if (options.offline && !options.offlinePage) {
60 | options.runtimeCaching.push({
61 | urlPattern: options.pagesURLPattern,
62 | handler: options.offlineStrategy
63 | })
64 | }
65 |
66 | // Default revision
67 | if (!options.cacheOptions.revision) {
68 | options.cacheOptions.revision = randomString(12)
69 | }
70 | const normalizePreCaching = (arr: any | any[]) => [].concat(arr).map(url => ({
71 | revision: options.cacheOptions.revision,
72 | ...(typeof url === 'string' ? { url } : url)
73 | }))
74 |
75 | // Add start_url to precaching
76 | if (pwa.manifest && pwa.manifest.start_url) {
77 | options.preCaching.unshift(...normalizePreCaching(pwa.manifest.start_url))
78 | }
79 |
80 | // Add offlineAssets to precaching
81 | if (options.offlineAssets.length) {
82 | options.preCaching.unshift(...normalizePreCaching(options.offlineAssets))
83 | }
84 |
85 | // Add offlinePage to precaching
86 | if (options.offlinePage) {
87 | options.preCaching.unshift(...(normalizePreCaching(options.offlinePage)))
88 | }
89 |
90 | // Default cacheId
91 | if (options.cacheOptions.cacheId === undefined) {
92 | options.cacheOptions.cacheId = (process.env.npm_package_name || 'nuxt') + (nuxt.options.dev ? '-dev' : '-prod')
93 | }
94 |
95 | // Normalize preCaching
96 | options.preCaching = normalizePreCaching(options.preCaching)
97 |
98 | // Normalize runtimeCaching
99 | const pluginModules = {
100 | BackgroundSync: 'backgroundSync.BackgroundSyncPlugin',
101 | BroadcastUpdate: 'broadcastUpdate.BroadcastUpdatePlugin',
102 | CacheableResponse: 'cacheableResponse.CacheableResponsePlugin',
103 | Expiration: 'expiration.ExpirationPlugin',
104 | RangeRequests: 'rangeRequests.RangeRequestsPlugin'
105 | }
106 |
107 | options.runtimeCaching = options.runtimeCaching.map((entry) => {
108 | return {
109 | ...entry,
110 | handler: startCase(entry.handler) || 'NetworkFirst',
111 | method: entry.method || 'GET',
112 | strategyPlugins: (entry.strategyPlugins || []).map((plugin) => {
113 | const use = pluginModules[plugin.use]
114 | if (!use) {
115 | // eslint-disable-next-line no-console
116 | console.warn(`Invalid strategy plugin ${plugin.use}`)
117 | return false
118 | }
119 | return {
120 | use,
121 | config: Array.isArray(plugin.config) ? plugin.config : [plugin.config]
122 | }
123 | }).filter(Boolean)
124 | }
125 | })
126 |
127 | // Workbox URL
128 | if (!options.workboxURL) {
129 | options.workboxURL = `https://cdn.jsdelivr.net/npm/workbox-cdn@${options.workboxVersion}/workbox/workbox-sw.js`
130 | }
131 |
132 | // Workbox Config
133 | if (options.config.debug === undefined) {
134 | // Debug field is by default set to true for localhost domain which is not always ideal
135 | options.config.debug = options.dev || nuxt.options.dev
136 | }
137 |
138 | return options
139 | }
140 |
--------------------------------------------------------------------------------
/docs/content/en/manifest.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Manifest Module
3 | description: Manifest adds Web App Manifest with no pain
4 | position: 4
5 | category: Modules
6 | ---
7 |
8 | Manifest adds [Web App Manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest) with no pain.
9 |
10 | You can pass options to `pwa.manifest` in `nuxt.config.js` to override defaults. Check the
11 | [valid options](https://developer.mozilla.org/en-US/docs/Web/Manifest#Members) available and and its
12 | [default options](#default-options) for deeper insights.
13 |
14 | ```js{}[nuxt.config.js]
15 | pwa: {
16 | manifest: {
17 | name: 'My Awesome App',
18 | lang: 'fa',
19 | useWebmanifestExtension: false
20 | }
21 | }
22 | ```
23 |
24 | ## Default options
25 |
26 | | Property | Type | Default | Description |
27 | | --------------------------------- | --------------- | ------------------------------------------------------------ | --------------------------------------------------------------- |
28 | | `name` \*1 | `String` | `package.json`'s name property | [maximum of 45 characters] |
29 | | `short_name` \*1 | `String` | `package.json`'s name property | [maximum of 12 characters] |
30 | | `description` \*2 | `String` | `package.json`'s description property | |
31 | | `icons` \*1 | `Array