Files
hapi/web/vite.config.ts
wushenghua e998f05eb2 merge: hapi v0.30.7
Six text conflicts, all resolved:
- cli/apiMachine + runner/run: adopt upstream's MachinePathPolicy; keep the
  local policy that browsing stays disabled until --workspace-root is set
  (banner text + upstream's browse test adapted to assert the local gate).
- cli/codex root.test.ts: keep both the syncHistory split test and upstream's
  change_title/name test; fixture return merges metadata + syncHistory.
- web/SessionList: combine the local personal/global pinned split with
  upstream's search-relevance ranking (deps union).
- web/AskUserQuestionFooter: take upstream's draft-restore effect and thread
  the local skippedByQuestion through the draft store; the local DSH skip test
  moves to AskUserQuestionFooter.skip.test.tsx so upstream's vi.mock setup in
  the original file cannot shadow it.

Also: bun install for the assistant-ui 0.15.21 bump, regenerated
shared/fixtures from the merged pipeline.

Test status vs the pre-merge baseline (d6cf54d7), identical pre-existing
failures only: cli 7, hub 2 (opencodeClear), web 35, shared 2 (DSH); relay
clean; typecheck clean across all packages.
2026-09-29 21:14:39 +08:00

245 lines
9.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { defineConfig, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
import { readFileSync, readdirSync } from 'node:fs'
import { resolve } from 'node:path'
import { shareTargetPathnameFromBase } from './src/lib/sharePath'
function spaFallback(): Plugin {
return {
name: 'spa-fallback',
configureServer(server) {
server.middlewares.use((req, _res, next) => {
const url = (req.url ?? '').split('?')[0]
if (url === '/' || url === '' || url.includes('.') || url.startsWith('/@') || url.startsWith('/api') || url.startsWith('/socket.io') || url.startsWith('/src/')) {
next()
return
}
req.url = url.startsWith('/studio/') ? '/studio.html' : '/index.html'
next()
})
}
}
}
const base = process.env.VITE_BASE_URL || '/'
const shareAction = shareTargetPathnameFromBase(base)
const hubTarget = process.env.VITE_HUB_PROXY || 'http://127.0.0.1:3006'
const appVersion = readAppVersion()
function readAppVersion(): string {
const buildInfoPath = resolve(__dirname, '../shared/src/buildInfo.ts')
const buildInfo = readFileSync(buildInfoPath, 'utf8')
const match = buildInfo.match(/export const APP_VERSION = ['"]([^'"]+)['"]/)
if (!match) {
throw new Error(`Could not read APP_VERSION from ${buildInfoPath}`)
}
return match[1]
}
function getVendorChunkName(id: string): string | undefined {
if (!id.includes('/node_modules/')) {
return undefined
}
if (id.includes('/node_modules/@xterm/')) {
return 'vendor-terminal'
}
if (
id.includes('/node_modules/@assistant-ui/')
|| id.includes('/node_modules/remark-gfm/')
|| id.includes('/node_modules/hast-util-to-jsx-runtime/')
) {
return 'vendor-assistant'
}
if (id.includes('/node_modules/@elevenlabs/react/')) {
return 'vendor-voice'
}
return undefined
}
function copyKaTeXFonts(): Plugin {
return {
name: 'copy-katex-fonts',
apply: 'build',
generateBundle() {
const fontsDir = resolve(__dirname, 'node_modules/katex/dist/fonts')
for (const fileName of readdirSync(fontsDir)) {
if (!/\.(?:ttf|woff|woff2)$/i.test(fileName)) continue
this.emitFile({
type: 'asset',
fileName: `assets/fonts/${fileName}`,
source: readFileSync(resolve(fontsDir, fileName))
})
}
}
}
}
export default defineConfig({
appType: 'spa',
define: {
__APP_VERSION__: JSON.stringify(appVersion),
},
server: {
host: true,
allowedHosts: ['hapidev.weishu.me'],
proxy: {
'/api': {
target: hubTarget,
changeOrigin: true
},
'/socket.io': {
target: hubTarget,
ws: true
}
}
},
plugins: [
react(),
spaFallback(),
copyKaTeXFonts(),
VitePWA({
// User-controlled reload avoids mid-session surprise reloads (autoUpdate reloads all tabs).
registerType: 'prompt',
includeAssets: ['favicon.ico', 'apple-touch-icon-180x180.png', 'mask-icon.svg'],
strategies: 'injectManifest',
srcDir: 'src',
filename: 'sw.ts',
manifest: {
name: 'HAPI',
short_name: 'HAPI',
description: 'AI-powered development assistant',
// An installed Android WebAPK stores theme_color once at install time and uses it
// as a fixed toolbar color, so any value here pins the status bar to one appearance.
// With none, Chrome falls back to white in light mode and black in dark mode. That
// follows the Android system uiMode, not the in-app appearance setting, so the bar
// and the app diverge while an appearance override is active. `undefined` is
// explicit because the plugin fills in its own #42b883 default otherwise, and the
// resulting "theme_color is missing" build warning is wrong: theme_color is
// optional for installability.
theme_color: undefined,
// Splash background stays light; only the status bar needed to become adaptive.
background_color: '#ffffff',
display: 'standalone',
orientation: 'portrait',
scope: base,
start_url: base,
icons: [
{
src: 'pwa-64x64.png',
sizes: '64x64',
type: 'image/png',
purpose: 'any'
},
{
src: 'pwa-192x192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any'
},
{
src: 'pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any'
},
{
src: 'pwa-maskable-192x192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'maskable'
},
{
src: 'pwa-maskable-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable'
}
],
// Web Share Target — Android Chrome routes POSTs to /share
// when the user picks HAPI in the system share sheet. The
// service worker (`web/src/sw.ts`) intercepts POST /share,
// stashes the multipart payload in IndexedDB, and 303-
// redirects to /share?id=<transferId> for the SPA picker.
// `*/*` is the broad fallback; explicit MIME prefixes stay
// first because some Chrome versions only honor declared
// prefixes when surfacing in the share sheet.
share_target: {
action: shareAction,
method: 'POST',
enctype: 'multipart/form-data',
params: {
title: 'title',
text: 'text',
url: 'url',
files: [
{
name: 'files',
accept: [
'image/*',
'application/pdf',
'text/*',
'application/json',
'application/zip',
'*/*'
]
}
]
}
}
},
injectManifest: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff,woff2}'],
// The SPA entry contains the complete multi-agent control surface.
// Keep it available offline after adding Pi history import instead
// of silently dropping the entry chunk from the PWA precache.
maximumFileSizeToCacheInBytes: 3 * 1024 * 1024
},
devOptions: {
enabled: true,
type: 'module'
}
})
],
base,
optimizeDeps: {
// dev 下 VitePWA 会给每个页面注入 service worker 注册,而 sw.ts 里的
// workbox 依赖要等 SW 真正注册后才被 Vite 发现,触发一次「optimized
// dependencies changed → reloading」的整页刷新。这个刷新会打断当时
// 正在跑的 e2e(表现为元素凭空消失)。提前声明即可在启动时一次预构建。
include: [
'workbox-precaching',
'workbox-routing',
'workbox-strategies',
'workbox-expiration'
]
},
resolve: {
// 单例 React 兜底:workspace 里个别依赖(如 @radix-ui/react-popover)
// 没被链进 web/node_modules,只能从仓库根解析,于是拿到与应用代码
// 不同的那份 react 实例。两份 React 同时存在会让带 hook 的第三方
// 组件在渲染时抛 "Invalid hook call",并连累整棵 React 树卸载。
dedupe: ['react', 'react-dom'],
alias: {
'@': resolve(__dirname, 'src')
}
},
build: {
outDir: 'dist',
emptyOutDir: true,
rollupOptions: {
output: {
manualChunks(id) {
return getVendorChunkName(id)
}
}
}
}
})