import { spawn } from "node:child_process"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, extname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import type { IncomingMessage, ServerResponse } from "node:http"; import { defineConfig, type Plugin } from "vite"; const root = fileURLToPath(new URL(".", import.meta.url)); const toolsSrc = resolve(root, "../src"); const toolsDir = resolve(root, ".."); const workspaceRoot = resolve(root, "../.."); const assetsDir = resolve(workspaceRoot, "assets/characters"); const textureCli = resolve(toolsSrc, "texture/cli.ts"); function readBody(req: IncomingMessage): Promise { return new Promise((resolveBody, reject) => { const chunks: Buffer[] = []; req.on("data", (chunk: Buffer) => chunks.push(chunk)); req.on("end", () => resolveBody(Buffer.concat(chunks))); req.on("error", reject); }); } function sendJson(res: ServerResponse, status: number, body: unknown): void { res.statusCode = status; res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(body)); } function runTextureCli(payload: unknown): Promise> { return new Promise((resolvePromise, reject) => { const child = spawn( process.execPath, ["--import", "tsx", textureCli], { cwd: toolsDir, stdio: ["pipe", "pipe", "pipe"], env: process.env, }, ); let stdout = ""; let stderr = ""; child.stdout.on("data", (c: Buffer) => (stdout += c.toString())); child.stderr.on("data", (c: Buffer) => (stderr += c.toString())); child.on("error", reject); child.on("close", (code) => { const line = stdout.trim().split("\n").filter(Boolean).pop() ?? ""; try { const json = JSON.parse(line) as Record; if (code !== 0 && !json["ok"]) { reject(new Error(String(json["error"] ?? (stderr || `texture cli exited ${code}`)))); return; } resolvePromise(json); } catch { reject(new Error(stderr || stdout || `texture cli exited ${code}`)); } }); child.stdin.write(JSON.stringify(payload)); child.stdin.end(); }); } /** Dev-only APIs: save GLB, UV guide / Grok texture, serve character assets. */ function creatorApiPlugin(): Plugin { return { name: "psx-creator-api", configureServer(server) { server.middlewares.use(async (req, res, next) => { // GET /assets-characters/ if (req.method === "GET" && req.url?.startsWith("/assets-characters/")) { const name = decodeURIComponent(req.url.slice("/assets-characters/".length).split("?")[0] ?? ""); if (!name || name.includes("..") || name.includes("/")) { res.statusCode = 400; res.end("bad path"); return; } try { const bytes = await readFile(resolve(assetsDir, name)); const ext = extname(name).toLowerCase(); const mime = ext === ".png" ? "image/png" : ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" : ext === ".glb" ? "model/gltf-binary" : "application/octet-stream"; res.statusCode = 200; res.setHeader("Content-Type", mime); res.setHeader("Cache-Control", "no-store"); res.end(bytes); } catch { res.statusCode = 404; res.end("not found"); } return; } if (req.method !== "POST") { next(); return; } try { if (req.url === "/api/save") { const raw = await readBody(req); const payload = JSON.parse(raw.toString("utf8")) as { id?: string; recipe?: unknown; glbBase64?: string; }; const id = payload.id?.replace(/[^a-z0-9_-]/gi, "") ?? ""; if (!id || !payload.recipe || !payload.glbBase64) { sendJson(res, 400, { error: "Expected { id, recipe, glbBase64 }" }); return; } await mkdir(assetsDir, { recursive: true }); const glbPath = resolve(assetsDir, `${id}.glb`); const recipePath = resolve(assetsDir, `${id}.recipe.json`); const glb = Buffer.from(payload.glbBase64, "base64"); await writeFile(glbPath, glb); await writeFile(recipePath, `${JSON.stringify(payload.recipe, null, 2)}\n`); sendJson(res, 200, { ok: true, glb: glbPath, recipe: recipePath, bytes: glb.byteLength }); return; } if (req.url === "/api/texture") { const raw = await readBody(req); const payload = JSON.parse(raw.toString("utf8")) as { recipe?: unknown; mode?: "guide" | "texture"; force?: boolean; albedoBase64?: string; }; if (!payload.recipe) { sendJson(res, 400, { error: "Expected { recipe, mode }" }); return; } const result = await runTextureCli({ recipe: payload.recipe, mode: payload.mode === "texture" ? "texture" : "guide", force: payload.force === true, ...(payload.albedoBase64 ? { albedoBase64: payload.albedoBase64 } : {}), }); sendJson(res, result["ok"] ? 200 : 500, result); return; } } catch (error) { const message = error instanceof Error ? error.message : String(error); sendJson(res, 500, { error: message }); return; } next(); }); }, }; } export default defineConfig({ root, plugins: [creatorApiPlugin()], resolve: { alias: { "@creator": toolsSrc, }, }, server: { port: 5174, open: false, fs: { allow: [workspaceRoot, dirname(toolsSrc)], }, }, build: { outDir: resolve(root, "dist"), emptyOutDir: true, target: "es2022", }, optimizeDeps: { include: [ "three", "three/examples/jsm/loaders/GLTFLoader.js", "three/examples/jsm/controls/OrbitControls.js", ], }, });