vite.config.ts
Rama main · réplica de lectura del repositorio original.
import { defineConfig, transformWithEsbuild } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'node:path'
import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import postcss, { type Container } from 'postcss'
import { SUPPORTED_LANGUAGES } from './src/i18n/language'
const require = createRequire(import.meta.url)
const tailwindBrowserRuntime = readFileSync(require.resolve('@tailwindcss/browser'), 'utf8')
// §23 version probe: every production build gets a unique version id, baked
// into the bundle (__APP_VERSION__) AND emitted as dist/version.json. Open tabs
// compare the two and reload themselves at a safe moment after a deploy
// (src/lib/app-update.ts). CI can pin a stable id via GIT_SHA/GITHUB_SHA;
// otherwise the build timestamp is used (every build = a new version).
const appVersion = process.env.APP_VERSION || process.env.GIT_SHA || process.env.GITHUB_SHA || `build-${Date.now().toString(36)}`
// The public release version is separate from the per-build reload identifier.
// About must show a package version even when CI uses a commit SHA as its ID.
const releaseVersion = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version
async function startupAssets() {
// Inline only the tokens the startup screen needs, retaining their original
// selectors and media queries so the app and preboot cannot drift apart.
const properties = new Set([
'--accent-hue', '--color-bg', '--color-fg', '--color-fg-muted',
'--color-accent', '--color-accent-hover', '--color-accent-fg',
'--color-ring', '--font-sans', '--z-overlay', '--z-max',
])
const tokens = postcss.parse(readFileSync(new URL('./src/styles/tokens.css', import.meta.url), 'utf8'))
function prune(container: Container) {
container.each((node) => {
if (node.type === 'decl') {
if (!properties.has(node.prop)) node.remove()
} else if ((node.type === 'rule' || node.type === 'atrule') && node.nodes) {
prune(node)
if (!node.nodes.length) node.remove()
} else node.remove()
})
}
prune(tokens)
const wordmark = readFileSync(new URL('./src/assets/brand/aivory-wordmark.svg', import.meta.url)).toString('base64')
const styles = tokens.toString() + `\n:root{--aivory-startup-wordmark:url("data:image/svg+xml;base64,${wordmark}")}\n` +
readFileSync(new URL('./src/styles/startup.css', import.meta.url), 'utf8')
const messages = Object.fromEntries(SUPPORTED_LANGUAGES.map(({ code }) => [code,
JSON.parse(readFileSync(new URL(`./src/i18n/locales/${code}/common.json`, import.meta.url), 'utf8')).startup,
]))
const languages = await transformWithEsbuild(
readFileSync(new URL('./src/i18n/language.ts', import.meta.url), 'utf8'), 'language.ts',
{ format: 'iife', globalName: 'aivoryLanguages', minify: true },
)
const script = `(function(){var startupMessages=${JSON.stringify(messages)};${languages.code}\n` +
readFileSync(new URL('./src/lib/startup-preboot.js', import.meta.url), 'utf8') + '\n})()'
return { styles, script: script.replace(/<\/script/gi, '<\\/script'), messages }
}
// https://vitejs.dev/config/
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify(appVersion),
__RELEASE_VERSION__: JSON.stringify(releaseVersion),
},
plugins: [
react(),
{
name: 'aivory-inline-startup',
transformIndexHtml: {
order: 'pre',
async handler(html, context) {
if (context.filename !== path.resolve(__dirname, 'index.html')) return html
const { styles, script, messages } = await startupAssets()
return html
.replace('/* aivory:startup-styles */', styles)
.replace('/* aivory:startup-script */', script)
.replace('<!-- aivory:noscript -->', `<p lang="zh">${messages.zh.noScript}</p><p lang="en">${messages.en.noScript}</p>`)
},
},
},
{
name: 'aivory-emit-version-json',
apply: 'build',
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'version.json',
source: JSON.stringify({ version: appVersion }),
})
},
},
{
name: 'aivory-stable-tailwind-browser-runtime',
configureServer(server) {
server.middlewares.use('/tailwind-browser.js', (_req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'text/javascript; charset=utf-8')
res.setHeader('Cache-Control', 'no-cache')
res.end(tailwindBrowserRuntime)
})
},
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'tailwind-browser.js',
source: tailwindBrowserRuntime,
})
},
},
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
optimizeDeps: {
// The dev-time dependency pre-bundler targets es2020 by default, but
// @docx-editor.dev/core ships TOP-LEVEL AWAIT in its dist. Production builds
// already pass (build.target is es2022) — without this the dev server alone
// fails to optimise the Word editor's chunk, so `npm run dev` breaks the
// moment a .docx is opened. Keep the two targets aligned.
esbuildOptions: {
target: 'es2022',
},
},
server: {
port: 5173,
host: true,
proxy: {
'/api': {
target: process.env.VITE_API_TARGET ?? 'http://localhost:8787',
changeOrigin: true,
// SSE (fetch streaming) proxies fine over HTTP/1.1. ws:true forwards the
// WebSocket upgrade for /api/audio/stream (live Volcano voice) to the Go
// backend in dev; HMR runs on Vite's own socket, so this is scoped to
// /api and doesn't touch it.
ws: true,
},
},
},
build: {
target: 'es2022',
sourcemap: false,
cssCodeSplit: true,
},
worker: {
format: 'es',
},
})