diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 0000000..f77beb8 --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,27 @@ +name: Linter + +on: [push, pull_request] + +jobs: + linter: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Use Node.js 12.x + uses: actions/setup-node@v2 + with: + node-version: '12.x' + - name: Cache NPM dependencies + uses: actions/cache@v1 + with: + path: node_modules + key: ${{ runner.OS }}-npm-cache + restore-keys: | + ${{ runner.OS }}-npm-cache + - name: Install Dependencies + run: npm install + - name: Lint + run: | + npm run eslint + env: + CI: true diff --git a/.github/workflows/tester.yml b/.github/workflows/tester.yml new file mode 100644 index 0000000..cfa980e --- /dev/null +++ b/.github/workflows/tester.yml @@ -0,0 +1,60 @@ +name: Tester + +on: [push, pull_request] + +jobs: + tester: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node-version: ['10.x', '12.x', '14.x'] + fail-fast: false + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - name: Cache NPM dependencies + uses: actions/cache@v1 + with: + path: node_modules + key: ${{ runner.os }}-npm-cache + restore-keys: ${{ runner.os }}-npm-cache + - name: Install Dependencies + run: npm install + - name: Test + run: npm run test + env: + CI: true + coverage: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + node-version: ['12.x'] + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - name: Cache NPM dependencies + uses: actions/cache@v1 + with: + path: node_modules + key: ${{ runner.os }}-npm-cache + restore-keys: ${{ runner.os }}-npm-cache + - name: Install Dependencies + run: npm install + - name: Run test + run: npm run test + # - name: Coverage + # run: npm run test-cov + # env: + # CI: true + # - name: Coveralls + # uses: coverallsapp/github-action@master + # with: + # github-token: ${{ secrets.github_token }} diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 0000000..89c2748 --- /dev/null +++ b/.yarnrc @@ -0,0 +1,2 @@ +disable-self-update-check true +registry "https://registry.npmjs.org" diff --git a/README.md b/README.md index 0922384..4645c75 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,16 @@ ## Install -If you use [next](https://github.com/theme-next/hexo-theme-next) or [cake](https://github.com/jiangtj/hexo-theme-cake) theme, just install it. - ```bash yarn add @jiangtj/hexo-next-pwa ``` -Others, you need to install hexo5.0 or the latest master branch +require: hexo5+ or [next](https://github.com/theme-next/hexo-theme-next)/[cake](https://github.com/jiangtj/hexo-theme-cake) theme ## Configure +I added some default configurations, see [default.yaml](default.yaml) + ```yml pwa: # Generate manifest.json @@ -40,44 +40,33 @@ pwa: # type: image/png # Generate sw.js serviceWorker: - cdn: https://cdn.jsdelivr.net/npm/workbox-sw@5/build/workbox-sw.min.js - # See workbox-build's `generateSW()` API - # Here are some default configuration, see `./default.yaml` + precache: + # precache posts url + posts: + enable: true + sort: -date + limit: 10 + # precache pages url + pages: true options: + # sw file path swDest: /sw.js ``` -`serviceWorker.options` refer to [the workbox-build's `generateSW()` API](https://developers.google.cn/web/tools/workbox/reference-docs/latest/module-workbox-build#.generateSW). Some configurations are not supported for the time being. See the compatibility table below. +`serviceWorker.precache` define how to precache url. -| feature | status | +`serviceWorker.options` refer to [the workbox-build's `generateSW()` API](https://developers.google.cn/web/tools/workbox/reference-docs/latest/module-workbox-build#.generateSW). Some configurations are not supported, due to the precache manifest is generated in different ways. + +| options | compatibility | | :--- | :--- | -| swDest | ✔ relative to build directory | -| importScripts | ✔ | -| offlineGoogleAnalytics | ✔ | -| runtimeCaching | ✔ | +| swDest | relative to build directory | | globDirectory | ✖ | -| additionalManifestEntries | ✖ | -| babelPresetEnvTargets | ✖ | -| cacheId | plan | -| cleanupOutdatedCaches | ✖ | -| clientsClaim | ✖ | -| directoryIndex | ✖ | | dontCacheBustURLsMatching | ✖ | | globFollow | ✖ | | globIgnores | ✖ | | globPatterns | ✖ | | globStrict | ✖ | -| ignoreURLParametersMatching | ✖ | -| inlineWorkboxRuntime | ✖ | | manifestTransforms | ✖ | -| maximumFileSizeToCacheInBytes | plan | -| mode | ✖ | +| maximumFileSizeToCacheInBytes | ✖ | | modifyURLPrefix | ✖ | -| navigateFallback | ✖ | -| navigateFallbackDenylist | ✖ | -| navigateFallbackAllowlist | ✖ | -| navigationPreload | ✖ | -| skipWaiting | plan | -| sourcemap | ✖ | | templatedURLs | ✖ | - diff --git a/default.yaml b/default.yaml index e3ede3e..6453de4 100644 --- a/default.yaml +++ b/default.yaml @@ -19,32 +19,37 @@ pwa: # sizes: 512x512 # type: image/png serviceWorker: - cdn: https://cdn.jsdelivr.net/npm/workbox-sw@5/build/workbox-sw.min.js - disableDevLogs: false + precache: + posts: + enable: true + sort: -date + limit: 10 + pages: true options: swDest: /sw.js - importScripts: [] runtimeCaching: - urlPattern: / handler: NetworkFirst options: cacheName: index - - urlPattern: !!js/regexp /\.(?:js|css)$/ + - urlPattern: regexp:\.(?:js|css)$ handler: StaleWhileRevalidate options: cacheName: js-css - - urlPattern: !!js/regexp /\.(?:png|gif|jpg|jpeg|svg)$/ + - urlPattern: regexp:\.(?:png|gif|jpg|jpeg|svg)$ handler: CacheFirst options: cacheName: images + cacheableResponse: + statuses: [0, 200] expiration: maxEntries: 60 maxAgeSeconds: 2592000 # 30 * 24 * 60 * 60 - - urlPattern: !!js/regexp /^https:\/\/fonts\.googleapis\.com/ + - urlPattern: regexp:^https:\/\/fonts\.googleapis\.com handler: StaleWhileRevalidate options: cacheName: google-fonts-stylesheets - - urlPattern: !!js/regexp /^https:\/\/fonts\.gstatic\.com/ + - urlPattern: regexp:^https:\/\/fonts\.gstatic\.com handler: CacheFirst options: cacheName: google-fonts-webfonts @@ -52,4 +57,3 @@ pwa: statuses: [0, 200] expiration: maxAgeSeconds: 31536000 # 365 * 24 * 60 * 60 - offlineGoogleAnalytics: false \ No newline at end of file diff --git a/index.js b/index.js index 43efc5c..22a4533 100644 --- a/index.js +++ b/index.js @@ -3,34 +3,24 @@ 'use strict'; const { generator } = hexo.extend; -const { mergeWith } = require('lodash'); -const yaml = require('js-yaml'); -const fs = require('fs'); -const { join } = require('path'); const injector = require('hexo-extend-injector2')(hexo); +const config = require('./lib/get-default-config')(hexo); +const validate = require('./lib/validate-sw-options'); -/** - * config - */ -const defaultConfig = yaml.load(fs.readFileSync(join(__dirname, 'default.yaml'), 'utf8')); -const config = mergeWith(defaultConfig.pwa, hexo.config.pwa, (objValue, srcValue) => { - if (Array.isArray(objValue)) { - return srcValue; - } -}); +const { manifest, serviceWorker } = config; +serviceWorker.options = validate(serviceWorker.options); /** * generator manifest */ -injector.register('head-end', ``); +injector.register('head-end', ``); generator.register('pwa_manifest', () => { - const manifest = config.manifest; return { path: manifest.path, data: JSON.stringify( Object.assign({ - name: config.title, - start_url: config.url + name: hexo.config.title, + start_url: hexo.config.url }, manifest.body) ) }; @@ -43,9 +33,18 @@ injector.register('body-end', ` `); -generator.register('pwa_service_worker', () => require('./lib/generate-sw-string')(config.serviceWorker)); + +generator.register('pwa_service_worker', locals => { + return require('./lib/generate-sw-string')(locals, serviceWorker) + .then(({files}) => { + return files.map(file => ({ + path: file.name, + data: file.contents + })); + }); +}); diff --git a/lib/define-cache.js b/lib/define-cache.js deleted file mode 100644 index f56fbdc..0000000 --- a/lib/define-cache.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -module.exports = class DefineCache { - constructor() { - this.cache = new Map(); - } - set(key, val) { - const defineSet = this.cache.get(key) || new Set(); - defineSet.add(val); - this.cache.set(key, defineSet); - } - toString() { - return Array.from(this.cache.entries()) - .map(([key, val]) => { - return `const { ${Array.from(val.values()).join(', ')} } = ${key};`; - }) - .join('\n'); - } -}; diff --git a/lib/generate-sw-string.js b/lib/generate-sw-string.js index 7f11215..1d40ad4 100644 --- a/lib/generate-sw-string.js +++ b/lib/generate-sw-string.js @@ -1,65 +1,38 @@ 'use strict'; -const ejs = require('ejs'); -const { resolve } = require('path'); -const { upperFirst } = require('lodash'); -const DefineCache = require('./define-cache'); -const define = new DefineCache(); +const getStringHash = require('./get-string-hash'); +const writeServiceWorkerUsingDefaultTemplate = require('./write-sw-using-default-template'); -const handeRuntimeCaching = runtimeCachingEntry => { - let capture = runtimeCachingEntry.urlPattern; - if (typeof capture === 'string') { - capture = `'${capture}'`; - } - - const options = runtimeCachingEntry.options || {}; - - options.plugins = options.plugins || []; - ['backgroundSync', 'broadcastUpdate', 'cacheableResponse', 'expiration'].forEach(item => { - if (options[item]) { - const plugin = {}; - plugin.name = item; - plugin.options = options[item]; - options.plugins.push(plugin); - delete options[item]; - } - }); - options.plugins = options.plugins.map(plugin => { - const pluginDefine = `${upperFirst(plugin.name)}Plugin`; - define.set(`workbox.${plugin.name}`, pluginDefine); - return `new ${pluginDefine}(${JSON.stringify(plugin.options)})`; - }); +module.exports = async (locals, serviceWorker) => { - define.set('workbox.strategies', runtimeCachingEntry.handler); - const handler = `new ${runtimeCachingEntry.handler}(${JSON.stringify(options, (key, value) => { - if (key === 'plugins') { - return '$plugins-point'; - } - return value; - })})`.replace('"$plugins-point"', `[${options.plugins.join(',')}]`); + const { precache, options } = serviceWorker; + const { pages, posts } = locals; - const method = runtimeCachingEntry.method || 'GET'; + const manifestEntries = []; - define.set('workbox.routing', 'registerRoute'); - return `registerRoute(${capture}, ${handler}, '${method}');`; -}; - -module.exports = serviceWorker => { - const options = Object.assign({ - workboxCDN: serviceWorker.cdn, - disableDevLogs: serviceWorker.disableDevLogs - }, serviceWorker.options); + if (precache.posts.enable) { + posts.sort(precache.posts.sort) + .limit(precache.posts.limit) + .forEach(item => { + manifestEntries.push({ + url: item.path, + revision: getStringHash(item.content) + }); + }); + } - if (options.runtimeCaching) { - options.runtimeCaching = options.runtimeCaching.map(handeRuntimeCaching); + if (precache.pages) { + pages.forEach(item => { + manifestEntries.push({ + url: item.path, + revision: getStringHash(item.content) + }); + }); } - options.importDefine = define.toString(); + const files = await writeServiceWorkerUsingDefaultTemplate(Object.assign({ + manifestEntries + }, options)); - return { - path: options.swDest, - data: () => { - return ejs.renderFile(resolve(__dirname, '../templates/sw-template.ejs'), options); - } - }; + return {files}; }; diff --git a/lib/get-default-config.js b/lib/get-default-config.js new file mode 100644 index 0000000..83ba2ac --- /dev/null +++ b/lib/get-default-config.js @@ -0,0 +1,20 @@ +'use strict'; + +const handleRegExp = require('./handle-regexp'); +const { load } = require('js-yaml'); +const { readFileSync } = require('fs'); +const { join } = require('path'); +const { mergeWith } = require('lodash'); + +module.exports = ctx => { + const schema = require('./get-yaml-schema')(ctx); + const defaultConfig = load(readFileSync(join(__dirname, '../default.yaml'), 'utf8'), { schema }); + const config = mergeWith(defaultConfig.pwa, ctx.config.pwa, (objValue, srcValue) => { + if (Array.isArray(objValue)) { + return srcValue; + } + }); + config.serviceWorker.options.runtimeCaching.forEach(element => handleRegExp(element, 'urlPattern')); + return config; +}; + diff --git a/lib/get-string-hash.js b/lib/get-string-hash.js new file mode 100644 index 0000000..3ca4d4c --- /dev/null +++ b/lib/get-string-hash.js @@ -0,0 +1,17 @@ +/* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ + +'use strict'; + +const crypto = require('crypto'); + +module.exports = string => { + const md5 = crypto.createHash('md5'); + md5.update(string); + return md5.digest('hex'); +}; diff --git a/lib/get-yaml-schema.js b/lib/get-yaml-schema.js new file mode 100644 index 0000000..1f25e31 --- /dev/null +++ b/lib/get-yaml-schema.js @@ -0,0 +1,15 @@ +'use strict'; + +const yaml = require('js-yaml'); + +module.exports = ctx => { + let schema = yaml.DEFAULT_SCHEMA; + try { + const { all } = require('js-yaml-js-types'); + schema = yaml.DEFAULT_SCHEMA.extend(all); + } catch (ignore) { + ctx.log.debug('Can not get js-yaml-js-types, please use regexp: prefix!'); + } + return schema; +}; + diff --git a/lib/handle-regexp.js b/lib/handle-regexp.js new file mode 100644 index 0000000..5d9eb33 --- /dev/null +++ b/lib/handle-regexp.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = (element, ...props) => { + for (const prop of props) { + const val = element[prop]; + if (typeof val === 'string') { + if (val.startsWith('regexp:')) { + element[prop] = new RegExp(val.substring(7)); + } + } + } +}; + diff --git a/lib/validate-sw-options.js b/lib/validate-sw-options.js new file mode 100644 index 0000000..ab4e976 --- /dev/null +++ b/lib/validate-sw-options.js @@ -0,0 +1,10 @@ +'use strict'; + +const generateSWSchema = require('workbox-build/src/options/schema/generate-sw'); +const validate = require('workbox-build/src/lib/validate-options'); + +module.exports = options => { + options.globDirectory = 'ignore'; + return validate(options, generateSWSchema); +}; + diff --git a/lib/write-sw-using-default-template.js b/lib/write-sw-using-default-template.js new file mode 100644 index 0000000..d895d0c --- /dev/null +++ b/lib/write-sw-using-default-template.js @@ -0,0 +1,66 @@ +/* eslint-disable node/no-extraneous-require */ +/* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. +*/ + +'use strict'; + +const bundle = require('workbox-build/src/lib/bundle'); +const populateSWTemplate = require('workbox-build/src/lib/populate-sw-template'); + +module.exports = async ({ + babelPresetEnvTargets, + cacheId, + cleanupOutdatedCaches, + clientsClaim, + directoryIndex, + disableDevLogs, + ignoreURLParametersMatching, + importScripts, + inlineWorkboxRuntime, + manifestEntries, + mode, + navigateFallback, + navigateFallbackDenylist, + navigateFallbackAllowlist, + navigationPreload, + offlineGoogleAnalytics, + runtimeCaching, + skipWaiting, + sourcemap, + swDest +}) => { + + const unbundledCode = populateSWTemplate({ + cacheId, + cleanupOutdatedCaches, + clientsClaim, + directoryIndex, + disableDevLogs, + ignoreURLParametersMatching, + importScripts, + manifestEntries, + navigateFallback, + navigateFallbackDenylist, + navigateFallbackAllowlist, + navigationPreload, + offlineGoogleAnalytics, + runtimeCaching, + skipWaiting + }); + + const files = await bundle({ + babelPresetEnvTargets, + inlineWorkboxRuntime, + mode, + sourcemap, + swDest, + unbundledCode + }); + + return files; +}; diff --git a/package.json b/package.json index 641859a..8457115 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,17 @@ { "name": "@jiangtj/hexo-next-pwa", - "version": "1.0.2", + "version": "2.0.1-beta6", "description": "", "main": "index.js", "files": [ "index.js", "default.yaml", - "lib", - "templates" + "lib" ], "scripts": { "eslint": "eslint .", "test": "mocha test/index.js", - "test-workbox-build": "mocha test/workbox-build.js" + "p": "yarn --proxy=http://localhost:1080" }, "repository": { "type": "git", @@ -29,17 +28,17 @@ }, "homepage": "https://github.com/jiangtj-lab/hexo-next-pwa#readme", "dependencies": { - "ejs": "^3.1.3", "hexo-extend-injector2": "^0.2.0", - "js-yaml": "^3.14.0", - "lodash": "^4.17.15" + "js-yaml": "^4.0.0", + "js-yaml-js-types": "^1.0.0", + "lodash": "^4.17.20", + "workbox-build": "^6.1.0" }, "devDependencies": { "chai": "4.2.0", - "eslint": "7.11.0", + "eslint": "7.19.0", "eslint-config-hexo": "4.1.0", - "hexo": "5.2.0", - "mocha": "8.0.1", - "workbox-build": "5.1.4" + "hexo": "5.3.0", + "mocha": "8.2.1" } } diff --git a/templates/sw-template.ejs b/templates/sw-template.ejs deleted file mode 100644 index cb0b24d..0000000 --- a/templates/sw-template.ejs +++ /dev/null @@ -1,19 +0,0 @@ -/* global importScripts, workbox */ - -'use strict'; - -importScripts('<%= workboxCDN %>'); - -<%= importDefine %> - -<% if (Array.isArray(importScripts) && importScripts.length > 0) { %> -importScripts( - <%- importScripts.map(JSON.stringify).join(',\n ') %> -); -<% } %> - -<% if (runtimeCaching) { %><%- runtimeCaching.join('\n') %><% } %> - -<% if (offlineGoogleAnalytics) { %>workbox.googleAnalytics.initialize();<% } %> - -<% if (disableDevLogs) { %>self.__WB_DISABLE_DEV_LOGS = true;<% } %> diff --git a/test/index.js b/test/index.js index a5bc2b0..bb389d9 100644 --- a/test/index.js +++ b/test/index.js @@ -1,50 +1,39 @@ 'use strict'; require('chai').should(); + const Hexo = require('hexo'); -// eslint-disable-next-line no-unused-vars const hexo = new Hexo(__dirname, { silent: true }); -const { load } = require('js-yaml'); -const { readFileSync, writeFileSync, mkdirSync } = require('fs'); -const { resolve } = require('path'); - -const defaultYaml = readFileSync(resolve(__dirname, '../default.yaml'), 'utf8'); -const defaultConfig = () => { - return load(defaultYaml).pwa; -}; - -try { - mkdirSync('test/temp'); -} catch (ignore) {} +const { readFileSync } = require('fs'); +const { join } = require('path'); describe('main', () => { - it('generate-sw-string() with default', () => { - const { serviceWorker } = defaultConfig(); - const template = require('../lib/generate-sw-string')(serviceWorker); - template.path.should.eql(serviceWorker.options.swDest); - template.data() - .then(item => writeFileSync('./test/temp/sw-default.js', item)); + it('test js-yaml-js-types', () => { + const yaml = require('js-yaml'); + const schema = require('../lib/get-yaml-schema')(hexo); + const config = yaml.load(readFileSync(join(__dirname, 'js-yaml-js-types.yaml'), 'utf8'), { schema }); + config.should.eql({ urlPattern: /\.(?:js|css)$/ }); }); - it('generate-sw-string() with offlineGoogleAnalytics', () => { - const { serviceWorker } = defaultConfig(); - Object.assign(serviceWorker.options, { - offlineGoogleAnalytics: true + it('test default config', () => { + const config = require('../lib/get-default-config')(hexo); + const { runtimeCaching } = config.serviceWorker.options; + runtimeCaching[0].should.eql({ + urlPattern: '/', + handler: 'NetworkFirst', + options: { cacheName: 'index' } + }); + runtimeCaching[1].should.eql({ + urlPattern: /\.(?:js|css)$/, + handler: 'StaleWhileRevalidate', + options: { cacheName: 'js-css' } }); - const template = require('../lib/generate-sw-string')(serviceWorker); - template.data() - .then(item => writeFileSync('./test/temp/sw-analytics.js', item)); }); - it('generate-sw-string() with importScripts', () => { - const { serviceWorker } = defaultConfig(); - Object.assign(serviceWorker.options, { - importScripts: [ - 'custom1', 'custom2', 'custom3' - ] - }); - const template = require('../lib/generate-sw-string')(serviceWorker); - template.data() - .then(item => writeFileSync('./test/temp/sw-scripts.js', item)); + it('validate default sw options', () => { + const config = require('../lib/get-default-config')(hexo); + const validate = require('../lib/validate-sw-options'); + const options = validate(config.serviceWorker.options); + options.should.exist; }); }); diff --git a/test/js-yaml-js-types.yaml b/test/js-yaml-js-types.yaml new file mode 100644 index 0000000..619e738 --- /dev/null +++ b/test/js-yaml-js-types.yaml @@ -0,0 +1 @@ +urlPattern: !!js/regexp /\.(?:js|css)$/ diff --git a/test/workbox-build.js b/test/workbox-build.js deleted file mode 100644 index 6352fe0..0000000 --- a/test/workbox-build.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; - -/** - * 生成原生workbox-build内容,作为内建的generate-sw-string的参考对象 - */ - -require('chai').should(); -const { load } = require('js-yaml'); -const { readFileSync } = require('fs'); -const { resolve } = require('path'); -const { generateSW } = require('workbox-build'); - -describe('main', () => { - const config = load(readFileSync(resolve(__dirname, '../default.yaml'), 'utf8')).pwa; - - const options = Object.assign(config.serviceWorker.options, { - mode: 'dev', - sourcemap: false, - swDest: 'test/temp/sw-build.js', - offlineGoogleAnalytics: true - }); - - generateSW(options) - .then(obj => console.log(obj)); -});