Files
Sanctification/card-harness/vite.config.mjs

95 lines
4.1 KiB
JavaScript

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()] })