Ctrl K
ComunidadGuías…

vite.config.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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',
  },
})