102 lines
3.5 KiB
TypeScript
102 lines
3.5 KiB
TypeScript
/**
|
|
* Vite build configuration and PWA asset emission.
|
|
*
|
|
* The same per-build identifier is compiled into the browser bundle and the
|
|
* service worker. This makes an hourly/visibility update check discover a new
|
|
* deployment while still leaving activation under explicit user control.
|
|
*/
|
|
import { defineConfig } from 'vite'
|
|
import react from '@vitejs/plugin-react'
|
|
import { readFileSync } from 'node:fs'
|
|
import { parse } from 'smol-toml'
|
|
|
|
// A new URL makes the browser check and stage every deployed service worker.
|
|
// The waiting worker is still activated explicitly by the user in the console.
|
|
const pwaBuildId = Date.now().toString(36)
|
|
const serviceWorkerSource = readFileSync(new URL('./pwa/control-sw.js', import.meta.url), 'utf8')
|
|
const bundledFontLicenses = ['noto-serif-sc', 'zcool-xiaowei', 'lxgw-wenkai'].map(font => ({
|
|
fileName: `fonts/licenses/${font}-OFL.txt`,
|
|
source: readFileSync(
|
|
new URL(`./node_modules/@fontsource/${font}/LICENSE`, import.meta.url),
|
|
'utf8',
|
|
),
|
|
}))
|
|
const i18nResource = parse(
|
|
readFileSync(new URL('../../resources/i18n.toml', import.meta.url), 'utf8'),
|
|
) as unknown as {
|
|
default_language: string
|
|
locales: Record<string, { messages: Record<string, string> }>
|
|
}
|
|
const defaultLanguage = i18nResource.default_language
|
|
const defaultMessages = i18nResource.locales[defaultLanguage].messages
|
|
const manifest = {
|
|
id: '/control',
|
|
name: defaultMessages['pwa.manifest.name'],
|
|
short_name: defaultMessages['pwa.manifest.short_name'],
|
|
description: defaultMessages['pwa.manifest.description'],
|
|
lang: defaultLanguage,
|
|
dir: 'auto',
|
|
start_url: '/control/?source=pwa',
|
|
scope: '/control/',
|
|
display: 'standalone',
|
|
orientation: 'any',
|
|
background_color: '#03121a',
|
|
theme_color: '#0a302f',
|
|
categories: ['utilities', 'productivity'],
|
|
icons: [
|
|
{ src: '/pwa/icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
|
|
{ src: '/pwa/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
|
|
{
|
|
src: '/pwa/icon-maskable-512.png',
|
|
sizes: '512x512',
|
|
type: 'image/png',
|
|
purpose: 'maskable',
|
|
},
|
|
],
|
|
}
|
|
|
|
export default defineConfig({
|
|
plugins: [
|
|
react(),
|
|
{
|
|
name: 'same-origin-woff2-fonts',
|
|
enforce: 'pre',
|
|
transform(source, id) {
|
|
if (!id.includes('/node_modules/@fontsource/') || !id.endsWith('.css')) return
|
|
return source
|
|
.replaceAll('font-display: swap', 'font-display: block')
|
|
.replace(/,\s*url\([^)]*\.woff\) format\(['"]woff['"]\)/g, '')
|
|
},
|
|
},
|
|
{
|
|
name: 'control-pwa-assets',
|
|
transformIndexHtml(html) {
|
|
return html
|
|
.replaceAll('__DEFAULT_LANGUAGE__', defaultLanguage)
|
|
.replaceAll('__APP_NAME__', defaultMessages['pwa.manifest.name'])
|
|
.replaceAll('__APP_SHORT_NAME__', defaultMessages['pwa.manifest.short_name'])
|
|
.replaceAll('__APP_DESCRIPTION__', defaultMessages['pwa.manifest.description'])
|
|
},
|
|
generateBundle() {
|
|
for (const license of bundledFontLicenses) {
|
|
this.emitFile({ type: 'asset', ...license })
|
|
}
|
|
this.emitFile({
|
|
type: 'asset',
|
|
fileName: 'control/sw.js',
|
|
source: serviceWorkerSource.replaceAll('__PWA_BUILD_ID__', pwaBuildId),
|
|
})
|
|
this.emitFile({
|
|
type: 'asset',
|
|
fileName: 'control/manifest.webmanifest',
|
|
source: JSON.stringify(manifest, null, 2),
|
|
})
|
|
},
|
|
},
|
|
],
|
|
define: {
|
|
__PWA_BUILD_ID__: JSON.stringify(pwaBuildId),
|
|
__I18N_RESOURCE__: JSON.stringify(i18nResource),
|
|
},
|
|
})
|