Improved harness - with card selection
This commit is contained in:
@@ -1,41 +1,94 @@
|
||||
import { createReadStream } from 'node:fs'
|
||||
import { stat } from 'node:fs/promises'
|
||||
import { defineConfig } from 'vite'
|
||||
import { cardArtRoot, scanCardCatalog, writeCardCatalog } from './scripts/card-catalog.mjs'
|
||||
import { acceptedCardsRoot, resolveCardAssetRequest, scanCardCatalog } from './scripts/card-catalog.mjs'
|
||||
import { readSurfaceDefaults, surfaceDefaultsPath, updateSurfaceDefaults } from './scripts/surface-defaults.mjs'
|
||||
|
||||
function cardCatalogPlugin() {
|
||||
return {
|
||||
name: 'card-catalog',
|
||||
async buildStart() {
|
||||
const catalog = await writeCardCatalog()
|
||||
if (!catalog.cards.length || catalog.errors.length) {
|
||||
this.error(`Invalid card catalog:\n${catalog.errors.join('\n') || 'No valid card pairs found'}`)
|
||||
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' })
|
||||
}
|
||||
},
|
||||
configureServer(server) {
|
||||
server.middlewares.use('/__card_catalog', async (_request, response) => {
|
||||
try {
|
||||
const catalog = await scanCardCatalog()
|
||||
response.statusCode = 200
|
||||
response.setHeader('Content-Type', 'application/json')
|
||||
response.setHeader('Cache-Control', 'no-store')
|
||||
response.end(JSON.stringify(catalog))
|
||||
} catch (error) {
|
||||
response.statusCode = 500
|
||||
response.setHeader('Content-Type', 'application/json')
|
||||
response.end(JSON.stringify({
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}))
|
||||
}
|
||||
})
|
||||
server.watcher.add(cardArtRoot)
|
||||
server.watcher.on('all', (_event, path) => {
|
||||
if (path.startsWith(cardArtRoot) && path.toLowerCase().endsWith('.png')) {
|
||||
server.ws.send({ type: 'custom', event: 'card-catalog:update' })
|
||||
}
|
||||
})
|
||||
},
|
||||
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' })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [cardCatalogPlugin()],
|
||||
})
|
||||
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()] })
|
||||
|
||||
Reference in New Issue
Block a user