Add PWA support
This commit is contained in:
		
							parent
							
								
									b959371784
								
							
						
					
					
						commit
						f3ccfcbe48
					
				|  | @ -0,0 +1 @@ | ||||||
|  | Fixes service worker (#1634) | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | Handle PWA correctly and provide better cache strategy for album covers (#1721) | ||||||
|  | @ -24,10 +24,22 @@ module.exports = { | ||||||
|     'vue/no-v-html': 'off', // TODO: tackle this properly
 |     'vue/no-v-html': 'off', // TODO: tackle this properly
 | ||||||
|     'vue/no-use-v-if-with-v-for': 'off', |     'vue/no-use-v-if-with-v-for': 'off', | ||||||
| 
 | 
 | ||||||
|     '@typescript-eslint/ban-ts-comment': 'off', |     // NOTE: Handled by typescript
 | ||||||
|     'no-undef': 'off', |     'no-undef': 'off', | ||||||
|  |     'no-unused-vars': 'off', | ||||||
|  | 
 | ||||||
|  |     // TODO (wvffle): Migrate to VUI
 | ||||||
|  |     //                We're using `// @ts-ignore` in jQuery extensions
 | ||||||
|  |     //                and gettext for vue 2
 | ||||||
|  |     '@typescript-eslint/ban-ts-comment': 'off', | ||||||
|  | 
 | ||||||
|     // TODO (wvffle): Enable typescript rules later
 |     // TODO (wvffle): Enable typescript rules later
 | ||||||
|     '@typescript-eslint/no-this-alias': 'off', |     '@typescript-eslint/no-this-alias': 'off', | ||||||
|     '@typescript-eslint/no-empty-function': 'off' |     '@typescript-eslint/no-empty-function': 'off', | ||||||
|  |     '@typescript-eslint/no-unused-vars': 'off', | ||||||
|  | 
 | ||||||
|  |     // TODO (wvffle): Migration to pinia
 | ||||||
|  |     //                Vuex 3 store does not have types defined, hence we use `any`
 | ||||||
|  |     '@typescript-eslint/no-explicit-any': 'off' | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -50,6 +50,7 @@ | ||||||
|     "@babel/core": "7.17.12", |     "@babel/core": "7.17.12", | ||||||
|     "@babel/plugin-transform-runtime": "7.17.12", |     "@babel/plugin-transform-runtime": "7.17.12", | ||||||
|     "@babel/preset-env": "7.16.11", |     "@babel/preset-env": "7.16.11", | ||||||
|  |     "@types/jest": "27.4.1", | ||||||
|     "@types/jquery": "3.5.14", |     "@types/jquery": "3.5.14", | ||||||
|     "@types/lodash-es": "4.17.6", |     "@types/lodash-es": "4.17.6", | ||||||
|     "@typescript-eslint/eslint-plugin": "5.19.0", |     "@typescript-eslint/eslint-plugin": "5.19.0", | ||||||
|  | @ -72,12 +73,18 @@ | ||||||
|     "jest-cli": "27.5.1", |     "jest-cli": "27.5.1", | ||||||
|     "moxios": "0.4.0", |     "moxios": "0.4.0", | ||||||
|     "sinon": "13.0.2", |     "sinon": "13.0.2", | ||||||
|  |     "ts-jest": "27.1.4", | ||||||
|     "typescript": "4.6.3", |     "typescript": "4.6.3", | ||||||
|     "unplugin-vue2-script-setup": "0.10.2", |     "unplugin-vue2-script-setup": "0.10.2", | ||||||
|     "vite": "2.8.6", |     "vite": "2.8.6", | ||||||
|  |     "vite-plugin-pwa": "0.12.0", | ||||||
|     "vite-plugin-vue2": "1.9.3", |     "vite-plugin-vue2": "1.9.3", | ||||||
|     "vue-jest": "3.0.7", |     "vue-jest": "3.0.7", | ||||||
|     "vue-template-compiler": "2.6.14" |     "vue-template-compiler": "2.6.14", | ||||||
|  |     "workbox-core": "6.5.3", | ||||||
|  |     "workbox-precaching": "6.5.3", | ||||||
|  |     "workbox-routing": "6.5.3", | ||||||
|  |     "workbox-strategies": "6.5.3" | ||||||
|   }, |   }, | ||||||
|   "resolutions": { |   "resolutions": { | ||||||
|     "vue-plyr/plyr": "3.6.12" |     "vue-plyr/plyr": "3.6.12" | ||||||
|  | @ -132,14 +139,19 @@ | ||||||
|   ], |   ], | ||||||
|   "jest": { |   "jest": { | ||||||
|     "moduleFileExtensions": [ |     "moduleFileExtensions": [ | ||||||
|  |       "ts", | ||||||
|       "js", |       "js", | ||||||
|       "json", |       "json", | ||||||
|       "vue" |       "vue" | ||||||
|     ], |     ], | ||||||
|     "transform": { |     "transform": { | ||||||
|       ".*\\.(vue)$": "vue-jest", |       ".*\\.(vue)$": "vue-jest", | ||||||
|       "^.+\\.js$": "babel-jest" |       "^.+\\.js$": "babel-jest", | ||||||
|  |       "^.+\\.ts$": "ts-jest" | ||||||
|     }, |     }, | ||||||
|  |     "transformIgnorePatterns": [ | ||||||
|  |       "<rootDir>/node_modules/(?!lodash-es/.*)" | ||||||
|  |     ], | ||||||
|     "moduleNameMapper": { |     "moduleNameMapper": { | ||||||
|       "^~/(.*)$": "<rootDir>/src/$1" |       "^~/(.*)$": "<rootDir>/src/$1" | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  | @ -1,93 +0,0 @@ | ||||||
| /* eslint no-undef: "off" */ |  | ||||||
| 
 |  | ||||||
| // This is the code piece that GenerateSW mode can't provide for us.
 |  | ||||||
| // This code listens for the user's confirmation to update the app.
 |  | ||||||
| workbox.loadModule('workbox-routing') |  | ||||||
| workbox.loadModule('workbox-strategies') |  | ||||||
| workbox.loadModule('workbox-expiration') |  | ||||||
| 
 |  | ||||||
| self.addEventListener('message', (e) => { |  | ||||||
|   if (!e.data) { |  | ||||||
|     return |  | ||||||
|   } |  | ||||||
|   console.log('[sw] received message', e.data) |  | ||||||
|   switch (e.data.command) { |  | ||||||
|     case 'skipWaiting': |  | ||||||
|       self.skipWaiting() |  | ||||||
|       break |  | ||||||
|     case 'serverChosen': |  | ||||||
|       self.registerServerRoutes(e.data.serverUrl) |  | ||||||
|       break |  | ||||||
|     default: |  | ||||||
|       // NOOP
 |  | ||||||
|       break |  | ||||||
|   } |  | ||||||
| }) |  | ||||||
| workbox.core.clientsClaim() |  | ||||||
| 
 |  | ||||||
| const router = new workbox.routing.Router() |  | ||||||
| router.addCacheListener() |  | ||||||
| router.addFetchListener() |  | ||||||
| 
 |  | ||||||
| let registeredServerRoutes = [] |  | ||||||
| self.registerServerRoutes = (serverUrl) => { |  | ||||||
|   console.log('[sw] Setting up API caching for', serverUrl) |  | ||||||
|   registeredServerRoutes.forEach((r) => { |  | ||||||
|     console.log('[sw] Unregistering previous API route...', r) |  | ||||||
|     router.unregisterRoute(r) |  | ||||||
|   }) |  | ||||||
|   if (!serverUrl) { |  | ||||||
|     return |  | ||||||
|   } |  | ||||||
|   const regexReadyServerUrl = serverUrl.replace('.', '\\.') |  | ||||||
|   registeredServerRoutes = [] |  | ||||||
|   const networkFirstPaths = [ |  | ||||||
|     'api/v1/', |  | ||||||
|     'media/' |  | ||||||
|   ] |  | ||||||
|   const networkFirstExcludedPaths = [ |  | ||||||
|     'api/v1/listen' |  | ||||||
|   ] |  | ||||||
|   const strategy = new workbox.strategies.NetworkFirst({ |  | ||||||
|     cacheName: 'api-cache:' + serverUrl, |  | ||||||
|     plugins: [ |  | ||||||
|       new workbox.expiration.Plugin({ |  | ||||||
|         maxAgeSeconds: 24 * 60 * 60 * 7 |  | ||||||
|       }) |  | ||||||
|     ] |  | ||||||
|   }) |  | ||||||
|   const networkFirstRoutes = networkFirstPaths.map((path) => { |  | ||||||
|     const regex = new RegExp(regexReadyServerUrl + path) |  | ||||||
|     return new workbox.routing.RegExpRoute(regex, () => {}) |  | ||||||
|   }) |  | ||||||
|   const matcher = ({ url, event }) => { |  | ||||||
|     for (let index = 0; index < networkFirstExcludedPaths.length; index++) { |  | ||||||
|       const blacklistedPath = networkFirstExcludedPaths[index] |  | ||||||
|       if (url.pathname.startsWith('/' + blacklistedPath)) { |  | ||||||
|         // the path is blacklisted, we don't cache it at all
 |  | ||||||
|         console.log('[sw] Path is blacklisted, not caching', url.pathname) |  | ||||||
|         return false |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     // we call other regex matchers
 |  | ||||||
|     for (let index = 0; index < networkFirstRoutes.length; index++) { |  | ||||||
|       const route = networkFirstRoutes[index] |  | ||||||
|       const result = route.match({ url, event }) |  | ||||||
|       if (result) { |  | ||||||
|         return result |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return false |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const route = new workbox.routing.Route(matcher, strategy) |  | ||||||
|   console.log('[sw] registering new API route...', route) |  | ||||||
|   router.registerRoute(route) |  | ||||||
|   registeredServerRoutes.push(route) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // The precaching code provided by Workbox.
 |  | ||||||
| self.__precacheManifest = [].concat(self.__precacheManifest || []) |  | ||||||
| 
 |  | ||||||
| // workbox.precaching.suppressWarnings(); // Only used with Vue CLI 3 and Workbox v3.
 |  | ||||||
| workbox.precaching.precacheAndRoute(self.__precacheManifest, {}) |  | ||||||
|  | @ -111,64 +111,6 @@ const { width } = useWindowSize() | ||||||
| const player = ref() | const player = ref() | ||||||
| const showShortcutsModal = ref(false) | const showShortcutsModal = ref(false) | ||||||
| const showSetInstanceModal = ref(false) | const showSetInstanceModal = ref(false) | ||||||
| // export default { |  | ||||||
| //   computed: { |  | ||||||
| //     ...mapState({ |  | ||||||
| //       serviceWorker: state => state.ui.serviceWorker |  | ||||||
| //     }), |  | ||||||
| //   }, |  | ||||||
| //   watch: { |  | ||||||
| //     'serviceWorker.updateAvailable': { |  | ||||||
| //       handler (v) { |  | ||||||
| //         if (!v) { |  | ||||||
| //           return |  | ||||||
| //         } |  | ||||||
| //         const self = this |  | ||||||
| //         this.$store.commit('ui/addMessage', { |  | ||||||
| //           content: this.$pgettext('App/Message/Paragraph', 'A new version of the app is available.'), |  | ||||||
| //           date: new Date(), |  | ||||||
| //           key: 'refreshApp', |  | ||||||
| //           displayTime: 0, |  | ||||||
| //           classActions: 'bottom attached opaque', |  | ||||||
| //           actions: [ |  | ||||||
| //             { |  | ||||||
| //               text: this.$pgettext('App/Message/Paragraph', 'Update'), |  | ||||||
| //               class: 'primary', |  | ||||||
| //               click: function () { |  | ||||||
| //                 self.updateApp() |  | ||||||
| //               } |  | ||||||
| //             }, |  | ||||||
| //             { |  | ||||||
| //               text: this.$pgettext('App/Message/Paragraph', 'Later'), |  | ||||||
| //               class: 'basic' |  | ||||||
| //             } |  | ||||||
| //           ] |  | ||||||
| //         }) |  | ||||||
| //       }, |  | ||||||
| //       immediate: true |  | ||||||
| //     } |  | ||||||
| //   }, |  | ||||||
| //   async created () { |  | ||||||
| //     if (navigator.serviceWorker) { |  | ||||||
| //       navigator.serviceWorker.addEventListener( |  | ||||||
| //         'controllerchange', () => { |  | ||||||
| //           if (this.serviceWorker.refreshing) return |  | ||||||
| //           this.$store.commit('ui/serviceWorker', { |  | ||||||
| //             refreshing: true |  | ||||||
| //           }) |  | ||||||
| //           window.location.reload() |  | ||||||
| //         } |  | ||||||
| //       ) |  | ||||||
| //     } |  | ||||||
| //   }, |  | ||||||
| //   methods: { |  | ||||||
| //     updateApp () { |  | ||||||
| //       this.$store.commit('ui/serviceWorker', { updateAvailable: false }) |  | ||||||
| //       if (!this.serviceWorker.registration || !this.serviceWorker.registration.waiting) { return } |  | ||||||
| //       this.serviceWorker.registration.waiting.postMessage({ command: 'skipWaiting' }) |  | ||||||
| //     }, |  | ||||||
| //   } |  | ||||||
| // } |  | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|  |  | ||||||
|  | @ -1,45 +1,40 @@ | ||||||
| import { InitModule } from '~/types' | import { AppModule } from '~/types' | ||||||
| import { register } from 'register-service-worker' | import { registerSW } from 'virtual:pwa-register' | ||||||
|  | import logger from '~/logging' | ||||||
|  | import Vue from 'vue' | ||||||
| 
 | 
 | ||||||
| export const install: InitModule = ({ store }) => { | const { $pgettext } = Vue.prototype | ||||||
|   if (import.meta.env.PROD) { | 
 | ||||||
|     register(`${import.meta.env.BASE_URL}service-worker.js`, { | export const install: AppModule = ({ store }) => { | ||||||
|       registrationOptions: { scope: '/' }, |   const updateSW = registerSW({ | ||||||
|       ready () { |     onRegisterError () { | ||||||
|         console.log( |       logger.default.error('SW install error') | ||||||
|           'App is being served from cache by a service worker.' |  | ||||||
|         ) |  | ||||||
|     }, |     }, | ||||||
|       registered (registration) { |     onOfflineReady () { | ||||||
|         console.log('Service worker has been registered.') |       logger.default.info('Funkwhale is being served from cache by a service worker.') | ||||||
|         // check for updates every 2 hours
 |     }, | ||||||
|         const checkInterval = 1000 * 60 * 60 * 2 |     onRegistered () { | ||||||
|         // var checkInterval = 1000 * 5
 |       logger.default.info('Service worker has been registered.') | ||||||
|         setInterval(() => { |     }, | ||||||
|           console.log('Checking for service worker update…') |     onNeedRefresh () { | ||||||
|           registration.update() |       store.commit('ui/addMessage', { | ||||||
|         }, checkInterval) |         content: $pgettext('App/Message/Paragraph', 'A new version of the app is available.'), | ||||||
|         store.commit('ui/serviceWorker', { registration: registration }) |         date: new Date(), | ||||||
|         if (registration.active) { |         key: 'refreshApp', | ||||||
|           registration.active.postMessage({ command: 'serverChosen', serverUrl: store.state.instance.instanceUrl }) |         displayTime: 0, | ||||||
|         } |         classActions: 'bottom attached opaque', | ||||||
|       }, |         actions: [ | ||||||
|       cached () { |           { | ||||||
|         console.log('Content has been cached for offline use.') |             text: $pgettext('App/Message/Paragraph', 'Update'), | ||||||
|       }, |             class: 'primary', | ||||||
|       updatefound () { |             click: () => updateSW() | ||||||
|         console.log('New content is downloading.') |           }, | ||||||
|       }, |           { | ||||||
|       updated (registration) { |             text: $pgettext('App/Message/Paragraph', 'Later'), | ||||||
|         console.log('New content is available; please refresh!') |             class: 'basic' | ||||||
|         store.commit('ui/serviceWorker', { updateAvailable: true, registration: registration }) |  | ||||||
|       }, |  | ||||||
|       offline () { |  | ||||||
|         console.log('No internet connection found. App is running in offline mode.') |  | ||||||
|       }, |  | ||||||
|       error (error) { |  | ||||||
|         console.error('Error during service worker registration:', error) |  | ||||||
|           } |           } | ||||||
|  |         ] | ||||||
|       }) |       }) | ||||||
|     } |     } | ||||||
|  |   }) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,48 @@ | ||||||
|  | import { cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching' | ||||||
|  | import { NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies' | ||||||
|  | import { ExpirationPlugin } from 'workbox-expiration' | ||||||
|  | import { registerRoute } from 'workbox-routing' | ||||||
|  | import { clientsClaim } from 'workbox-core' | ||||||
|  | 
 | ||||||
|  | declare let self: ServiceWorkerGlobalScope | ||||||
|  | 
 | ||||||
|  | // NOTE: Clean up outdated caches
 | ||||||
|  | //       With each new production build, all precached assets
 | ||||||
|  | //       that were modified are added to the cache. The old versions
 | ||||||
|  | //       need to be removed manually.
 | ||||||
|  | cleanupOutdatedCaches() | ||||||
|  | 
 | ||||||
|  | // Let new service worker claim control of already open web pages
 | ||||||
|  | // https://developer.chrome.com/docs/workbox/modules/workbox-core/#clients-claim
 | ||||||
|  | clientsClaim() | ||||||
|  | 
 | ||||||
|  | // Support for an update prompt handled by VitePWA:
 | ||||||
|  | // https://vite-plugin-pwa.netlify.app/guide/prompt-for-update.html
 | ||||||
|  | self.addEventListener('message', (event) => { | ||||||
|  |   if (event.data?.type === 'SKIP_WAITING') { | ||||||
|  |     return self.skipWaiting() | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | // NOTE: Network-First cache for API calls
 | ||||||
|  | //       We're using cache only when the user goes offline
 | ||||||
|  | registerRoute(({ url }) => { | ||||||
|  |   if (url.pathname.startsWith('/api/v1/listen')) return false | ||||||
|  |   return url.pathname.startsWith('/api/v1') | ||||||
|  | }, new NetworkFirst({ | ||||||
|  |   plugins: [ | ||||||
|  |     // Expire after a week
 | ||||||
|  |     new ExpirationPlugin({ maxAgeSeconds: 7 * 24 * 3600 }) | ||||||
|  |   ] | ||||||
|  | })) | ||||||
|  | 
 | ||||||
|  | // NOTE: Stale-While-Revalidate cache for album covers
 | ||||||
|  | //       We're serving from cache if available and making a request
 | ||||||
|  | //       in the background to update the cache for next request
 | ||||||
|  | registerRoute(({ url }) => { | ||||||
|  |   return url.pathname.startsWith('/media') | ||||||
|  | }, new StaleWhileRevalidate()) | ||||||
|  | 
 | ||||||
|  | // Precache all assets and add routes for them
 | ||||||
|  | // https://developer.chrome.com/docs/workbox/reference/workbox-precaching/#method-precacheAndRoute
 | ||||||
|  | precacheAndRoute(self.__WB_MANIFEST) | ||||||
|  | @ -9,12 +9,6 @@ function getDefaultUrl () { | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function notifyServiceWorker (registration, message) { |  | ||||||
|   if (registration && registration.active) { |  | ||||||
|     registration.active.postMessage(message) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export default { | export default { | ||||||
|   namespaced: true, |   namespaced: true, | ||||||
|   state: { |   state: { | ||||||
|  | @ -87,7 +81,7 @@ export default { | ||||||
|         value = value + '/' |         value = value + '/' | ||||||
|       } |       } | ||||||
|       state.instanceUrl = value |       state.instanceUrl = value | ||||||
|       notifyServiceWorker(state.registration, { command: 'serverChosen', serverUrl: state.instanceUrl }) | 
 | ||||||
|       // append the URL to the list (and remove existing one if needed)
 |       // append the URL to the list (and remove existing one if needed)
 | ||||||
|       if (value) { |       if (value) { | ||||||
|         const index = state.knownInstances.indexOf(value) |         const index = state.knownInstances.indexOf(value) | ||||||
|  |  | ||||||
|  | @ -174,11 +174,6 @@ export default { | ||||||
|         orderingDirection: '-', |         orderingDirection: '-', | ||||||
|         ordering: 'creation_date' |         ordering: 'creation_date' | ||||||
|       } |       } | ||||||
|     }, |  | ||||||
|     serviceWorker: { |  | ||||||
|       refreshing: false, |  | ||||||
|       registration: null, |  | ||||||
|       updateAvailable: false |  | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   getters: { |   getters: { | ||||||
|  | @ -310,9 +305,6 @@ export default { | ||||||
|       state.routePreferences[route].orderingDirection = value |       state.routePreferences[route].orderingDirection = value | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     serviceWorker: (state, value) => { |  | ||||||
|       state.serviceWorker = { ...state.serviceWorker, ...value } |  | ||||||
|     }, |  | ||||||
|     window: (state, value) => { |     window: (state, value) => { | ||||||
|       state.window = value |       state.window = value | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -19,8 +19,9 @@ export function parseAPIErrors (responseData: APIErrorResponse, parentField?: st | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       const value = responseData[field] |       const value = responseData[field] | ||||||
|       if (value as string[]) { |       if (Array.isArray(value)) { | ||||||
|         errors.push(...(value as string[]).map(err => { |         const values = value as string[] | ||||||
|  |         errors.push(...values.map(err => { | ||||||
|           return err.toLocaleLowerCase().includes('this field ') |           return err.toLocaleLowerCase().includes('this field ') | ||||||
|             ? `${fieldName}: ${err}` |             ? `${fieldName}: ${err}` | ||||||
|             : err |             : err | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ export default { | ||||||
| 
 | 
 | ||||||
|     return hours >= 1 |     return hours >= 1 | ||||||
|       ? `${hours}:${pad(min)}:${pad(sec)}` |       ? `${hours}:${pad(min)}:${pad(sec)}` | ||||||
|       : `${pad(min)}:${pad(sec)}` |       : `${min}:${pad(sec)}` | ||||||
|   }, |   }, | ||||||
|   durationFormatted (v: string) { |   durationFormatted (v: string) { | ||||||
|     const duration = parseInt(v) |     const duration = parseInt(v) | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import {expect} from 'chai' | import {expect} from 'chai' | ||||||
| import moment from 'moment' | import moment from 'moment' | ||||||
| import {truncate, ago, capitalize, year, unique} from '~/filters' | import {truncate, ago, capitalize, year, unique} from '~/init/filters' | ||||||
| 
 | 
 | ||||||
| describe('filters', () => { | describe('filters', () => { | ||||||
|   describe('truncate', () => { |   describe('truncate', () => { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| var sinon = require('sinon') | var sinon = require('sinon') | ||||||
| import {expect} from 'chai' | import {expect} from 'chai' | ||||||
|  | import * as _ from 'lodash-es' | ||||||
| 
 | 
 | ||||||
| import store from '~/store/queue' | import store from '~/store/queue' | ||||||
| import { testAction } from '../../utils' | import { testAction } from '../../utils' | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
|     "baseUrl": ".", |     "baseUrl": ".", | ||||||
|     "module": "ESNext", |     "module": "ESNext", | ||||||
|     "target": "ESNext", |     "target": "ESNext", | ||||||
|     "lib": ["DOM", "ESNext"], |     "lib": ["DOM", "ESNext", "WebWorker"], | ||||||
|     "strict": true, |     "strict": true, | ||||||
|     "esModuleInterop": true, |     "esModuleInterop": true, | ||||||
|     "jsx": "preserve", |     "jsx": "preserve", | ||||||
|  |  | ||||||
|  | @ -1,9 +1,8 @@ | ||||||
| import { defineConfig, HmrOptions } from 'vite' | import { defineConfig, HmrOptions } from 'vite' | ||||||
| import { createVuePlugin as Vue2 } from 'vite-plugin-vue2' | import { createVuePlugin as Vue2 } from 'vite-plugin-vue2' | ||||||
| import ScriptSetup from 'unplugin-vue2-script-setup/vite' | import ScriptSetup from 'unplugin-vue2-script-setup/vite' | ||||||
| 
 | import { VitePWA } from 'vite-plugin-pwa' | ||||||
| // @ts-ignore
 | import { resolve } from 'path' | ||||||
| import path from 'path' |  | ||||||
| 
 | 
 | ||||||
| const port = +(process.env.VUE_PORT ?? 8080) | const port = +(process.env.VUE_PORT ?? 8080) | ||||||
| 
 | 
 | ||||||
|  | @ -29,6 +28,18 @@ export default defineConfig(() => ({ | ||||||
|     // https://github.com/antfu/unplugin-vue2-script-setup
 |     // https://github.com/antfu/unplugin-vue2-script-setup
 | ||||||
|     ScriptSetup(), |     ScriptSetup(), | ||||||
| 
 | 
 | ||||||
|  |     // https://github.com/antfu/vite-plugin-pwa
 | ||||||
|  |     VitePWA({ | ||||||
|  |       strategies: 'injectManifest', | ||||||
|  |       srcDir: 'src', | ||||||
|  |       filename: 'serviceWorker.ts', | ||||||
|  |       devOptions: { | ||||||
|  |         enabled: true, | ||||||
|  |         type: 'module', | ||||||
|  |         navigateFallback: 'index.html' | ||||||
|  |       } | ||||||
|  |     }), | ||||||
|  | 
 | ||||||
|     { |     { | ||||||
|       name: 'fix-fomantic-ui-css', |       name: 'fix-fomantic-ui-css', | ||||||
|       transform (src, id) { |       transform (src, id) { | ||||||
|  | @ -41,7 +52,7 @@ export default defineConfig(() => ({ | ||||||
|   server: { port, hmr }, |   server: { port, hmr }, | ||||||
|   resolve: { |   resolve: { | ||||||
|     alias: { |     alias: { | ||||||
|       '~': path.resolve(__dirname, './src') |       '~': resolve(__dirname, './src') | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   build: { |   build: { | ||||||
|  |  | ||||||
							
								
								
									
										840
									
								
								front/yarn.lock
								
								
								
								
							
							
						
						
									
										840
									
								
								front/yarn.lock
								
								
								
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue
	
	 Kasper Seweryn
						Kasper Seweryn