95 lines
4.1 KiB
JavaScript
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()] })
|