import { createReadStream } from 'node:fs' import { stat } from 'node:fs/promises' import { defineConfig } from 'vite' import { acceptedCardsRoot, resolveCardAssetRequest, scanCardCatalog } from './scripts/card-catalog.mjs' import { readSurfaceDefaults, surfaceDefaultsPath, updateSurfaceDefaults } from './scripts/surface-defaults.mjs' function json(response, status, value) { response.statusCode = status response.setHeader('Content-Type', 'application/json') response.setHeader('Cache-Control', 'no-store') response.end(JSON.stringify(value)) } function readJsonBody(request) { return new Promise((resolve, reject) => { let body = '' request.setEncoding('utf8') request.on('data', chunk => { body += chunk if (body.length > 65536) reject(new Error('Request body is too large')) }) request.on('end', () => { try { resolve(JSON.parse(body)) } catch { reject(new Error('Request body is not valid JSON')) } }) request.on('error', reject) }) } function installRoutes(server, writable) { let cachedCatalog let cachedAssetPaths let catalogPromise const catalog = async () => { if (cachedCatalog) return cachedCatalog catalogPromise ??= scanCardCatalog().then(value => { cachedCatalog = value cachedAssetPaths = new Set(value.cards.flatMap(card => Object.values(card.variants).flatMap(group => Object.values(group).flatMap(asset => [asset.artwork, asset.mask, asset.textMask].filter(Boolean) .map(assetUrl => new URL(assetUrl, 'http://card-harness.local').pathname))))) return value }).finally(() => { catalogPromise = undefined }) return catalogPromise } server.middlewares.use(async (request, response, next) => { const url = new URL(request.url ?? '/', 'http://card-harness.local') try { if (url.pathname === '/__card_catalog') return json(response, 200, await catalog()) if (url.pathname === '/__surface_defaults') { if (request.method === 'GET') return json(response, 200, await readSurfaceDefaults(writable)) if (request.method === 'POST' && writable) return json(response, 200, await updateSurfaceDefaults(await readJsonBody(request))) return json(response, 405, { error: 'Surface defaults are read-only in this server mode' }) } if (url.pathname.startsWith('/__card_asset/')) { await catalog() if (!cachedAssetPaths?.has(url.pathname)) return json(response, 404, { error: 'Unknown card asset' }) const path = await resolveCardAssetRequest(url.pathname) if (!path) return json(response, 404, { error: 'Unknown card asset' }) const metadata = await stat(path) response.statusCode = 200 response.setHeader('Content-Type', 'image/png') response.setHeader('Content-Length', String(metadata.size)) response.setHeader('Cache-Control', url.searchParams.has('v') ? 'public, max-age=31536000, immutable' : 'no-cache') return createReadStream(path).pipe(response) } next() } catch (error) { const status = Number.isInteger(error?.statusCode) ? error.statusCode : 500 json(response, status, { error: error instanceof Error ? error.message : String(error) }) } }) if (server.watcher) { server.watcher.add([acceptedCardsRoot, surfaceDefaultsPath]) server.watcher.on('all', (_event, path) => { if (path.startsWith(acceptedCardsRoot)) { cachedCatalog = undefined cachedAssetPaths = undefined server.ws.send({ type: 'custom', event: 'card-catalog:update' }) } if (path === surfaceDefaultsPath) server.ws.send({ type: 'custom', event: 'surface-defaults:update' }) }) } } function cardLibraryPlugin() { return { name: 'card-library', async buildStart() { const catalog = await scanCardCatalog() if (!catalog.cards.length || catalog.errors.length) { this.error(`Invalid accepted card catalog:\n${catalog.errors.join('\n') || 'No approved cards found'}`) } }, configureServer(server) { installRoutes(server, true) }, configurePreviewServer(server) { installRoutes(server, false) }, } } export default defineConfig({ plugins: [cardLibraryPlugin()] })