Compare commits

...

3 commits

Author SHA1 Message Date
userquin
8552bc96f9 chore: update pwa module configuration 2023-01-20 19:37:37 +01:00
userquin
c189f951ff chore: remove periodic sync for updates on Tauri 2023-01-20 17:35:36 +01:00
userquin
b85c1bacc2 feat(native): add pwa to Tauri 2023-01-20 16:25:28 +01:00
7 changed files with 196 additions and 53 deletions

View file

@ -11,6 +11,8 @@ export function setupPageHeader() {
return acc return acc
}, {} as Record<string, Directions>) }, {} as Record<string, Directions>)
const publicRuntimeConfig = useRuntimeConfig().public
useHeadFixed({ useHeadFixed({
htmlAttrs: { htmlAttrs: {
lang: () => locale.value, lang: () => locale.value,
@ -23,12 +25,17 @@ export function setupPageHeader() {
titleTemplate += ` (${buildInfo.env})` titleTemplate += ` (${buildInfo.env})`
return titleTemplate return titleTemplate
}, },
link: process.client && useRuntimeConfig().public.pwaEnabled link: process.client && publicRuntimeConfig.pwaEnabled && !publicRuntimeConfig.tauriPlatform
? () => [{ ? () => [{
key: 'webmanifest', key: 'webmanifest',
rel: 'manifest', rel: 'manifest',
href: `/manifest-${locale.value}${colorMode.value === 'dark' ? '-dark' : ''}.webmanifest`, href: `/manifest-${locale.value}${colorMode.value === 'dark' ? '-dark' : ''}.webmanifest`,
}] }]
: [], : process.client && publicRuntimeConfig.pwaEnabled && publicRuntimeConfig.tauriPlatform
? () => [{
rel: 'manifest',
href: '/manifest.webmanifest',
}]
: [],
}) })
} }

View file

@ -37,6 +37,34 @@ export const createI18n = async (): Promise<LocalizedWebManifest> => {
const defaultManifest: Required<WebManifestEntry> = pwa.webmanifest[env] const defaultManifest: Required<WebManifestEntry> = pwa.webmanifest[env]
if (process.env.TAURI_PLATFORM) {
return {
'en-US': {
...defaultManifest,
lang: 'en-US',
dir: 'ltr',
scope: '/',
id: '/',
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#ffffff',
icons: [
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
},
],
},
}
}
const locales: RequiredWebManifestEntry[] = await Promise.all( const locales: RequiredWebManifestEntry[] = await Promise.all(
pwaLocales pwaLocales
.filter(l => l.code !== 'en-US') .filter(l => l.code !== 'en-US')

View file

@ -22,6 +22,7 @@ export default defineNuxtModule<VitePWANuxtOptions>({
return vitePwaClientPlugin?.api return vitePwaClientPlugin?.api
} }
let webmanifests: LocalizedWebManifest | undefined let webmanifests: LocalizedWebManifest | undefined
const tauriPlatform = !!process.env.TAURI_PLATFORM
// TODO: combine with configurePWAOptions? // TODO: combine with configurePWAOptions?
nuxt.hook('nitro:init', (nitro) => { nuxt.hook('nitro:init', (nitro) => {
@ -41,50 +42,59 @@ export default defineNuxtModule<VitePWANuxtOptions>({
throw new Error('Remove vite-plugin-pwa plugin from Vite Plugins entry in Nuxt config file!') throw new Error('Remove vite-plugin-pwa plugin from Vite Plugins entry in Nuxt config file!')
webmanifests = await createI18n() webmanifests = await createI18n()
const generateManifest = (entry: string) => {
const manifest = webmanifests![entry]
if (!manifest)
throw new Error(`No webmanifest found for locale/theme ${entry}`)
return JSON.stringify(manifest)
}
viteInlineConfig.plugins.push({
name: 'elk:pwa:locales:build',
apply: 'build',
generateBundle(_, bundle) {
if (options.disable || !bundle)
return
Object.keys(webmanifests!).map(wm => [wm, `manifest-${wm}.webmanifest`]).forEach(([wm, fileName]) => { if (tauriPlatform) {
bundle[fileName] = { options.filename = 'tauri-sw.ts'
isAsset: true, options.manifest = webmanifests['en-US']!
type: 'asset', options.injectManifest = options.injectManifest || {}
name: undefined, options.injectManifest.injectionPoint = undefined
source: generateManifest(wm), }
fileName, else {
} const generateManifest = (entry: string) => {
}) const manifest = webmanifests![entry]
}, if (!manifest)
}) throw new Error(`No webmanifest found for locale/theme ${entry}`)
viteInlineConfig.plugins.push({ return JSON.stringify(manifest)
name: 'elk:pwa:locales:dev', }
apply: 'serve', viteInlineConfig.plugins.push({
configureServer(server) { name: 'elk:pwa:locales:build',
const localeMatcher = new RegExp(`^${nuxt.options.app.baseURL}manifest-(.*).webmanifest$`) apply: 'build',
server.middlewares.use((req, res, next) => { generateBundle(_, bundle) {
const match = req.url?.match(localeMatcher) if (options.disable || !bundle)
const entry = match && webmanifests![match[1]] return
if (entry) {
res.statusCode = 200 Object.keys(webmanifests!).map(wm => [wm, `manifest-${wm}.webmanifest`]).forEach(([wm, fileName]) => {
res.setHeader('Content-Type', 'application/manifest+json') bundle[fileName] = {
res.write(JSON.stringify(entry), 'utf-8') isAsset: true,
res.end() type: 'asset',
} name: undefined,
else { source: generateManifest(wm),
next() fileName,
} }
}) })
}, },
}) })
viteInlineConfig.plugins.push({
name: 'elk:pwa:locales:dev',
apply: 'serve',
configureServer(server) {
const localeMatcher = new RegExp(`^${nuxt.options.app.baseURL}manifest-(.*).webmanifest$`)
server.middlewares.use((req, res, next) => {
const match = req.url?.match(localeMatcher)
const entry = match && webmanifests![match[1]]
if (entry) {
res.statusCode = 200
res.setHeader('Content-Type', 'application/manifest+json')
res.write(JSON.stringify(entry), 'utf-8')
res.end()
}
else {
next()
}
})
},
})
}
configurePWAOptions(options, nuxt) configurePWAOptions(options, nuxt)
const plugins = VitePWA(options) const plugins = VitePWA(options)
@ -106,7 +116,7 @@ export default defineNuxtModule<VitePWANuxtOptions>({
return return
viteServer.middlewares.stack.push({ route: webManifest, handle: emptyHandle }) viteServer.middlewares.stack.push({ route: webManifest, handle: emptyHandle })
if (webmanifests) { if (webmanifests && !tauriPlatform) {
Object.keys(webmanifests).forEach((wm) => { Object.keys(webmanifests).forEach((wm) => {
viteServer.middlewares.stack.push({ viteServer.middlewares.stack.push({
route: `${nuxt.options.app.baseURL}manifest-${wm}.webmanifest`, route: `${nuxt.options.app.baseURL}manifest-${wm}.webmanifest`,
@ -131,6 +141,10 @@ export default defineNuxtModule<VitePWANuxtOptions>({
} }
else { else {
nuxt.hook('nitro:config', async (nitroConfig) => { nuxt.hook('nitro:config', async (nitroConfig) => {
// /manifest.webmanifest added on nuxt config file
if (!tauriPlatform)
return
nitroConfig.routeRules = nitroConfig.routeRules || {} nitroConfig.routeRules = nitroConfig.routeRules || {}
for (const locale of pwaLocales) { for (const locale of pwaLocales) {
nitroConfig.routeRules![`/manifest-${locale.code}.webmanifest`] = { nitroConfig.routeRules![`/manifest-${locale.code}.webmanifest`] = {

View file

@ -15,7 +15,6 @@ export default defineNuxtModule({
if (nuxt.options.dev) if (nuxt.options.dev)
nuxt.options.ssr = false nuxt.options.ssr = false
nuxt.options.pwa.disable = true
nuxt.options.sourcemap.client = false nuxt.options.sourcemap.client = false
nuxt.options.alias = { nuxt.options.alias = {
@ -44,12 +43,6 @@ export default defineNuxtModule({
// cleanup files copied from the public folder that we don't need // cleanup files copied from the public folder that we don't need
nuxt.hook('close', async () => { nuxt.hook('close', async () => {
await rm('.output/public/_redirects') await rm('.output/public/_redirects')
await rm('.output/public/apple-touch-icon.png')
await rm('.output/public/elk-og.png')
await rm('.output/public/favicon.ico')
await rm('.output/public/pwa-192x192.png')
await rm('.output/public/pwa-512x512.png')
await rm('.output/public/robots.txt')
}) })
}, },
}) })

View file

@ -92,6 +92,7 @@ export default defineNuxtConfig({
public: { public: {
env: '', // set in build-env module env: '', // set in build-env module
buildInfo: {} as BuildInfo, // set in build-env module buildInfo: {} as BuildInfo, // set in build-env module
tauriPlatform: !!process.env.TAURI_PLATFORM,
pwaEnabled: !isDevelopment || process.env.VITE_DEV_PWA === 'true', pwaEnabled: !isDevelopment || process.env.VITE_DEV_PWA === 'true',
// We use LibreTranslate(https://github.com/LibreTranslate/LibreTranslate) as our default translation server #76 // We use LibreTranslate(https://github.com/LibreTranslate/LibreTranslate) as our default translation server #76
translateApi: '', translateApi: '',

View file

@ -37,6 +37,9 @@ export default defineNuxtPlugin(() => {
registrationError.value = true registrationError.value = true
}, },
onRegisteredSW(swUrl, r) { onRegisteredSW(swUrl, r) {
if (useRuntimeConfig().public.tauriPlatform)
return
// should add support in pwa plugin // should add support in pwa plugin
if (r?.active?.state === 'activated') { if (r?.active?.state === 'activated') {
swActivated.value = true swActivated.value = true

View file

@ -0,0 +1,97 @@
/// <reference lib="WebWorker" />
/// <reference types="vite/client" />
import { clientsClaim } from 'workbox-core'
import { registerRoute } from 'workbox-routing'
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies'
import { CacheableResponsePlugin } from 'workbox-cacheable-response'
import { ExpirationPlugin } from 'workbox-expiration'
import { onNotificationClick, onPush } from './web-push-notifications'
declare let self: ServiceWorkerGlobalScope
self.skipWaiting()
clientsClaim()
if (import.meta.env.DEV) {
// Avoid caching on dev: force always go to the server
registerRoute(
() => true,
new NetworkFirst({
cacheName: 'elk-dev',
plugins: [
new CacheableResponsePlugin({ statuses: [-1] }),
],
}),
)
}
if (import.meta.env.PROD) {
const denyList: RegExp[] = [/^\/api\//, /^\/login\//, /^\/oauth\//, /^\/signin\//, /^\/web-share-target\//]
const matchDenyList = (url: URL) => {
const pathnameAndSearch = url.pathname + url.search
for (const regExp of denyList) {
if (regExp.test(pathnameAndSearch))
return false
}
return true
}
// Cache page navigations (html) with a Network First strategy
registerRoute(
({ sameOrigin, request, url }) => {
return sameOrigin && request.mode === 'navigate' && matchDenyList(url)
},
new NetworkFirst({
cacheName: 'elk-pages',
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
],
}),
)
// Cache CSS, JS, and Web Worker requests with a Stale While Revalidate strategy
registerRoute(
({ sameOrigin, request }) =>
sameOrigin && (request.destination === 'style'
|| request.destination === 'manifest'
|| request.destination === 'script'
|| request.destination === 'worker'),
new StaleWhileRevalidate({
cacheName: 'elk-assets',
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
],
}),
)
// include shiki cache
registerRoute(
({ sameOrigin, url }) =>
sameOrigin && url.pathname.startsWith('/shiki/'),
new StaleWhileRevalidate({
cacheName: 'elk-shiki',
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
// 365 days max
new ExpirationPlugin({ maxAgeSeconds: 60 * 60 * 24 * 365 }),
],
}),
)
// Cache images with a Cache First strategy
registerRoute(
({ sameOrigin, request }) =>
sameOrigin && request.destination === 'image',
new CacheFirst({
cacheName: 'elk-images',
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
// 150 max, 30 days max: purge on quota error
new ExpirationPlugin({ purgeOnQuotaError: true, maxEntries: 150, maxAgeSeconds: 60 * 60 * 24 * 30 }),
],
}),
)
}
self.addEventListener('push', onPush)
self.addEventListener('notificationclick', onNotificationClick)