| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136 |
- /**
- * Vite plugin that exposes a POST endpoint for the timeline playground
- * to write CSS changes back to source files on disk.
- *
- * POST /__playground/apply-css
- * Body: { edits: Array<{ file: string; anchor: string; prop: string; value: string }> }
- *
- * For each edit the plugin finds `anchor` in the file, then locates the
- * next `prop: <anything>;` after it and replaces the value portion.
- * `file` is a basename resolved against the UI component packages.
- */
- import type { Plugin } from "vite"
- import type { IncomingMessage, ServerResponse } from "node:http"
- import fs from "node:fs"
- import path from "node:path"
- import { fileURLToPath } from "node:url"
- const here = path.dirname(fileURLToPath(import.meta.url))
- const roots = [path.resolve(here, "../../session-ui/src/components"), path.resolve(here, "../../ui/src/components")]
- const ENDPOINT = "/__playground/apply-css"
- type Edit = { file: string; anchor: string; prop: string; value: string }
- type Result = { file: string; prop: string; ok: boolean; error?: string }
- function applyEdits(content: string, edits: Edit[]): { content: string; results: Result[] } {
- const results: Result[] = []
- let out = content
- for (const edit of edits) {
- const name = edit.file
- const idx = out.indexOf(edit.anchor)
- if (idx === -1) {
- results.push({ file: name, prop: edit.prop, ok: false, error: `Anchor not found: ${edit.anchor.slice(0, 50)}` })
- continue
- }
- // From the anchor position, find the next occurrence of `prop: <value>`
- // We match `prop:` followed by any value up to `;`
- const after = out.slice(idx)
- const re = new RegExp(`(${escapeRegex(edit.prop)}\\s*:\\s*)([^;]+)(;)`)
- const match = re.exec(after)
- if (!match) {
- results.push({ file: name, prop: edit.prop, ok: false, error: `Property "${edit.prop}" not found after anchor` })
- continue
- }
- const start = idx + match.index + match[1].length
- const end = start + match[2].length
- out = out.slice(0, start) + edit.value + out.slice(end)
- results.push({ file: name, prop: edit.prop, ok: true })
- }
- return { content: out, results }
- }
- function escapeRegex(s: string) {
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
- }
- export function playgroundCss(): Plugin {
- return {
- name: "playground-css",
- configureServer(server) {
- server.middlewares.use((req: IncomingMessage, res: ServerResponse, next: () => void) => {
- if (req.url !== ENDPOINT) return next()
- if (req.method !== "POST") {
- res.statusCode = 405
- res.setHeader("Content-Type", "application/json")
- res.end(JSON.stringify({ error: "Method not allowed" }))
- return
- }
- let data = ""
- req.on("data", (chunk: Buffer) => {
- data += chunk.toString()
- })
- req.on("end", () => {
- let payload: { edits: Edit[] }
- try {
- payload = JSON.parse(data)
- } catch {
- res.statusCode = 400
- res.setHeader("Content-Type", "application/json")
- res.end(JSON.stringify({ error: "Invalid JSON" }))
- return
- }
- if (!Array.isArray(payload.edits)) {
- res.statusCode = 400
- res.setHeader("Content-Type", "application/json")
- res.end(JSON.stringify({ error: "Missing edits array" }))
- return
- }
- // Group by file
- const grouped = new Map<string, Edit[]>()
- for (const edit of payload.edits) {
- if (!edit.file || !edit.anchor || !edit.prop || edit.value === undefined) continue
- const abs = roots.map((root) => path.resolve(root, edit.file)).find((file) => fs.existsSync(file))
- if (!abs || !roots.some((root) => abs.startsWith(root))) continue
- const key = abs
- if (!grouped.has(key)) grouped.set(key, [])
- grouped.get(key)!.push(edit)
- }
- const results: Result[] = []
- for (const [abs, edits] of grouped) {
- const name = path.basename(abs)
- if (!fs.existsSync(abs)) {
- for (const e of edits) results.push({ file: name, prop: e.prop, ok: false, error: "File not found" })
- continue
- }
- try {
- const content = fs.readFileSync(abs, "utf-8")
- const applied = applyEdits(content, edits)
- results.push(...applied.results)
- if (applied.results.some((r) => r.ok)) {
- fs.writeFileSync(abs, applied.content, "utf-8")
- }
- } catch (err) {
- for (const e of edits) results.push({ file: name, prop: e.prop, ok: false, error: String(err) })
- }
- }
- res.statusCode = 200
- res.setHeader("Content-Type", "application/json")
- res.end(JSON.stringify({ results }))
- })
- })
- },
- }
- }
|