├── .eslintrc.json ├── .github └── workflows │ ├── codeql-analysis.yml │ └── tests.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── SECURITY.md ├── babel.config.json ├── index.js ├── package-lock.json ├── package.json └── test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:prettier/recommended", 9 | "plugin:import/errors" 10 | ], 11 | "overrides": [ 12 | { 13 | "files": ["test.js"], 14 | "env": { "jest": true } 15 | } 16 | ], 17 | "parser": "@babel/eslint-parser", 18 | "parserOptions": { 19 | "ecmaVersion": 6, 20 | "sourceType": "script" 21 | }, 22 | "rules": { 23 | "import/order": ["error", { "newlines-between": "always" }] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | schedule: 9 | - cron: '27 6 * * 3' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v3 23 | 24 | - name: Initialize CodeQL 25 | uses: github/codeql-action/init@v2 26 | with: 27 | languages: javascript 28 | 29 | - name: Autobuild 30 | uses: github/codeql-action/autobuild@v2 31 | 32 | - name: Perform CodeQL Analysis 33 | uses: github/codeql-action/analyze@v2 34 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: "Tests" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup node 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: 14 22 | 23 | - name: Install modules 24 | run: npm ci 25 | 26 | - name: Run tests 27 | run: npm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc.json 2 | .github/ 3 | .idea/ 4 | babel.config.json 5 | node_modules/ 6 | test.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Cansin Yildiz 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # next-with-workbox 2 | [![tests](https://github.com/cansin/next-with-workbox/actions/workflows/tests.yml/badge.svg)](https://github.com/cansin/next-with-workbox/actions/workflows/tests.yml) 3 | [![codeql](https://github.com/cansin/next-with-workbox/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/cansin/next-with-workbox/actions/workflows/codeql-analysis.yml) 4 | [![size](https://img.shields.io/bundlephobia/minzip/next-with-workbox)](https://bundlephobia.com/result?p=next-with-workbox) 5 | [![dependencies](https://img.shields.io/librariesio/release/npm/next-with-workbox)](https://libraries.io/npm/next-with-workbox) 6 | [![downloads](https://img.shields.io/npm/dm/next-with-workbox)](https://www.npmjs.com/package/next-with-workbox) 7 | [![license](https://img.shields.io/github/license/cansin/next-with-workbox)](https://github.com/cansin/next-with-workbox/blob/master/LICENSE) 8 | 9 | Higher order Next.js config to generate a [Workbox service worker](https://developers.google.com/web/tools/workbox). 10 | It auto-magically sets up certain aspects like pre-caching `public` folder and cache busting exclusions in order 11 | to get the most out of Workbox with Next.js. 12 | Heavily inspired from [shadowwalker/next-pwa](https://github.com/shadowwalker/next-pwa). 13 | 14 | ## Install 15 | 16 | ```bash 17 | npm install next-with-workbox --save 18 | # or 19 | yarn add next-with-workbox 20 | ``` 21 | 22 | ## Basic Usage 23 | 24 | Update or create `next.config.js` with 25 | 26 | ```js 27 | const withWorkbox = require("next-with-workbox"); 28 | 29 | module.exports = withWorkbox({ 30 | workbox: { 31 | swSrc: "worker.js", 32 | }, 33 | // . 34 | // .. 35 | // ... other Next.js config 36 | }); 37 | ``` 38 | 39 | Add `public/sw.js` and `public/sw.js.map` to your `.gitignore` 40 | 41 | ```git 42 | public/sw.js 43 | public/sw.js.map 44 | ``` 45 | 46 | Create your service worker at `/path/to/your-next-app/worker.js` 47 | 48 | ```js 49 | import { precacheAndRoute } from "workbox-precaching"; 50 | 51 | precacheAndRoute(self.__WB_MANIFEST); 52 | ``` 53 | 54 | Register your service worker at `/path/to/your-next-app/pages/_app.js`: 55 | 56 | ```js 57 | import React, { useEffect } from "react"; 58 | import { Workbox } from "workbox-window"; 59 | 60 | function App({ Component, pageProps }) { 61 | useEffect(() => { 62 | if ( 63 | !("serviceWorker" in navigator) || 64 | process.env.NODE_ENV !== "production" 65 | ) { 66 | console.warn("Pwa support is disabled"); 67 | return; 68 | } 69 | 70 | const wb = new Workbox("sw.js", { scope: "/" }); 71 | wb.register(); 72 | }, []); 73 | 74 | return ; 75 | } 76 | 77 | export default App; 78 | ``` 79 | 80 | ## Configuration 81 | 82 | There are options you can use to customize the behavior of this plugin 83 | by adding `workbox` object in the Next.js config in `next.config.js`. 84 | Alongside those documented `workbox` options below, this library would 85 | also pass through any [Workbox plugin options](https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-webpack-plugin) 86 | to the appropriate plugin 87 | 88 | ```js 89 | const withWorkbox = require("next-with-workbox"); 90 | 91 | module.exports = withWorkbox({ 92 | workbox: { 93 | dest: "public", 94 | swDest: "sw.js", 95 | swSrc: "worker.js", 96 | force: true, 97 | }, 98 | }); 99 | ``` 100 | 101 | ### Available Options 102 | 103 | - **dest:** string - the destination folder to put generated files, relative to the project root. 104 | - defaults to `public`. 105 | - **swDest:** string - the destination file to write the service worker code to. 106 | - defaults to `sw.js`. 107 | - **swSrc:** string - the input file to read the custom service worker code from. Setting this 108 | switches to [InjectManifest](https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-webpack-plugin.InjectManifest) plugin. 109 | If not set, [GenerateSW](https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-webpack-plugin.GenerateSW) plugin 110 | is used. 111 | - defaults to `false`. 112 | - **force:** boolean - whether to force enable Workbox in dev mode. 113 | - defaults to `false`. 114 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 3.0.x | :white_check_mark: | 8 | | 2.0.1 | :white_check_mark: | 9 | | < 2.0.1 | :x: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Please either use [issues](https://github.com/cansin/next-with-workbox/issues) to report a vulnerability, 14 | or create a [pull request](https://github.com/cansin/next-with-workbox/pulls) to fix them. 15 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const crypto = require("crypto"); 3 | 4 | const glob = require("glob"); 5 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 6 | const WorkboxPlugin = require("workbox-webpack-plugin"); 7 | 8 | const getRevision = (file) => 9 | crypto.createHash("md5").update(Buffer.from(file)).digest("hex"); 10 | 11 | function withWorkbox(nextConfig = {}) { 12 | return { 13 | ...nextConfig, 14 | webpack(config, options) { 15 | if (typeof nextConfig.webpack === "function") { 16 | config = nextConfig.webpack(config, options); 17 | } 18 | 19 | const { 20 | dev, 21 | isServer, 22 | config: { 23 | workbox: { 24 | additionalManifestEntries = [], 25 | dest = "public", 26 | dontCacheBustURLsMatching = false, 27 | exclude = [], 28 | force = false, 29 | modifyURLPrefix = {}, 30 | swDest = "sw.js", 31 | swSrc = false, 32 | ...workboxOptions 33 | } = {}, 34 | }, 35 | } = options; 36 | 37 | if (isServer) { 38 | return config; 39 | } 40 | 41 | if (dev && !force) { 42 | console.log("> Progressive Web App is disabled"); 43 | return config; 44 | } 45 | 46 | const swDestPath = path.join(options.dir, dest, swDest); 47 | 48 | console.log("> Progressive web app is enabled using Workbox"); 49 | console.log(`> Service worker destination path: "${swDestPath}"`); 50 | 51 | config.plugins.push( 52 | new CleanWebpackPlugin({ 53 | cleanOnceBeforeBuildPatterns: [swDestPath, `${swDestPath}.map`], 54 | }) 55 | ); 56 | 57 | const defaultDontCacheBustURLsMatching = /^\/_next\/static\/.*/iu; 58 | const defaultWorkboxOptions = { 59 | swDest: swDestPath, 60 | dontCacheBustURLsMatching: dontCacheBustURLsMatching 61 | ? new RegExp( 62 | `${dontCacheBustURLsMatching.source}|${defaultDontCacheBustURLsMatching.source}`, 63 | "iu" 64 | ) 65 | : defaultDontCacheBustURLsMatching, 66 | additionalManifestEntries: glob 67 | .sync("**/*", { 68 | cwd: dest, 69 | nodir: true, 70 | }) 71 | .filter((f) => f.indexOf(swDest) !== 0) 72 | .map((f) => ({ 73 | url: `/${f}`, 74 | revision: getRevision(`public/${f}`), 75 | })) 76 | .concat(additionalManifestEntries), 77 | exclude: [ 78 | /^build-manifest\.json$/i, 79 | /^react-loadable-manifest\.json$/i, 80 | /\/_error\.js$/i, 81 | /\.js\.map$/i, 82 | ...exclude, 83 | ], 84 | modifyURLPrefix: { 85 | [`${config.output.publicPath || ""}static/`]: "/_next/static/", 86 | ...modifyURLPrefix, 87 | }, 88 | }; 89 | 90 | if (swSrc) { 91 | const swSrcPath = path.join(options.dir, swSrc); 92 | console.log(`> Service worker source path: "${swSrcPath}"`); 93 | console.log('> Using "WorkboxPlugin.InjectManifest" plugin'); 94 | config.plugins.push( 95 | new WorkboxPlugin.InjectManifest({ 96 | swSrc: swSrcPath, 97 | ...defaultWorkboxOptions, 98 | ...workboxOptions, 99 | }) 100 | ); 101 | } else { 102 | console.log('> Using "WorkboxPlugin.GenerateSW" plugin'); 103 | config.plugins.push( 104 | new WorkboxPlugin.GenerateSW({ 105 | ...defaultWorkboxOptions, 106 | ...workboxOptions, 107 | }) 108 | ); 109 | } 110 | 111 | console.log("> Progressive web app configuration complete"); 112 | return config; 113 | }, 114 | }; 115 | } 116 | 117 | module.exports = withWorkbox; 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-with-workbox", 3 | "version": "3.0.5", 4 | "private": false, 5 | "description": "Higher order Next.js configuration for generating a service worker", 6 | "keywords": [ 7 | "next.js", 8 | "workbox", 9 | "inject-manifest", 10 | "generate-sw", 11 | "service-worker", 12 | "webpack" 13 | ], 14 | "homepage": "https://github.com/cansin/next-with-workbox#readme", 15 | "bugs": { 16 | "url": "https://github.com/cansin/next-with-workbox/issues" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/cansin/next-with-workbox.git" 21 | }, 22 | "license": "MIT", 23 | "author": { 24 | "name": "Cansin Yildiz", 25 | "email": "cansinyildiz@gmail.com", 26 | "url": "https://www.cansinyildiz.com/" 27 | }, 28 | "main": "index.js", 29 | "scripts": { 30 | "test": "eslint . && jest" 31 | }, 32 | "dependencies": { 33 | "clean-webpack-plugin": "^4.0.0", 34 | "glob": "^8.0.3", 35 | "workbox-webpack-plugin": "^6.5.3" 36 | }, 37 | "devDependencies": { 38 | "@babel/eslint-parser": "^7.17.0", 39 | "@babel/preset-env": "^7.18.0", 40 | "eslint": "^8.16.0", 41 | "eslint-config-prettier": "^8.5.0", 42 | "eslint-plugin-import": "^2.26.0", 43 | "eslint-plugin-prettier": "^4.0.0", 44 | "jest": "^28.1.0", 45 | "prettier": "^2.6.2", 46 | "webpack": "^5.72.1" 47 | }, 48 | "peerDependencies": { 49 | "next": ">=10", 50 | "workbox-window": ">=5" 51 | }, 52 | "engines": { 53 | "node": ">=14" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const withWorkbox = require("./index"); 2 | 3 | const dir = process.cwd(); 4 | 5 | describe("workbox", () => { 6 | it("can use WorkboxPlugin.InjectManifest", () => { 7 | const options = { 8 | workbox: { 9 | additionalManifestEntries: [ 10 | { url: "/offline", revision: "fake-revision" }, 11 | ], 12 | swSrc: "worker.js", 13 | }, 14 | }; 15 | const nextConfig = withWorkbox(options); 16 | 17 | const webpackConfig = nextConfig.webpack( 18 | { output: { publicPath: undefined }, plugins: [] }, 19 | { dev: false, dir, isServer: false, config: options } 20 | ); 21 | 22 | expect(nextConfig).toEqual({ 23 | webpack: expect.any(Function), 24 | workbox: { 25 | additionalManifestEntries: [ 26 | { revision: "fake-revision", url: "/offline" }, 27 | ], 28 | swSrc: "worker.js", 29 | }, 30 | }); 31 | 32 | expect(webpackConfig).toEqual({ 33 | output: { publicPath: undefined }, 34 | plugins: [ 35 | { 36 | apply: expect.any(Function), 37 | cleanAfterEveryBuildPatterns: [], 38 | cleanOnceBeforeBuildPatterns: [ 39 | `${dir}/public/sw.js`, 40 | `${dir}/public/sw.js.map`, 41 | ], 42 | cleanStaleWebpackAssets: true, 43 | currentAssets: [], 44 | dangerouslyAllowCleanPatternsOutsideProject: false, 45 | dry: false, 46 | handleDone: expect.any(Function), 47 | handleInitial: expect.any(Function), 48 | initialClean: false, 49 | outputPath: "", 50 | protectWebpackAssets: true, 51 | removeFiles: expect.any(Function), 52 | verbose: false, 53 | }, 54 | { 55 | alreadyCalled: false, 56 | config: { 57 | additionalManifestEntries: [ 58 | { revision: "fake-revision", url: "/offline" }, 59 | ], 60 | dontCacheBustURLsMatching: /^\/_next\/static\/.*/iu, 61 | exclude: [ 62 | /^build-manifest\.json$/i, 63 | /^react-loadable-manifest\.json$/i, 64 | /\/_error\.js$/i, 65 | /\.js\.map$/i, 66 | ], 67 | modifyURLPrefix: { "static/": "/_next/static/" }, 68 | swDest: `${dir}/public/sw.js`, 69 | swSrc: `${dir}/worker.js`, 70 | }, 71 | }, 72 | ], 73 | }); 74 | }); 75 | 76 | it("can use WorkboxPlugin.GenerateSW", () => { 77 | const options = { 78 | workbox: { 79 | additionalManifestEntries: [ 80 | { url: "/offline", revision: "fake-revision" }, 81 | ], 82 | }, 83 | }; 84 | const nextConfig = withWorkbox(options); 85 | 86 | const webpackConfig = nextConfig.webpack( 87 | { output: { publicPath: undefined }, plugins: [] }, 88 | { dev: false, dir, isServer: false, config: options } 89 | ); 90 | 91 | expect(nextConfig).toEqual({ 92 | webpack: expect.any(Function), 93 | workbox: { 94 | additionalManifestEntries: [ 95 | { revision: "fake-revision", url: "/offline" }, 96 | ], 97 | }, 98 | }); 99 | 100 | expect(webpackConfig).toEqual({ 101 | output: { publicPath: undefined }, 102 | plugins: [ 103 | { 104 | apply: expect.any(Function), 105 | cleanAfterEveryBuildPatterns: [], 106 | cleanOnceBeforeBuildPatterns: [ 107 | `${dir}/public/sw.js`, 108 | `${dir}/public/sw.js.map`, 109 | ], 110 | cleanStaleWebpackAssets: true, 111 | currentAssets: [], 112 | dangerouslyAllowCleanPatternsOutsideProject: false, 113 | dry: false, 114 | handleDone: expect.any(Function), 115 | handleInitial: expect.any(Function), 116 | initialClean: false, 117 | outputPath: "", 118 | protectWebpackAssets: true, 119 | removeFiles: expect.any(Function), 120 | verbose: false, 121 | }, 122 | { 123 | alreadyCalled: false, 124 | config: { 125 | additionalManifestEntries: [ 126 | { revision: "fake-revision", url: "/offline" }, 127 | ], 128 | dontCacheBustURLsMatching: /^\/_next\/static\/.*/iu, 129 | exclude: [ 130 | /^build-manifest\.json$/i, 131 | /^react-loadable-manifest\.json$/i, 132 | /\/_error\.js$/i, 133 | /\.js\.map$/i, 134 | ], 135 | modifyURLPrefix: { "static/": "/_next/static/" }, 136 | swDest: `${dir}/public/sw.js`, 137 | }, 138 | }, 139 | ], 140 | }); 141 | }); 142 | }); 143 | --------------------------------------------------------------------------------