Files

115 lines
3.8 KiB
TypeScript

import {defineConfig, type Plugin} from 'vite'
import {readFileSync} from 'fs'
import {createHash} from 'crypto'
import {resolve, join} from 'path'
import {tmpdir} from 'os'
import presets from './web/servers.json'
const PORT_FILE = join(tmpdir(), 'xftp-test-server.port')
const FIXTURES = resolve(import.meta.dirname, '../tests/fixtures')
const __dirname = import.meta.dirname
function parseHost(addr: string): string {
const m = addr.match(/@(.+)$/)
if (!m) throw new Error('bad server address: ' + addr)
const host = m[1].split(',')[0]
return host.includes(':') ? host : host + ':443'
}
function cspPlugin(servers: string[], isDev: boolean): Plugin {
const origins = servers.map(s => 'https://' + parseHost(s)).join(' ')
return {
name: 'csp-connect-src',
transformIndexHtml: {
order: 'pre',
handler(html) {
if (isDev) {
return html.replace(/<meta\s[^>]*?Content-Security-Policy[\s\S]*?>/i, '')
}
// Auto-compute SHA-256 hashes for inline scripts (CSP script-src)
const scriptHashes: string[] = []
html.replace(/<script>(.+?)<\/script>/gs, (_m: string, content: string) => {
scriptHashes.push("'sha256-" + createHash('sha256').update(content, 'utf-8').digest('base64') + "'")
return _m
})
return html
.replace('__CSP_CONNECT_SRC__', origins)
.replace('__CSP_SCRIPT_HASHES__', scriptHashes.join(' '))
}
}
}
}
// Compute fingerprint from ca.crt (SHA-256 of DER)
function getFingerprint(): string {
const pem = readFileSync(join(FIXTURES, 'ca.crt'), 'utf-8')
const der = Buffer.from(pem.replace(/-----[^-]+-----/g, '').replace(/\s/g, ''), 'base64')
return createHash('sha256').update(der).digest('base64')
.replace(/\+/g, '-').replace(/\//g, '_')
}
// Plugin to inject __XFTP_SERVERS__ via Vite define (reads PORT_FILE written by test/runSetup.ts)
function xftpServersPlugin(): Plugin {
const fp = getFingerprint()
return {
name: 'xftp-servers-define',
config() {
const port = readFileSync(PORT_FILE, 'utf-8').trim()
const serverAddr = `xftp://${fp}@localhost:${port}`
return {define: {__XFTP_SERVERS__: JSON.stringify([serverAddr])}}
}
}
}
export default defineConfig(({mode}) => {
const define: Record<string, string> = {}
let servers: string[]
const plugins: Plugin[] = []
if (mode === 'development') {
// In development mode, use the test server (port from globalSetup)
plugins.push(xftpServersPlugin())
define['__XFTP_PROXY_PORT__'] = JSON.stringify(null)
// For CSP plugin, use localhost placeholder (CSP stripped in dev server anyway)
servers = ['xftp://fp@localhost:443']
} else {
// In production mode, use the preset servers
servers = [...presets.simplex]
define['__XFTP_SERVERS__'] = JSON.stringify(servers)
define['__XFTP_PROXY_PORT__'] = JSON.stringify(null)
}
plugins.push(cspPlugin(servers, mode === 'development'))
const httpsConfig = mode === 'development' ? {
key: readFileSync(join(FIXTURES, 'web.key')),
cert: readFileSync(join(FIXTURES, 'web.crt')),
} : undefined
return {
base: './',
root: 'web',
build: {
outDir: resolve(__dirname, 'dist-web'),
emptyOutDir: true,
target: 'esnext',
minify: false,
chunkSizeWarningLimit: 1200,
rollupOptions: {
external: ['node:http2', 'url'],
output: {
entryFileNames: 'assets/[name].js',
chunkFileNames: 'assets/[name].js',
assetFileNames: 'assets/[name][extname]',
},
},
},
server: httpsConfig ? {https: httpsConfig} : {},
preview: {host: true, https: false},
define,
worker: {format: 'es' as const, rollupOptions: {external: ['node:http2', 'url'], output: {entryFileNames: 'assets/[name].js'}}},
plugins,
}
})