feat: frontend PWA scaffolding with React and Tailwind

This commit is contained in:
Daniel Bedeleanu
2026-04-10 14:05:50 +03:00
parent 373652e8b8
commit 7f8b24aabb
33622 changed files with 4317382 additions and 0 deletions

2
frontend/node_modules/next-pwa/.gitpod.yml generated vendored Normal file
View File

@@ -0,0 +1,2 @@
tasks:
- init: yarn install

21
frontend/node_modules/next-pwa/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 ShadowWalker w@weiw.io https://weiw.io
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

337
frontend/node_modules/next-pwa/README.md generated vendored Normal file
View File

@@ -0,0 +1,337 @@
# Zero Config [PWA](https://web.dev/learn/pwa/) Plugin for [Next.js](https://nextjs.org/)
This plugin is powered by [workbox](https://developer.chrome.com/docs/workbox/) and other good stuff.
![size](https://img.shields.io/bundlephobia/minzip/next-pwa.svg) ![dependencies](https://img.shields.io/librariesio/release/npm/next-pwa) ![downloads](https://img.shields.io/npm/dw/next-pwa.svg) ![license](https://img.shields.io/npm/l/next-pwa.svg)
👋 Share your awesome PWA project 👉 [here](https://github.com/shadowwalker/next-pwa/discussions/206)
**Features**
- 0⃣ Zero config for registering and generating service worker
- ✨ Optimized precache and runtime cache
- 💯 Maximize lighthouse score
- 🎈 Easy to understand examples
- 📴 Completely offline support with fallbacks [example](https://github.com/shadowwalker/next-pwa/tree/master/examples/offline-fallback-v2) 🆕
- 📦 Use [workbox](https://developer.chrome.com/docs/workbox/) and [workbox-window](https://developer.chrome.com/docs/workbox/modules/workbox-window) v6
- 🍪 Work with cookies out of the box
- 🔉 Default range requests for audios and videos
- ☕ No custom server needed for Next.js 9+ [example](https://github.com/shadowwalker/next-pwa/tree/master/examples/next-9)
- 🔧 Handle PWA lifecycle events opt-in [example](https://github.com/shadowwalker/next-pwa/tree/master/examples/lifecycle)
- 📐 Custom worker to run extra code with code splitting and **typescript** support [example](https://github.com/shadowwalker/next-pwa/tree/master/examples/custom-ts-worker)
- 📜 [Public environment variables](https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser) available in custom worker as usual
- 🐞 Debug service worker with confidence in development mode without caching
- 🌏 Internationalization (a.k.a I18N) with `next-i18next` [example](https://github.com/shadowwalker/next-pwa/tree/master/examples/next-i18next)
- 🛠 Configurable by the same [workbox configuration options](https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin) for [GenerateSW](https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin/#generatesw-plugin) and [InjectManifest](https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin/#injectmanifest-plugin)
- 🚀 Spin up a [GitPod](https://gitpod.io/#https://github.com/shadowwalker/next-pwa/) and try out examples in rocket speed
- ⚡ Support [blitz.js](https://blitzjs.com/) (simply add to `blitz.config.js`)
- 🔩 (Experimental) precaching `.module.js` when `next.config.js` has `experimental.modern` set to `true`
> **NOTE 1** - `next-pwa` version 2.0.0+ should only work with `next.js` 9.1+, and static files should only be served through `public` directory. This will make things simpler.
>
> **NOTE 2** - If you encounter error `TypeError: Cannot read property **'javascript' of undefined**` during build, [please consider upgrade to webpack5 in `next.config.js`](https://github.com/shadowwalker/next-pwa/issues/198#issuecomment-817205700).
---
[![Open in Gitpod](https://img.shields.io/badge/Open%20In-Gitpod.io-%231966D2?style=for-the-badge&logo=gitpod)](https://gitpod.io/#https://github.com/shadowwalker/next-pwa/)
## Install
> If you are new to `next.js` or `react.js` at all, you may want to first checkout [learn next.js](https://nextjs.org/learn/basics/create-nextjs-app) or [next.js document](https://nextjs.org/docs/getting-started). Then start from [a simple example](https://github.com/shadowwalker/next-pwa/tree/master/examples/next-9) or [progressive-web-app example in next.js repository](https://github.com/vercel/next.js/tree/canary/examples/progressive-web-app).
```bash
yarn add next-pwa
```
## Basic Usage
### Step 1: withPWA
Update or create `next.config.js` with
```javascript
const withPWA = require('next-pwa')({
dest: 'public'
})
module.exports = withPWA({
// next.js config
})
```
After running `next build`, this will generate two files in your `public`: `workbox-*.js` and `sw.js`, which will automatically be served statically.
If you are using Next.js version 9 or newer, then skip the options below and move on to Step 2.
If you are using Next.js older than version 9, you'll need to pick an option below before continuing to Step 2.
### Option 1: Host Static Files
Copy files to your static file hosting server, so that they are accessible from the following paths: `https://yourdomain.com/sw.js` and `https://yourdomain.com/workbox-*.js`.
One example is using Firebase hosting service to host those files statically. You can automate the copy step using scripts in your deployment workflow.
> For security reasons, you must host these files directly from your domain. If the content is delivered using a redirect, the browser will refuse to run the service worker.
### Option 2: Use Custom Server
When an HTTP request is received, test if those files are requested, then return those static files.
Example `server.js`
```javascript
const { createServer } = require('http')
const { join } = require('path')
const { parse } = require('url')
const next = require('next')
const app = next({ dev: process.env.NODE_ENV !== 'production' })
const handle = app.getRequestHandler()
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true)
const { pathname } = parsedUrl
if (pathname === '/sw.js' || /^\/(workbox|worker|fallback)-\w+\.js$/.test(pathname)) {
const filePath = join(__dirname, '.next', pathname)
app.serveStatic(req, res, filePath)
} else {
handle(req, res, parsedUrl)
}
}).listen(3000, () => {
console.log(`> Ready on http://localhost:${3000}`)
})
})
```
> The following setup has nothing to do with `next-pwa` plugin, and you probably have already set them up. If not, go ahead and set them up.
### Step 2: Add Manifest File (Example)
Create a `manifest.json` file in your `public` folder:
```json
{
"name": "PWA App",
"short_name": "App",
"icons": [
{
"src": "/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#FFFFFF",
"background_color": "#FFFFFF",
"start_url": "/",
"display": "standalone",
"orientation": "portrait"
}
```
### Step 3: Add Head Meta (Example)
Add the following into `_document.jsx` or `_app.tsx`, in `<Head>`:
```html
<meta name="application-name" content="PWA App" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="PWA App" />
<meta name="description" content="Best PWA App in the world" />
<meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="msapplication-config" content="/icons/browserconfig.xml" />
<meta name="msapplication-TileColor" content="#2B5797" />
<meta name="msapplication-tap-highlight" content="no" />
<meta name="theme-color" content="#000000" />
<link rel="apple-touch-icon" href="/icons/touch-icon-iphone.png" />
<link rel="apple-touch-icon" sizes="152x152" href="/icons/touch-icon-ipad.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/touch-icon-iphone-retina.png" />
<link rel="apple-touch-icon" sizes="167x167" href="/icons/touch-icon-ipad-retina.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
<link rel="manifest" href="/manifest.json" />
<link rel="mask-icon" href="/icons/safari-pinned-tab.svg" color="#5bbad5" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:url" content="https://yourdomain.com" />
<meta name="twitter:title" content="PWA App" />
<meta name="twitter:description" content="Best PWA App in the world" />
<meta name="twitter:image" content="https://yourdomain.com/icons/android-chrome-192x192.png" />
<meta name="twitter:creator" content="@DavidWShadow" />
<meta property="og:type" content="website" />
<meta property="og:title" content="PWA App" />
<meta property="og:description" content="Best PWA App in the world" />
<meta property="og:site_name" content="PWA App" />
<meta property="og:url" content="https://yourdomain.com" />
<meta property="og:image" content="https://yourdomain.com/icons/apple-touch-icon.png" />
<!-- apple splash screen images -->
<!--
<link rel='apple-touch-startup-image' href='/images/apple_splash_2048.png' sizes='2048x2732' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_1668.png' sizes='1668x2224' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_1536.png' sizes='1536x2048' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_1125.png' sizes='1125x2436' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_1242.png' sizes='1242x2208' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_750.png' sizes='750x1334' />
<link rel='apple-touch-startup-image' href='/images/apple_splash_640.png' sizes='640x1136' />
-->
```
> Tip: Put the `viewport` head meta tag into `_app.js` rather than in `_document.js` if you need it.
```typescript
<meta
name='viewport'
content='minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover'
/>
```
## Offline Fallbacks
Offline fallbacks are useful when the fetch failed from both cache and network, a precached resource is served instead of present an error from browser.
To get started simply add a `/_offline` page such as `pages/_offline.js` or `pages/_offline.jsx` or `pages/_offline.ts` or `pages/_offline.tsx`. Then you are all set! When the user is offline, all pages which are not cached will fallback to '/\_offline'.
**[Use this example to see it in action](https://github.com/shadowwalker/next-pwa/tree/master/examples/offline-fallback-v2)**
`next-pwa` helps you precache those resources on the first load, then inject a fallback handler to `handlerDidError` plugin to all `runtimeCaching` configs, so that precached resources are served when fetch failed.
You can also setup `precacheFallback.fallbackURL` in your [runtimeCaching config entry](https://developer.chrome.com/docs/workbox/reference/workbox-build/#type-RuntimeCaching) to implement similar functionality. The difference is that above method is based on the resource type, this method is based matched url pattern. If this config is set in the runtimeCaching config entry, resource type based fallback will be disabled automatically for this particular url pattern to avoid conflict.
## Configuration
There are options you can use to customize the behavior of this plugin by adding `pwa` object in the next config in `next.config.js`:
```javascript
const withPWA = require('next-pwa')({
dest: 'public'
// disable: process.env.NODE_ENV === 'development',
// register: true,
// scope: '/app',
// sw: 'service-worker.js',
//...
})
module.exports = withPWA({
// next.js config
})
```
### Available Options
- disable: boolean - whether to disable pwa feature as a whole
- default: `false`
- set `disable: false`, so that it will generate service worker in both `dev` and `prod`
- set `disable: true` to completely disable PWA
- if you don't need to debug service worker in `dev`, you can set `disable: process.env.NODE_ENV === 'development'`
- register: boolean - whether to let this plugin register service worker for you
- default to `true`
- set to `false` when you want to handle register service worker yourself, this could be done in `componentDidMount` of your root app. you can consider the [register.js](https://github.com/shadowwalker/next-pwa/blob/master/register.js) as an example.
- scope: string - url scope for pwa
- default: [`basePath`](https://nextjs.org/docs/api-reference/next.config.js/basepath) in `next.config.js` or `/`
- set to `/app` so that path under `/app` will be PWA while others are not
- sw: string - service worker script file name
- default: `/sw.js`
- set to another file name if you want to customize the output file name
- runtimeCaching - caching strategies (array or callback function)
- default: see the **Runtime Caching** section for the default configuration
- accepts an array of cache entry objects, [please follow the structure here](https://developer.chrome.com/docs/workbox/reference/workbox-build/#type-RuntimeCaching)
- Note: the order of the array matters. The first rule that matches is effective. Therefore, please **ALWAYS** put rules with larger scope behind the rules with a smaller and specific scope.
- publicExcludes - an array of glob pattern strings to exclude files in the `public` folder from being precached.
- default: `['!noprecache/**/*']` - this means that the default behavior will precache all the files inside your `public` folder but files inside `/public/noprecache` folder. You can simply put files inside that folder to not precache them without config this.
- example: `['!img/super-large-image.jpg', '!fonts/not-used-fonts.otf']`
- buildExcludes - an array of extra pattern or function to exclude files from being precached in `.next/static` (or your custom build) folder
- default: `[]`
- example: `[/chunks\/images\/.*$/]` - Don't precache files under `.next/static/chunks/images` (Highly recommend this to work with `next-optimized-images` plugin)
- doc: Array of (string, RegExp, or function()). One or more specifiers used to exclude assets from the precache manifest. This is interpreted following the same rules as Webpack's standard exclude option.
- cacheStartUrl - whether to cache start url
- default: `true`
- [discussion of use case to not cache start url at all](https://github.com/shadowwalker/next-pwa/pull/296#issuecomment-1094167025)
- dynamicStartUrl - if your start url returns different HTML document under different state (such as logged in vs. not logged in), this should be set to true.
- default: `true`
- effective when `cacheStartUrl` set to `true`
- recommend: set to **false** if your start url always returns same HTML document, then start url will be precached, this will help to speed up first load.
- dynamicStartUrlRedirect - if your start url redirect to another route such as `/login`, it's recommended to setup this redirected url for the best user experience.
- default: `undefined`
- effective when `dynamicStartUrlRedirect` set to `true`
- fallbacks - config precached routes to fallback when both cache and network not available to serve resources.
- **if you just need a offline fallback page, simply create a `/_offline` page such as `pages/_offline.js` and you are all set, no configuration necessary**
- default: `object`
- `fallbacks.document` - fallback route for document (page), default to `/_offline` if you created that page
- `fallbacks.image` - fallback route for image, default to none
- `fallbacks.audio` - fallback route for audio, default to none
- `fallbacks.video` - fallback route for video, default to none
- `fallbacks.font` - fallback route for font, default to none
- cacheOnFrontEndNav - enable additional route cache when navigate between pages with `next/link` on front end. Checkout this [example](https://github.com/shadowwalker/next-pwa/tree/master/examples/cache-on-front-end-nav) for some context about why this is implemented.
- default: `false`
- note: this improve user experience on special use cases but it also adds some overhead because additional network call, I suggest you consider this as a trade off.
- ~~subdomainPrefix: string - url prefix to allow hosting static files on a subdomain~~
- ~~default: `""` - i.e. default with no prefix~~
- ~~example: `/subdomain` if the app is hosted on `example.com/subdomain`~~
- deprecated, use [basePath](https://nextjs.org/docs/api-reference/next.config.js/basepath) instead
- reloadOnOnline - changes the behaviour of the app when the device detects that it has gone back "online" and has a network connection. Indicate if the app should call `location.reload()` to refresh the app.
- default: `true`
- customWorkerDir - customize the directory where `next-pwa` looks for a custom worker implementation to add to the service worker generated by workbox. For more information, check out the [custom worker example](https://github.com/shadowwalker/next-pwa/tree/master/examples/custom-ts-worker).
- default: `worker`
### Other Options
`next-pwa` uses `workbox-webpack-plugin`, other options which could also be put in `pwa` object can be found [**ON THE DOCUMENTATION**](https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin) for [GenerateSW](https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin/#generatesw-plugin) and [InjectManifest](https://developer.chrome.com/docs/workbox/modules/workbox-webpack-plugin/#injectmanifest-plugin). If you specify `swSrc`, `InjectManifest` plugin will be used, otherwise `GenerateSW` will be used to generate service worker.
### Runtime Caching
`next-pwa` uses a default runtime [cache.js](https://github.com/shadowwalker/next-pwa/blob/master/cache.js)
There is a great chance you may want to customize your own runtime caching rules. Please feel free to copy the default `cache.js` file and customize the rules as you like. Don't forget to inject the configurations into your `pwa` config in `next.config.js`.
Here is the [document on how to write runtime caching configurations](https://developer.chrome.com/docs/workbox/reference/workbox-build/#type-RuntimeCaching), including background sync and broadcast update features and more!
## Tips
1. [Common UX pattern to ask user to reload when new service worker is installed](https://github.com/shadowwalker/next-pwa/blob/master/examples/lifecycle/pages/index.js#L26-L38)
2. Use a convention like `{command: 'doSomething', message: ''}` object when `postMessage` to service worker. So that on the listener, it could do multiple different tasks using `if...else...`.
3. When you are debugging service worker, constantly `clean application cache` to reduce some flaky errors.
4. If you are redirecting the user to another route, please note [workbox by default only cache response with 200 HTTP status](https://developer.chrome.com/docs/workbox/modules/workbox-cacheable-response#what_are_the_defaults), if you really want to cache redirected page for the route, you can specify it in `runtimeCaching` such as `options.cacheableResponse.statuses=[200,302]`.
5. When debugging issues, you may want to format your generated `sw.js` file to figure out what's really going on.
6. Force `next-pwa` to generate worker box production build by specify the option `mode: 'production'` in your `pwa` section of `next.config.js`. Though `next-pwa` automatically generate the worker box development build during development (by running `next`) and worker box production build during production (by running `next build` and `next start`). You may still want to force it to production build even during development of your web app for following reason:
1. Reduce logging noise due to production build doesn't include logging.
2. Improve performance a bit due to production build is optimized and minified.
7. If you just want to disable worker box logging while keeping development build during development, [simply put `self.__WB_DISABLE_DEV_LOGS = true` in your `worker/index.js` (create one if you don't have one)](https://github.com/shadowwalker/next-pwa/blob/c48ef110360d0138ad2dacd82ab96964e3da2daf/examples/custom-worker/worker/index.js#L6).
8. It is common developers have to use `userAgent` string to determine if users are using Safari/iOS/MacOS or some other platform, [ua-parser-js](https://www.npmjs.com/package/ua-parser-js) library is a good friend for that purpose.
## Reference
1. [Google Workbox](https://developer.chrome.com/docs/workbox/what-is-workbox/)
2. [ServiceWorker, MessageChannel, & postMessage](https://ponyfoo.com/articles/serviceworker-messagechannel-postmessage) by [Nicolás Bevacqua](https://ponyfoo.com/contributors/ponyfoo)
3. [The Service Worker Lifecycle](https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle)
4. [6 Tips to make your iOS PWA feel like a native app](https://www.netguru.com/codestories/pwa-ios)
5. [Make Your PWA Available on Google Play Store](https://www.netguru.com/codestories/make-your-pwa-available-on-google-play-store)
## Fun PWA Projects
1. [Experience SAMSUNG on an iPhone - must open on an iPhone to start](https://itest.nz/)
2. [App Scope - like an app store for PWA](https://appsco.pe/)
3. [PWA Directory](https://pwa-directory.appspot.com/)
4. [PWA Builder - Alternative way to build awesome PWA](https://www.pwabuilder.com/)
## License
MIT

120
frontend/node_modules/next-pwa/build-custom-worker.js generated vendored Normal file
View File

@@ -0,0 +1,120 @@
'use strict'
const path = require('path')
const fs = require('fs')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const buildCustomWorker = ({ id, basedir, customWorkerDir, destdir, plugins, minify }) => {
let workerDir = undefined
if (fs.existsSync(path.join(basedir, customWorkerDir))) {
workerDir = path.join(basedir, customWorkerDir)
} else if (fs.existsSync(path.join(basedir, 'src', customWorkerDir))) {
workerDir = path.join(basedir, 'src', customWorkerDir)
}
if (!workerDir) return
const name = `worker-${id}.js`
const customWorkerEntries = ['ts', 'js']
.map(ext => path.join(workerDir, `index.${ext}`))
.filter(entry => fs.existsSync(entry))
if (customWorkerEntries.length === 0) return
if (customWorkerEntries.length > 1) {
console.warn(
`> [PWA] WARNING: More than one custom worker found (${customWorkerEntries.join(
','
)}), not building a custom worker`
)
return
}
const customWorkerEntry = customWorkerEntries[0]
console.log(`> [PWA] Custom worker found: ${customWorkerEntry}`)
console.log(`> [PWA] Build custom worker: ${path.join(destdir, name)}`)
webpack({
mode: 'none',
target: 'webworker',
entry: {
main: customWorkerEntry
},
resolve: {
extensions: ['.ts', '.js'],
fallback: {
module: false,
dgram: false,
dns: false,
path: false,
fs: false,
os: false,
crypto: false,
stream: false,
http2: false,
net: false,
tls: false,
zlib: false,
child_process: false
}
},
module: {
rules: [
{
test: /\.(t|j)s$/i,
use: [
{
loader: 'babel-loader',
options: {
presets: [
[
'next/babel',
{
'transform-runtime': {
corejs: false,
helpers: true,
regenerator: false,
useESModules: true
},
'preset-env': {
modules: false,
targets: 'chrome >= 56'
}
}
]
]
}
}
]
}
]
},
output: {
path: destdir,
filename: name
},
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [path.join(destdir, 'worker-*.js'), path.join(destdir, 'worker-*.js.map')]
})
].concat(plugins),
optimization: minify
? {
minimize: true,
minimizer: [new TerserPlugin()]
}
: undefined
}).run((error, status) => {
if (error || status.hasErrors()) {
console.error(`> [PWA] Failed to build custom worker`)
console.error(status.toString({ colors: true }))
process.exit(-1)
}
})
return name
}
module.exports = buildCustomWorker

146
frontend/node_modules/next-pwa/build-fallback-worker.js generated vendored Normal file
View File

@@ -0,0 +1,146 @@
'use strict'
const path = require('path')
const fs = require('fs')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const getFallbackEnvs = ({ fallbacks, basedir, id, pageExtensions }) => {
let { document, data } = fallbacks
if (!document) {
let pagesDir = undefined
if (fs.existsSync(path.join(basedir, 'pages'))) {
pagesDir = path.join(basedir, 'pages')
} else if (fs.existsSync(path.join(basedir, 'src', 'pages'))) {
pagesDir = path.join(basedir, 'src', 'pages')
}
if (!pagesDir) return
const offlines = pageExtensions
.map(ext => path.join(pagesDir, `_offline.${ext}`))
.filter(entry => fs.existsSync(entry))
if (offlines.length === 1) {
document = '/_offline'
}
}
if (data && data.endsWith('.json')) {
data = path.posix.join('/_next/data', id, data)
}
const envs = {
__PWA_FALLBACK_DOCUMENT__: document || false,
__PWA_FALLBACK_IMAGE__: fallbacks.image || false,
__PWA_FALLBACK_AUDIO__: fallbacks.audio || false,
__PWA_FALLBACK_VIDEO__: fallbacks.video || false,
__PWA_FALLBACK_FONT__: fallbacks.font || false,
__PWA_FALLBACK_DATA__: data || false
}
if (Object.values(envs).filter(v => !!v).length === 0) return
console.log('> [PWA] Fallback to precache routes when fetch failed from cache or network:')
if (envs.__PWA_FALLBACK_DOCUMENT__) console.log(`> [PWA] document (page): ${envs.__PWA_FALLBACK_DOCUMENT__}`)
if (envs.__PWA_FALLBACK_IMAGE__) console.log(`> [PWA] image: ${envs.__PWA_FALLBACK_IMAGE__}`)
if (envs.__PWA_FALLBACK_AUDIO__) console.log(`> [PWA] audio: ${envs.__PWA_FALLBACK_AUDIO__}`)
if (envs.__PWA_FALLBACK_VIDEO__) console.log(`> [PWA] video: ${envs.__PWA_FALLBACK_VIDEO__}`)
if (envs.__PWA_FALLBACK_FONT__) console.log(`> [PWA] font: ${envs.__PWA_FALLBACK_FONT__}`)
if (envs.__PWA_FALLBACK_DATA__) console.log(`> [PWA] data (/_next/data/**/*.json): ${envs.__PWA_FALLBACK_DATA__}`)
return envs
}
const buildFallbackWorker = ({ id, fallbacks, basedir, destdir, minify, pageExtensions }) => {
const envs = getFallbackEnvs({ fallbacks, basedir, id, pageExtensions })
if (!envs) return
const name = `fallback-${id}.js`
const fallbackJs = path.join(__dirname, `fallback.js`)
webpack({
mode: 'none',
target: 'webworker',
entry: {
main: fallbackJs
},
resolve: {
extensions: ['.js'],
fallback: {
module: false,
dgram: false,
dns: false,
path: false,
fs: false,
os: false,
crypto: false,
stream: false,
http2: false,
net: false,
tls: false,
zlib: false,
child_process: false
}
},
module: {
rules: [
{
test: /\.js$/i,
use: [
{
loader: 'babel-loader',
options: {
presets: [
[
'next/babel',
{
'transform-runtime': {
corejs: false,
helpers: true,
regenerator: false,
useESModules: true
},
'preset-env': {
modules: false,
targets: 'chrome >= 56'
}
}
]
]
}
}
]
}
]
},
output: {
path: destdir,
filename: name
},
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [path.join(destdir, 'fallback-*.js'), path.join(destdir, 'fallback-*.js.map')]
}),
new webpack.EnvironmentPlugin(envs)
],
optimization: minify
? {
minimize: true,
minimizer: [new TerserPlugin()]
}
: undefined
}).run((error, status) => {
if (error || status.hasErrors()) {
console.error(`> [PWA] Failed to build fallback worker`)
console.error(status.toString({ colors: true }))
process.exit(-1)
}
})
return { fallbacks, name, precaches: Object.values(envs).filter(v => !!v) }
}
module.exports = buildFallbackWorker

184
frontend/node_modules/next-pwa/cache.js generated vendored Normal file
View File

@@ -0,0 +1,184 @@
'use strict'
// Workbox RuntimeCaching config: https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-build#.RuntimeCachingEntry
module.exports = [
{
urlPattern: /^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-webfonts',
expiration: {
maxEntries: 4,
maxAgeSeconds: 365 * 24 * 60 * 60 // 365 days
}
}
},
{
urlPattern: /^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'google-fonts-stylesheets',
expiration: {
maxEntries: 4,
maxAgeSeconds: 7 * 24 * 60 * 60 // 7 days
}
}
},
{
urlPattern: /\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-font-assets',
expiration: {
maxEntries: 4,
maxAgeSeconds: 7 * 24 * 60 * 60 // 7 days
}
}
},
{
urlPattern: /\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-image-assets',
expiration: {
maxEntries: 64,
maxAgeSeconds: 24 * 60 * 60 // 24 hours
}
}
},
{
urlPattern: /\/_next\/image\?url=.+$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'next-image',
expiration: {
maxEntries: 64,
maxAgeSeconds: 24 * 60 * 60 // 24 hours
}
}
},
{
urlPattern: /\.(?:mp3|wav|ogg)$/i,
handler: 'CacheFirst',
options: {
rangeRequests: true,
cacheName: 'static-audio-assets',
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60 // 24 hours
}
}
},
{
urlPattern: /\.(?:mp4)$/i,
handler: 'CacheFirst',
options: {
rangeRequests: true,
cacheName: 'static-video-assets',
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60 // 24 hours
}
}
},
{
urlPattern: /\.(?:js)$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-js-assets',
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60 // 24 hours
}
}
},
{
urlPattern: /\.(?:css|less)$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'static-style-assets',
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60 // 24 hours
}
}
},
{
urlPattern: /\/_next\/data\/.+\/.+\.json$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'next-data',
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60 // 24 hours
}
}
},
{
urlPattern: /\.(?:json|xml|csv)$/i,
handler: 'NetworkFirst',
options: {
cacheName: 'static-data-assets',
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60 // 24 hours
}
}
},
{
urlPattern: ({ url }) => {
const isSameOrigin = self.origin === url.origin
if (!isSameOrigin) return false
const pathname = url.pathname
// Exclude /api/auth/callback/* to fix OAuth workflow in Safari without impact other environment
// Above route is default for next-auth, you may need to change it if your OAuth workflow has a different callback route
// Issue: https://github.com/shadowwalker/next-pwa/issues/131#issuecomment-821894809
if (pathname.startsWith('/api/auth/')) return false
if (pathname.startsWith('/api/')) return true
return false
},
handler: 'NetworkFirst',
method: 'GET',
options: {
cacheName: 'apis',
expiration: {
maxEntries: 16,
maxAgeSeconds: 24 * 60 * 60 // 24 hours
},
networkTimeoutSeconds: 10 // fall back to cache if api does not response within 10 seconds
}
},
{
urlPattern: ({ url }) => {
const isSameOrigin = self.origin === url.origin
if (!isSameOrigin) return false
const pathname = url.pathname
if (pathname.startsWith('/api/')) return false
return true
},
handler: 'NetworkFirst',
options: {
cacheName: 'others',
expiration: {
maxEntries: 32,
maxAgeSeconds: 24 * 60 * 60 // 24 hours
},
networkTimeoutSeconds: 10
}
},
{
urlPattern: ({ url }) => {
const isSameOrigin = self.origin === url.origin
return !isSameOrigin
},
handler: 'NetworkFirst',
options: {
cacheName: 'cross-origin',
expiration: {
maxEntries: 32,
maxAgeSeconds: 60 * 60 // 1 hour
},
networkTimeoutSeconds: 10
}
}
]

27
frontend/node_modules/next-pwa/fallback.js generated vendored Normal file
View File

@@ -0,0 +1,27 @@
'use strict'
self.fallback = async request => {
// https://developer.mozilla.org/en-US/docs/Web/API/RequestDestination
switch (request.destination) {
case 'document':
if (process.env.__PWA_FALLBACK_DOCUMENT__)
return caches.match(process.env.__PWA_FALLBACK_DOCUMENT__, { ignoreSearch: true })
case 'image':
if (process.env.__PWA_FALLBACK_IMAGE__)
return caches.match(process.env.__PWA_FALLBACK_IMAGE__, { ignoreSearch: true })
case 'audio':
if (process.env.__PWA_FALLBACK_AUDIO__)
return caches.match(process.env.__PWA_FALLBACK_AUDIO__, { ignoreSearch: true })
case 'video':
if (process.env.__PWA_FALLBACK_VIDEO__)
return caches.match(process.env.__PWA_FALLBACK_VIDEO__, { ignoreSearch: true })
case 'font':
if (process.env.__PWA_FALLBACK_FONT__)
return caches.match(process.env.__PWA_FALLBACK_FONT__, { ignoreSearch: true })
case '':
if (process.env.__PWA_FALLBACK_DATA__ && request.url.match(/\/_next\/data\/.+\/.+\.json$/i))
return caches.match(process.env.__PWA_FALLBACK_DATA__, { ignoreSearch: true })
default:
return Response.error()
}
}

341
frontend/node_modules/next-pwa/index.js generated vendored Normal file
View File

@@ -0,0 +1,341 @@
'use strict'
const path = require('path')
const fs = require('fs')
const globby = require('globby')
const crypto = require('crypto')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const WorkboxPlugin = require('workbox-webpack-plugin')
const defaultCache = require('./cache')
const buildCustomWorker = require('./build-custom-worker')
const buildFallbackWorker = require('./build-fallback-worker')
const getRevision = file => crypto.createHash('md5').update(fs.readFileSync(file)).digest('hex')
module.exports =
(pluginOptions = {}) =>
(nextConfig = {}) =>
Object.assign({}, nextConfig, {
webpack(config, options) {
const {
webpack,
buildId,
dev,
config: { distDir = '.next', pageExtensions = ['tsx', 'ts', 'jsx', 'js', 'mdx'], experimental = {} }
} = options
let basePath = options.config.basePath
if (!basePath) basePath = '/'
// For workbox configurations:
// https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-webpack-plugin.GenerateSW
const {
disable = false,
register = true,
dest = distDir,
sw = 'sw.js',
cacheStartUrl = true,
dynamicStartUrl = true,
dynamicStartUrlRedirect,
skipWaiting = true,
clientsClaim = true,
cleanupOutdatedCaches = true,
additionalManifestEntries,
ignoreURLParametersMatching = [],
importScripts = [],
publicExcludes = ['!noprecache/**/*'],
buildExcludes = [],
modifyURLPrefix = {},
manifestTransforms = [],
fallbacks = {},
cacheOnFrontEndNav = false,
reloadOnOnline = true,
scope = basePath,
customWorkerDir = 'worker',
subdomainPrefix, // deprecated, use basePath in next.config.js instead
...workbox
} = pluginOptions
if (typeof nextConfig.webpack === 'function') {
config = nextConfig.webpack(config, options)
}
if (disable) {
options.isServer && console.log('> [PWA] PWA support is disabled')
return config
}
if (subdomainPrefix) {
console.error(
'> [PWA] subdomainPrefix is deprecated, use basePath in next.config.js instead: https://nextjs.org/docs/api-reference/next.config.js/basepath'
)
}
console.log(`> [PWA] Compile ${options.isServer ? 'server' : 'client (static)'}`)
let { runtimeCaching = defaultCache } = pluginOptions
const _scope = path.posix.join(scope, '/')
// inject register script to main.js
const _sw = path.posix.join(basePath, sw.startsWith('/') ? sw : `/${sw}`)
config.plugins.push(
new webpack.DefinePlugin({
__PWA_SW__: `'${_sw}'`,
__PWA_SCOPE__: `'${_scope}'`,
__PWA_ENABLE_REGISTER__: `${Boolean(register)}`,
__PWA_START_URL__: dynamicStartUrl ? `'${basePath}'` : undefined,
__PWA_CACHE_ON_FRONT_END_NAV__: `${Boolean(cacheOnFrontEndNav)}`,
__PWA_RELOAD_ON_ONLINE__: `${Boolean(reloadOnOnline)}`
})
)
const registerJs = path.join(__dirname, 'register.js')
const entry = config.entry
config.entry = () =>
entry().then(entries => {
if (entries['main.js'] && !entries['main.js'].includes(registerJs)) {
entries['main.js'].unshift(registerJs)
}
return entries
})
if (!options.isServer) {
const _dest = path.join(options.dir, dest)
const customWorkerScriptName = buildCustomWorker({
id: buildId,
basedir: options.dir,
customWorkerDir,
destdir: _dest,
plugins: config.plugins.filter(plugin => plugin instanceof webpack.DefinePlugin),
minify: !dev
})
if (!!customWorkerScriptName) {
importScripts.unshift(customWorkerScriptName)
}
if (register) {
console.log(`> [PWA] Auto register service worker with: ${path.resolve(registerJs)}`)
} else {
console.log(
`> [PWA] Auto register service worker is disabled, please call following code in componentDidMount callback or useEffect hook`
)
console.log(`> [PWA] window.workbox.register()`)
}
console.log(`> [PWA] Service worker: ${path.join(_dest, sw)}`)
console.log(`> [PWA] url: ${_sw}`)
console.log(`> [PWA] scope: ${_scope}`)
config.plugins.push(
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [
path.join(_dest, 'workbox-*.js'),
path.join(_dest, 'workbox-*.js.map'),
path.join(_dest, sw),
path.join(_dest, `${sw}.map`)
]
})
)
// precache files in public folder
let manifestEntries = additionalManifestEntries
if (!Array.isArray(manifestEntries)) {
manifestEntries = globby
.sync(
[
'**/*',
'!workbox-*.js',
'!workbox-*.js.map',
'!worker-*.js',
'!worker-*.js.map',
'!fallback-*.js',
'!fallback-*.js.map',
`!${sw.replace(/^\/+/, '')}`,
`!${sw.replace(/^\/+/, '')}.map`,
...publicExcludes
],
{
cwd: 'public'
}
)
.map(f => ({
url: path.posix.join(basePath, `/${f}`),
revision: getRevision(`public/${f}`)
}))
}
if (cacheStartUrl) {
if (!dynamicStartUrl) {
manifestEntries.push({
url: basePath,
revision: buildId
})
} else if (typeof dynamicStartUrlRedirect === 'string' && dynamicStartUrlRedirect.length > 0) {
manifestEntries.push({
url: dynamicStartUrlRedirect,
revision: buildId
})
}
}
let _fallbacks = fallbacks
if (_fallbacks) {
const res = buildFallbackWorker({
id: buildId,
fallbacks,
basedir: options.dir,
destdir: _dest,
minify: !dev,
pageExtensions
})
if (res) {
_fallbacks = res.fallbacks
importScripts.unshift(res.name)
res.precaches.forEach(route => {
if (!manifestEntries.find(entry => entry.url.startsWith(route))) {
manifestEntries.push({
url: route,
revision: buildId
})
}
})
} else {
_fallbacks = undefined
}
}
const workboxCommon = {
swDest: path.join(_dest, sw),
additionalManifestEntries: dev ? [] : manifestEntries,
exclude: [
...buildExcludes,
({ asset, compilation }) => {
if (
asset.name.startsWith('server/') ||
asset.name.match(/^(build-manifest\.json|react-loadable-manifest\.json)$/)
) {
return true
}
if (dev && !asset.name.startsWith('static/runtime/')) {
return true
}
if (experimental.modern /* modern */) {
if (asset.name.endsWith('.module.js')) {
return false
}
if (asset.name.endsWith('.js')) {
return true
}
}
return false
}
],
modifyURLPrefix: {
...modifyURLPrefix,
'/_next/../public/': '/'
},
manifestTransforms: [
...manifestTransforms,
async (manifestEntries, compilation) => {
const manifest = manifestEntries.map(m => {
m.url = m.url.replace('/_next//static/image', '/_next/static/image')
m.url = m.url.replace('/_next//static/media', '/_next/static/media')
if (m.revision === null) {
let key = m.url
if (key.startsWith(config.output.publicPath)) {
key = m.url.substring(config.output.publicPath.length)
}
const assset = compilation.assetsInfo.get(key)
m.revision = assset ? assset.contenthash || buildId : buildId
}
m.url = m.url.replace(/\[/g, '%5B').replace(/\]/g, '%5D')
return m
})
return { manifest, warnings: [] }
}
]
}
if (workbox.swSrc) {
const swSrc = path.join(options.dir, workbox.swSrc)
console.log(`> [PWA] Inject manifest in ${swSrc}`)
config.plugins.push(
new WorkboxPlugin.InjectManifest({
...workboxCommon,
...workbox,
swSrc
})
)
} else {
if (dev) {
console.log(
'> [PWA] Build in develop mode, cache and precache are mostly disabled. This means offline support is disabled, but you can continue developing other functions in service worker.'
)
ignoreURLParametersMatching.push(/ts/)
runtimeCaching = [
{
urlPattern: /.*/i,
handler: 'NetworkOnly',
options: {
cacheName: 'dev'
}
}
]
}
if (dynamicStartUrl) {
runtimeCaching.unshift({
urlPattern: basePath,
handler: 'NetworkFirst',
options: {
cacheName: 'start-url',
plugins: [
{
cacheWillUpdate: async ({ request, response, event, state }) => {
if (response && response.type === 'opaqueredirect') {
return new Response(response.body, {
status: 200,
statusText: 'OK',
headers: response.headers
})
}
return response
}
}
]
}
})
}
if (_fallbacks) {
runtimeCaching.forEach(c => {
if (c.options.precacheFallback) return
if (Array.isArray(c.options.plugins) && c.options.plugins.find(p => 'handlerDidError' in p)) return
if (!c.options.plugins) c.options.plugins = []
c.options.plugins.push({
handlerDidError: async ({ request }) => self.fallback(request)
})
})
}
config.plugins.push(
new WorkboxPlugin.GenerateSW({
...workboxCommon,
skipWaiting,
clientsClaim,
cleanupOutdatedCaches,
ignoreURLParametersMatching,
importScripts,
...workbox,
runtimeCaching
})
)
}
}
return config
}
})

34
frontend/node_modules/next-pwa/package.json generated vendored Normal file
View File

@@ -0,0 +1,34 @@
{
"name": "next-pwa",
"version": "5.6.0",
"description": "Next.js with PWA, powered by workbox.",
"main": "index.js",
"repository": "https://github.com/shadowwalker/next-pwa",
"author": "ShadowWalker <w@weiw.io>",
"license": "MIT",
"private": false,
"keywords": [
"nextjs",
"pwa",
"workbox",
"web",
"service-worker"
],
"dependencies": {
"babel-loader": "^8.2.5",
"clean-webpack-plugin": "^4.0.0",
"globby": "^11.0.4",
"terser-webpack-plugin": "^5.3.3",
"workbox-webpack-plugin": "^6.5.4",
"workbox-window": "^6.5.4"
},
"devDependencies": {
"webpack": "^5.74.0"
},
"peerDependencies": {
"next": ">=9.0.0"
},
"resolutions": {
"@typescript-eslint/eslint-plugin": "5.18.0"
}
}

84
frontend/node_modules/next-pwa/register.js generated vendored Normal file
View File

@@ -0,0 +1,84 @@
import { Workbox } from 'workbox-window'
if (typeof window !== 'undefined' && 'serviceWorker' in navigator && typeof caches !== 'undefined') {
if (__PWA_START_URL__) {
caches.has('start-url').then(function (has) {
if (!has) {
caches.open('start-url').then(c => c.put(__PWA_START_URL__, new Response('', { status: 200 })))
}
})
}
window.workbox = new Workbox(window.location.origin + __PWA_SW__, { scope: __PWA_SCOPE__ })
if (__PWA_START_URL__) {
window.workbox.addEventListener('installed', async ({ isUpdate }) => {
if (!isUpdate) {
const cache = await caches.open('start-url')
const response = await fetch(__PWA_START_URL__)
let _response = response
if (response.redirected) {
_response = new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers })
}
await cache.put(__PWA_START_URL__, _response)
}
})
}
window.workbox.addEventListener('installed', async () => {
const data = window.performance
.getEntriesByType('resource')
.map(e => e.name)
.filter(n => n.startsWith(`${window.location.origin}/_next/data/`) && n.endsWith('.json'))
const cache = await caches.open('next-data')
data.forEach(d => cache.add(d))
})
if (__PWA_ENABLE_REGISTER__) {
window.workbox.register()
}
if (__PWA_CACHE_ON_FRONT_END_NAV__ || __PWA_START_URL__) {
const cacheOnFrontEndNav = function (url) {
if (!window.navigator.onLine) return
if (__PWA_CACHE_ON_FRONT_END_NAV__ && url !== __PWA_START_URL__) {
return caches.open('others').then(cache =>
cache.match(url, { ignoreSearch: true }).then(res => {
if (!res) return cache.add(url)
return Promise.resolve()
})
)
} else if (__PWA_START_URL__ && url === __PWA_START_URL__) {
return fetch(__PWA_START_URL__).then(function (response) {
if (!response.redirected) {
return caches.open('start-url').then(cache => cache.put(__PWA_START_URL__, response))
}
return Promise.resolve()
})
}
}
const pushState = history.pushState
history.pushState = function () {
pushState.apply(history, arguments)
cacheOnFrontEndNav(arguments[2])
}
const replaceState = history.replaceState
history.replaceState = function () {
replaceState.apply(history, arguments)
cacheOnFrontEndNav(arguments[2])
}
window.addEventListener('online', () => {
cacheOnFrontEndNav(window.location.pathname)
})
}
if (__PWA_RELOAD_ON_ONLINE__) {
window.addEventListener('online', () => {
location.reload()
})
}
}