Skip to content

Commit 902f6c0

Browse files
committed
feat: pwa
1 parent 8adcabc commit 902f6c0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+908
-73
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"rules": {
3+
"vue-libs/no-async-functions": 0
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
jest.setTimeout(30000)
2+
3+
const fs = require('fs')
4+
const path = require('path')
5+
const portfinder = require('portfinder')
6+
const { createServer } = require('http-server')
7+
const { defaults } = require('@vue/cli/lib/options')
8+
const create = require('@vue/cli-test-utils/createTestProject')
9+
const launchPuppeteer = require('@vue/cli-test-utils/launchPuppeteer')
10+
11+
let server, browser
12+
test('pwa', async () => {
13+
// it's ok to mutate here since jest loads each test in a separate vm
14+
defaults.plugins['@vue/cli-plugin-pwa'] = {}
15+
const project = await create('pwa-build', defaults)
16+
expect(project.has('src/registerServiceWorker.js')).toBe(true)
17+
18+
const { stdout } = await project.run('vue-cli-service build')
19+
expect(stdout).toMatch('Build complete.')
20+
21+
const distDir = path.join(project.dir, 'dist')
22+
const hasFile = file => fs.existsSync(path.join(distDir, file))
23+
expect(hasFile('index.html')).toBe(true)
24+
expect(hasFile('favicon.ico')).toBe(true)
25+
expect(hasFile('js')).toBe(true)
26+
expect(hasFile('css')).toBe(true)
27+
28+
// PWA specific files
29+
expect(hasFile('manifest.json')).toBe(true)
30+
expect(hasFile('img/icons/android-chrome-512x512.png')).toBe(true)
31+
32+
// Make sure the base preload/prefetch are not affected
33+
const index = await project.read('dist/index.html')
34+
// should split and preload app.js & vendor.js
35+
expect(index).toMatch(/<link rel=preload [^>]+app[^>]+\.js>/)
36+
expect(index).toMatch(/<link rel=preload [^>]+vendor[^>]+\.js>/)
37+
// should not preload manifest because it's inlined
38+
expect(index).not.toMatch(/<link rel=preload [^>]+manifest[^>]+\.js>/)
39+
// should inline manifest and wepback runtime
40+
expect(index).toMatch('webpackJsonp')
41+
42+
// PWA specific directives
43+
expect(index).toMatch(`<link rel=manifest href=/manifest.json>`)
44+
expect(index).toMatch(`<!--[if IE]><link rel="shortcut icon" href="/favicon.ico"><![endif]-->`)
45+
expect(index).toMatch(`<meta name=apple-mobile-web-app-capable content=yes>`)
46+
47+
// should import service worker script
48+
const main = await project.read('src/main.js')
49+
expect(main).toMatch(`import './registerServiceWorker'`)
50+
51+
const port = await portfinder.getPortPromise()
52+
server = createServer({ root: distDir })
53+
54+
await new Promise((resolve, reject) => {
55+
server.listen(port, err => {
56+
if (err) return reject(err)
57+
resolve()
58+
})
59+
})
60+
61+
const launched = await launchPuppeteer(`http://localhost:${port}/`)
62+
browser = launched.browser
63+
64+
await new Promise(r => setTimeout(r, 500))
65+
const logs = launched.logs
66+
expect(logs.some(msg => msg.match(/Content is cached for offline use/))).toBe(true)
67+
expect(logs.some(msg => msg.match(/This web app is being served cache-first/))).toBe(true)
68+
})
69+
70+
afterAll(async () => {
71+
await browser.close()
72+
server.close()
73+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = api => {
2+
api.render('./template')
3+
4+
api.postProcessFiles(files => {
5+
const main = files['src/main.js']
6+
if (main) {
7+
// inject import for registerServiceWorker script into main.js
8+
const lines = main.split('\n').reverse()
9+
const lastImportIndex = lines.findIndex(line => line.match(/^import/))
10+
lines[lastImportIndex] += `\nimport './registerServiceWorker'`
11+
files['src/main.js'] = lines.reverse().join('\n') + '\n'
12+
}
13+
})
14+
}
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "<%- rootOptions.projectName %>",
3+
"short_name": "<%- rootOptions.projectName %>",
4+
"icons": [
5+
{
6+
"src": "/img/icons/android-chrome-192x192.png",
7+
"sizes": "192x192",
8+
"type": "image/png"
9+
},
10+
{
11+
"src": "/img/icons/android-chrome-512x512.png",
12+
"sizes": "512x512",
13+
"type": "image/png"
14+
}
15+
],
16+
"start_url": "/index.html",
17+
"display": "standalone",
18+
"background_color": "#000000",
19+
"theme_color": "#4DBA87"
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* eslint-disable no-console */
2+
3+
import { register } from '@vue/cli-plugin-pwa/registerServiceWorker'
4+
5+
const serviceWorkerEventBus = register()
6+
7+
serviceWorkerEventBus.$on('new-content', () => {
8+
console.log('New content is available; please refresh.')
9+
})
10+
11+
serviceWorkerEventBus.$on('content-cached', () => {
12+
console.log('Content is cached for offline use.')
13+
})
14+
15+
serviceWorkerEventBus.$on('offline', () => {
16+
console.log('No internet connection found. App is running in offline mode.')
17+
})
18+
19+
serviceWorkerEventBus.$on('error', error => {
20+
console.error('Error during service worker registration:', error)
21+
})
22+
23+
export default serviceWorkerEventBus

packages/@vue/cli-plugin-pwa/index.js

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
module.exports = (api, options) => {
2+
api.chainWebpack(webpackConfig => {
3+
const name = api.service.pkg.name
4+
5+
// make sure the registerServiceWorker script is transpiled
6+
webpackConfig.module
7+
.rule('js')
8+
.include
9+
.add(require.resolve('./registerServiceWorker'))
10+
11+
// the pwa plugin hooks on to html-webpack-plugin
12+
// and injects icons, manifest links & other PWA related tags into <head>
13+
webpackConfig
14+
.plugin('pwa')
15+
.use(require('./lib/HtmlPwaPlugin'), [Object.assign({
16+
name
17+
}, options.pwa)])
18+
19+
// generate /service-worker.js in production mode
20+
if (process.env.NODE_ENV === 'production') {
21+
webpackConfig
22+
.plugin('sw-precache')
23+
.use(require('sw-precache-webpack-plugin'), [{
24+
cacheId: name,
25+
filename: 'service-worker.js',
26+
staticFileGlobs: [`${options.outputDir}/**/*.{js,html,css}`],
27+
minify: true,
28+
stripPrefix: `${options.outputDir}/`
29+
}])
30+
}
31+
})
32+
33+
// install dev server middleware for resetting service worker during dev
34+
const createNoopServiceWorkerMiddleware = require('./lib/noopServiceWorkerMiddleware')
35+
api.configureDevServer(app => {
36+
app.use(createNoopServiceWorkerMiddleware())
37+
})
38+
}

0 commit comments

Comments
 (0)