Files
2026-08-13 12:05:47 -07:00

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),
},
})