@upstart.gg/vite-plugins 0.1.61 → 0.1.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"upstart-editor-api.js","names":[],"sources":["../src/upstart-editor-api.ts"],"sourcesContent":["import MagicString from \"magic-string\";\nimport { parseSync } from \"oxc-parser\";\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport z from \"zod\";\nimport type { EditableEntry } from \"./vite-plugin-upstart-attrs\";\n\n/**\n * Escape user-typed text so it can be safely written as the BODY of a JS string\n * literal delimited by `quote` (\" ' or `). Escapes the delimiter, backslashes,\n * line terminators, and — for template literals — `${` interpolation starts.\n * The surrounding quotes themselves are NOT included.\n */\nexport function escapeStringLiteralBody(value: string, quote: string): string {\n let out = value.replace(/\\\\/g, \"\\\\\\\\\");\n if (quote === \"`\") {\n out = out.replace(/`/g, \"\\\\`\").replace(/\\$\\{/g, \"\\\\${\");\n } else {\n out = out.split(quote).join(\"\\\\\" + quote);\n }\n return out.replace(/\\n/g, \"\\\\n\").replace(/\\r/g, \"\\\\r\");\n}\n\ninterface AstNode {\n type: string;\n start: number;\n end: number;\n [key: string]: unknown;\n}\n\n/**\n * Locate the inline ArrayExpression that starts at `offset` by re-parsing the file.\n * Returns its bounds and element nodes (elisions become null), or null if not found.\n */\nfunction findArrayExpressionAt(\n code: string,\n filePath: string,\n offset: number,\n): { start: number; end: number; elements: (AstNode | null)[] } | null {\n const ast = parseSync(filePath, code, { sourceType: \"module\" });\n if (!ast.program) return null;\n\n let found: AstNode | null = null;\n const visit = (node: unknown): void => {\n if (found || !node || typeof node !== \"object\") return;\n if (Array.isArray(node)) {\n for (const child of node) visit(child);\n return;\n }\n const n = node as AstNode;\n if (n.type === \"ArrayExpression\" && n.start === offset) {\n found = n;\n return;\n }\n for (const key in n) {\n if (key === \"type\" || key === \"start\" || key === \"end\") continue;\n const value = n[key];\n if (value && typeof value === \"object\") visit(value);\n }\n };\n visit(ast.program);\n\n if (!found) return null;\n return {\n start: (found as AstNode).start,\n end: (found as AstNode).end,\n elements: ((found as AstNode).elements as (AstNode | null)[]) ?? [],\n };\n}\n\n/** Infer the quote char used by the array's string literals (defaults to \"). */\nfunction inferArrayQuote(code: string, elements: AstNode[]): string {\n for (const el of elements) {\n if (el.type === \"Literal\" && typeof el.value === \"string\") {\n const q = code[el.start];\n if (q === '\"' || q === \"'\" || q === \"`\") return q;\n }\n }\n return '\"';\n}\n\nexport const payloadEditText = z.object({\n action: z.literal(\"editText\"),\n language: z\n .string()\n .length(2)\n .regex(/^[a-z]{2}$/),\n namespace: z.string().regex(/^[a-z0-9_-]+$/),\n key: z.string().regex(/^[a-zA-Z0-9_.-]+$/),\n content: z.string(),\n});\n\nexport type PayloadEditText = z.infer<typeof payloadEditText>;\n\nexport const payloadEditTextDirect = z.object({\n action: z.literal(\"editTextDirect\"),\n id: z.string().min(1),\n content: z.string(),\n});\n\nexport type PayloadEditTextDirect = z.infer<typeof payloadEditTextDirect>;\n\nexport const payloadEditClassName = z.object({\n action: z.literal(\"editClassName\"),\n id: z.string().min(1),\n className: z.string(),\n});\n\nexport type PayloadEditClassName = z.infer<typeof payloadEditClassName>;\n\nexport const payloadEditImage = z.object({\n action: z.literal(\"editImage\"),\n id: z.string().min(1),\n // New src value to write into the <img> source, e.g. \"/images/fashion-10.webp\"\n src: z.string().min(1),\n});\n\nexport type PayloadEditImage = z.infer<typeof payloadEditImage>;\n\n// arrayId is \"<relativeFile>:<arrayStartOffset>\" — identifies the inline array literal.\nexport const payloadArrayItemAdd = z.object({\n action: z.literal(\"arrayItemAdd\"),\n arrayId: z.string().min(1),\n // Default content for the inserted element (the editor sends \"New item\").\n content: z.string().default(\"New item\"),\n});\n\nexport type PayloadArrayItemAdd = z.infer<typeof payloadArrayItemAdd>;\n\nexport const payloadArrayItemDelete = z.object({\n action: z.literal(\"arrayItemDelete\"),\n arrayId: z.string().min(1),\n index: z.number().int().min(0),\n});\n\nexport type PayloadArrayItemDelete = z.infer<typeof payloadArrayItemDelete>;\n\n// Replace the entire contents of an inline array literal in one edit — used by the\n// editor's batched \"apply\" (✓) control so a whole add/delete session is a single\n// source change + rebuild instead of one per item.\nexport const payloadArraySet = z.object({\n action: z.literal(\"arraySet\"),\n arrayId: z.string().min(1),\n items: z.array(z.string()).min(1),\n});\n\nexport type PayloadArraySet = z.infer<typeof payloadArraySet>;\n\nexport interface EditableRegistry {\n version: number;\n generatedAt: string;\n elements: Record<string, EditableEntry>;\n}\n\nexport type EditResult =\n | {\n success: true;\n error?: never;\n filePath: string;\n }\n | {\n success: false;\n error: string;\n filePath?: never;\n };\n\nexport class UpstartEditorAPI {\n private registry: EditableRegistry | null = null;\n private projectRoot: string;\n private registryPath: string;\n\n constructor(projectRoot: string, registryPath: string) {\n this.projectRoot = projectRoot;\n this.registryPath = registryPath;\n }\n\n /**\n * Load the registry from disk\n */\n async loadRegistry(): Promise<void> {\n const content = await fs.readFile(this.registryPath, \"utf-8\");\n this.registry = JSON.parse(content);\n }\n\n /**\n * Get the current registry (for testing/debugging)\n */\n getRegistry(): EditableRegistry | null {\n return this.registry;\n }\n\n /**\n * Set the registry directly (for testing)\n */\n setRegistry(registry: EditableRegistry): void {\n this.registry = registry;\n }\n\n /**\n * Edit a translation value in an i18next locale file.\n * Auto-detects flat keys (e.g. \"nav.home\" as literal key) vs nested keys (e.g. nav -> home).\n * Only updates existing keys — returns an error if the key is not found.\n */\n async editText(params: PayloadEditText): Promise<EditResult> {\n const parsed = payloadEditText.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { language, namespace, key, content: newContent } = parsed.data;\n const filePath = path.join(this.projectRoot, \"app\", \"locales\", language, `${namespace}.json`);\n\n let raw: string;\n try {\n raw = await fs.readFile(filePath, \"utf-8\");\n } catch (err) {\n return { success: false, error: `Failed to read locale file: ${filePath}` };\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(raw);\n } catch (err) {\n return { success: false, error: `Failed to parse locale file: ${filePath}` };\n }\n\n // Strategy 1: check for flat/literal key at top level\n if (key in data && typeof data[key] === \"string\") {\n data[key] = newContent;\n } else {\n // Strategy 2: nested traversal via dot notation\n const parts = key.split(\".\");\n let current: Record<string, unknown> = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n if (current[part] == null || typeof current[part] !== \"object\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current = current[part] as Record<string, unknown>;\n }\n\n const leafKey = parts[parts.length - 1];\n if (!(leafKey in current) || typeof current[leafKey] !== \"string\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current[leafKey] = newContent;\n }\n\n try {\n await fs.writeFile(filePath, JSON.stringify(data, null, 2) + \"\\n\");\n } catch (err) {\n return { success: false, error: `Failed to write locale file: ${filePath}` };\n }\n\n return { success: true, filePath };\n }\n\n /**\n * Edit a plain-text or rich-text JSX node directly in the source TSX file.\n * For rich-text entries, `content` should be the inner JSX/HTML of the element's children.\n */\n async editTextDirect(params: PayloadEditTextDirect): Promise<EditResult> {\n const parsed = payloadEditTextDirect.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { id, content } = parsed.data;\n if (!this.registry) {\n try {\n await this.loadRegistry();\n } catch (err) {\n return { success: false, error: `Failed to load registry: ${err}` };\n }\n }\n const entry = this.registry!.elements[id];\n if (!entry) {\n return { success: false, error: `Element ${id} not found in registry` };\n }\n if (entry.type !== \"text\" && entry.type !== \"rich-text\" && entry.type !== \"mixed-text\") {\n return { success: false, error: `Element ${id} is not a text element (type: ${entry.type})` };\n }\n if (entry.type === \"mixed-text\") {\n return this.applyMixedTextEdit(id, entry, content);\n }\n // Text entries that map to a JS string literal (e.g. inline array items) must be\n // escaped for the literal's quote style before being written between the quotes.\n const finalContent = entry.quote ? escapeStringLiteralBody(content, entry.quote) : content;\n return this.applyEdit(id, entry, finalContent);\n }\n\n /**\n * Edit the className of an element\n */\n async editClassName(params: PayloadEditClassName): Promise<EditResult> {\n const parsed = payloadEditClassName.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { id, className: newClassName } = parsed.data;\n if (!this.registry) {\n try {\n await this.loadRegistry();\n } catch (err) {\n return { success: false, error: `Failed to load registry: ${err}` };\n }\n }\n\n const entry = this.registry!.elements[id];\n if (!entry) {\n return { success: false, error: `Element ${id} not found in registry` };\n }\n\n if (entry.type !== \"className\") {\n return { success: false, error: `Element ${id} is not a className element (type: ${entry.type})` };\n }\n\n return this.applyEdit(id, entry, newClassName);\n }\n\n /**\n * Edit the `src` of an <img> element directly in the source TSX file.\n * The byte range points at the string literal between the quotes, so we only\n * rewrite the path itself. Copying the asset into the project is handled by\n * the caller (the sandbox server, which has bucket access).\n */\n async editImage(params: PayloadEditImage): Promise<EditResult> {\n const parsed = payloadEditImage.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { id, src } = parsed.data;\n if (!this.registry) {\n try {\n await this.loadRegistry();\n } catch (err) {\n return { success: false, error: `Failed to load registry: ${err}` };\n }\n }\n\n const entry = this.registry!.elements[id];\n if (!entry) {\n return { success: false, error: `Element ${id} not found in registry` };\n }\n\n if (entry.type !== \"image\") {\n return { success: false, error: `Element ${id} is not an image element (type: ${entry.type})` };\n }\n\n return this.applyEdit(id, entry, src);\n }\n\n /**\n * Add a new element to an inline array literal (e.g. `[\"a\", \"b\"]`), used by the\n * editor's \"+\" control on editable .map() lists. The inserted element matches\n * the quote style of the existing string elements and reuses their separator so\n * multi-line indentation is preserved.\n */\n async arrayItemAdd(params: PayloadArrayItemAdd): Promise<EditResult> {\n const parsed = payloadArrayItemAdd.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { arrayId, content } = parsed.data;\n const loc = this.resolveArrayId(arrayId);\n if (!loc) return { success: false, error: `Invalid arrayId: ${arrayId}` };\n\n try {\n const code = await fs.readFile(loc.filePath, \"utf-8\");\n const arr = findArrayExpressionAt(code, loc.relativeFile, loc.offset);\n if (!arr) {\n return { success: false, error: `Array not found at ${arrayId}. The file may have been modified.` };\n }\n const els = arr.elements.filter((e): e is AstNode => e !== null);\n const quote = inferArrayQuote(code, els);\n const literal = `${quote}${escapeStringLiteralBody(content, quote)}${quote}`;\n\n const s = new MagicString(code);\n if (els.length === 0) {\n s.appendLeft(arr.start + 1, literal);\n } else {\n const last = els[els.length - 1];\n // Reuse the separator between the last two elements (keeps multi-line\n // indentation); fall back to \", \" for a single-element array.\n const sep = els.length >= 2 ? code.slice(els[els.length - 2].end, last.start) : \", \";\n s.appendLeft(last.end, `${sep}${literal}`);\n }\n\n await fs.writeFile(loc.filePath, s.toString());\n // Element count changed — drop the cached registry so it reloads after the\n // dev pipeline re-transforms the file.\n this.registry = null;\n return { success: true, filePath: loc.filePath };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /**\n * Delete the element at `index` from an inline array literal. Refuses to remove\n * the last remaining element (which would leave an empty, un-addressable array).\n */\n async arrayItemDelete(params: PayloadArrayItemDelete): Promise<EditResult> {\n const parsed = payloadArrayItemDelete.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { arrayId, index } = parsed.data;\n const loc = this.resolveArrayId(arrayId);\n if (!loc) return { success: false, error: `Invalid arrayId: ${arrayId}` };\n\n try {\n const code = await fs.readFile(loc.filePath, \"utf-8\");\n const arr = findArrayExpressionAt(code, loc.relativeFile, loc.offset);\n if (!arr) {\n return { success: false, error: `Array not found at ${arrayId}. The file may have been modified.` };\n }\n const els = arr.elements.filter((e): e is AstNode => e !== null);\n if (index >= els.length) {\n return { success: false, error: `Index ${index} out of range for array at ${arrayId}` };\n }\n if (els.length <= 1) {\n return { success: false, error: \"Cannot delete the last remaining array item\" };\n }\n\n const s = new MagicString(code);\n const el = els[index];\n if (index > 0) {\n // Remove the separator before the element + the element itself.\n s.remove(els[index - 1].end, el.end);\n } else {\n // First element: remove it + the separator after it.\n s.remove(el.start, els[1].start);\n }\n\n await fs.writeFile(loc.filePath, s.toString());\n this.registry = null;\n return { success: true, filePath: loc.filePath };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /**\n * Replace the entire contents of an inline array literal with `items`, preserving\n * the source's quote style and multi-line indentation. Used by the editor's\n * batched ✓ apply so a whole add/delete session is a single edit + rebuild.\n */\n async arraySet(params: PayloadArraySet): Promise<EditResult> {\n const parsed = payloadArraySet.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { arrayId, items } = parsed.data;\n const loc = this.resolveArrayId(arrayId);\n if (!loc) return { success: false, error: `Invalid arrayId: ${arrayId}` };\n\n try {\n const code = await fs.readFile(loc.filePath, \"utf-8\");\n const arr = findArrayExpressionAt(code, loc.relativeFile, loc.offset);\n if (!arr) {\n return { success: false, error: `Array not found at ${arrayId}. The file may have been modified.` };\n }\n const els = arr.elements.filter((e): e is AstNode => e !== null);\n const quote = inferArrayQuote(code, els);\n const literal = (v: string) => `${quote}${escapeStringLiteralBody(v, quote)}${quote}`;\n\n const innerStart = arr.start + 1;\n const innerEnd = arr.end - 1;\n const innerSrc = code.slice(innerStart, innerEnd);\n\n let inner: string;\n if (innerSrc.includes(\"\\n\")) {\n // Multi-line: reuse the item indentation and the closing-bracket indent,\n // and keep a trailing comma (matches the common prettier/biome style).\n const itemIndent = innerSrc.match(/\\n([ \\t]*)\\S/)?.[1] ?? \" \";\n const outerIndent = innerSrc.match(/\\n([ \\t]*)$/)?.[1] ?? \"\";\n inner = `\\n${items.map((it) => itemIndent + literal(it)).join(\",\\n\")},\\n${outerIndent}`;\n } else {\n inner = items.map(literal).join(\", \");\n }\n\n const s = new MagicString(code);\n if (innerStart === innerEnd) {\n s.appendLeft(innerStart, inner);\n } else {\n s.overwrite(innerStart, innerEnd, inner);\n }\n await fs.writeFile(loc.filePath, s.toString());\n this.registry = null;\n return { success: true, filePath: loc.filePath };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /** Parse \"<relativeFile>:<offset>\" into an absolute path + numeric offset. */\n private resolveArrayId(arrayId: string): { filePath: string; relativeFile: string; offset: number } | null {\n const sep = arrayId.lastIndexOf(\":\");\n if (sep < 0) return null;\n const relativeFile = arrayId.slice(0, sep);\n const offset = Number(arrayId.slice(sep + 1));\n if (!relativeFile || !Number.isInteger(offset) || offset < 0) return null;\n return { filePath: path.join(this.projectRoot, relativeFile), relativeFile, offset };\n }\n\n /**\n * Apply an edit to a source file\n */\n private async applyEdit(id: string, entry: EditableEntry, newContent: string): Promise<EditResult> {\n const filePath = path.join(this.projectRoot, entry.file);\n\n try {\n const code = await fs.readFile(filePath, \"utf-8\");\n\n // Verify content at expected location\n const currentContent = code.slice(entry.startOffset, entry.endOffset);\n\n let actualStart = entry.startOffset;\n let actualEnd = entry.endOffset;\n\n if (currentContent !== entry.originalContent) {\n // Content has shifted - try to find it by searching. An empty original\n // can't be located by search (indexOf(\"\") === 0 would be a false match),\n // so treat it as not found rather than inserting at offset 0.\n const searchIndex = entry.originalContent !== \"\" ? code.indexOf(entry.originalContent) : -1;\n if (searchIndex === -1) {\n return {\n success: false,\n error: `Original content \"${entry.originalContent}\" not found in file ${entry.file}. The file may have been modified.`,\n };\n }\n actualStart = searchIndex;\n actualEnd = searchIndex + entry.originalContent.length;\n }\n\n // Apply the edit using MagicString. A zero-length range (e.g. an empty\n // string literal \"\") cannot be overwritten — insert the content instead.\n const s = new MagicString(code);\n if (actualStart === actualEnd) {\n s.appendLeft(actualStart, newContent);\n } else {\n s.overwrite(actualStart, actualEnd, newContent);\n }\n\n // Write the modified file\n await fs.writeFile(filePath, s.toString());\n\n // Calculate the length difference for offset adjustments\n const lengthDiff = newContent.length - entry.originalContent.length;\n\n // Update the registry entry\n entry.startOffset = actualStart;\n entry.endOffset = actualStart + newContent.length;\n entry.originalContent = newContent;\n\n // Shift all subsequent entries in the same file\n for (const [otherId, otherEntry] of Object.entries(this.registry!.elements)) {\n if (otherId !== id && otherEntry.file === entry.file && otherEntry.startOffset > actualStart) {\n otherEntry.startOffset += lengthDiff;\n otherEntry.endOffset += lengthDiff;\n }\n }\n\n // Save the updated registry\n await fs.writeFile(this.registryPath, JSON.stringify(this.registry, null, 2));\n\n return { success: true, filePath };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /**\n * Reconstruct JSX children from a user-edited template and the original expression segments,\n * then write the result back to the TSX source file.\n *\n * `template` uses `{{N}}` placeholders for expressions, e.g.:\n * \"© {{0}} Alex's Kitchen. All rights reserved.\"\n * The server replaces each placeholder with the original expression source from the registry.\n */\n private async applyMixedTextEdit(id: string, entry: EditableEntry, template: string): Promise<EditResult> {\n const exprSegments = (entry.segments ?? []).filter((s) => s.type === \"expr\");\n\n // Split template by {{N}} placeholders — odd indices are expression indices\n const parts = template.split(/\\{\\{(\\d+)\\}\\}/);\n\n // Reconstruct inner JSX: text parts interleaved with original expression sources\n let inner = \"\";\n for (let i = 0; i < parts.length; i++) {\n if (i % 2 === 0) {\n inner += parts[i];\n } else {\n const exprSeg = exprSegments[parseInt(parts[i])];\n inner += exprSeg ? exprSeg.raw : `{{${parts[i]}}}`;\n }\n }\n\n // Preserve leading/trailing JSX whitespace (indentation) from the original source\n const leadingWS = entry.originalContent.match(/^(\\s*)/)?.[1] ?? \"\";\n const trailingWS = entry.originalContent.match(/(\\s*)$/)?.[1] ?? \"\";\n const newContent = leadingWS + inner + trailingWS;\n\n return this.applyEdit(id, entry, newContent);\n }\n\n /**\n * Get element info by ID\n */\n getElement(id: string): EditableEntry | undefined {\n return this.registry?.elements[id];\n }\n\n /**\n * Get all elements of a specific type\n */\n getElementsByType(type: \"text\" | \"className\"): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.type === type) {\n result[id] = entry;\n }\n }\n return result;\n }\n\n /**\n * Get all elements in a specific file\n */\n getElementsByFile(file: string): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.file === file) {\n result[id] = entry;\n }\n }\n return result;\n }\n}\n"],"mappings":";;;;;;;;;;;;AAaA,SAAgB,wBAAwB,OAAe,OAAuB;CAC5E,IAAI,MAAM,MAAM,QAAQ,OAAO,OAAO;CACtC,IAAI,UAAU,KACZ,MAAM,IAAI,QAAQ,MAAM,MAAM,CAAC,QAAQ,SAAS,OAAO;MAEvD,MAAM,IAAI,MAAM,MAAM,CAAC,KAAK,OAAO,MAAM;CAE3C,OAAO,IAAI,QAAQ,OAAO,MAAM,CAAC,QAAQ,OAAO,MAAM;;;;;;AAcxD,SAAS,sBACP,MACA,UACA,QACqE;CACrE,MAAM,MAAM,UAAU,UAAU,MAAM,EAAE,YAAY,UAAU,CAAC;CAC/D,IAAI,CAAC,IAAI,SAAS,OAAO;CAEzB,IAAI,QAAwB;CAC5B,MAAM,SAAS,SAAwB;EACrC,IAAI,SAAS,CAAC,QAAQ,OAAO,SAAS,UAAU;EAChD,IAAI,MAAM,QAAQ,KAAK,EAAE;GACvB,KAAK,MAAM,SAAS,MAAM,MAAM,MAAM;GACtC;;EAEF,MAAM,IAAI;EACV,IAAI,EAAE,SAAS,qBAAqB,EAAE,UAAU,QAAQ;GACtD,QAAQ;GACR;;EAEF,KAAK,MAAM,OAAO,GAAG;GACnB,IAAI,QAAQ,UAAU,QAAQ,WAAW,QAAQ,OAAO;GACxD,MAAM,QAAQ,EAAE;GAChB,IAAI,SAAS,OAAO,UAAU,UAAU,MAAM,MAAM;;;CAGxD,MAAM,IAAI,QAAQ;CAElB,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO;EACL,OAAQ,MAAkB;EAC1B,KAAM,MAAkB;EACxB,UAAY,MAAkB,YAAmC,EAAE;EACpE;;;AAIH,SAAS,gBAAgB,MAAc,UAA6B;CAClE,KAAK,MAAM,MAAM,UACf,IAAI,GAAG,SAAS,aAAa,OAAO,GAAG,UAAU,UAAU;EACzD,MAAM,IAAI,KAAK,GAAG;EAClB,IAAI,MAAM,QAAO,MAAM,OAAO,MAAM,KAAK,OAAO;;CAGpD,OAAO;;AAGT,MAAa,kBAAkB,EAAE,OAAO;CACtC,QAAQ,EAAE,QAAQ,WAAW;CAC7B,UAAU,EACP,QAAQ,CACR,OAAO,EAAE,CACT,MAAM,aAAa;CACtB,WAAW,EAAE,QAAQ,CAAC,MAAM,gBAAgB;CAC5C,KAAK,EAAE,QAAQ,CAAC,MAAM,oBAAoB;CAC1C,SAAS,EAAE,QAAQ;CACpB,CAAC;AAIF,MAAa,wBAAwB,EAAE,OAAO;CAC5C,QAAQ,EAAE,QAAQ,iBAAiB;CACnC,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE;CACrB,SAAS,EAAE,QAAQ;CACpB,CAAC;AAIF,MAAa,uBAAuB,EAAE,OAAO;CAC3C,QAAQ,EAAE,QAAQ,gBAAgB;CAClC,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE;CACrB,WAAW,EAAE,QAAQ;CACtB,CAAC;AAIF,MAAa,mBAAmB,EAAE,OAAO;CACvC,QAAQ,EAAE,QAAQ,YAAY;CAC9B,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE;CAErB,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE;CACvB,CAAC;AAKF,MAAa,sBAAsB,EAAE,OAAO;CAC1C,QAAQ,EAAE,QAAQ,eAAe;CACjC,SAAS,EAAE,QAAQ,CAAC,IAAI,EAAE;CAE1B,SAAS,EAAE,QAAQ,CAAC,QAAQ,WAAW;CACxC,CAAC;AAIF,MAAa,yBAAyB,EAAE,OAAO;CAC7C,QAAQ,EAAE,QAAQ,kBAAkB;CACpC,SAAS,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC1B,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE;CAC/B,CAAC;AAOF,MAAa,kBAAkB,EAAE,OAAO;CACtC,QAAQ,EAAE,QAAQ,WAAW;CAC7B,SAAS,EAAE,QAAQ,CAAC,IAAI,EAAE;CAC1B,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,IAAI,EAAE;CAClC,CAAC;AAsBF,IAAa,mBAAb,MAA8B;CAC5B,WAA4C;CAC5C;CACA;CAEA,YAAY,aAAqB,cAAsB;EACrD,KAAK,cAAc;EACnB,KAAK,eAAe;;;;;CAMtB,MAAM,eAA8B;EAClC,MAAM,UAAU,MAAM,GAAG,SAAS,KAAK,cAAc,QAAQ;EAC7D,KAAK,WAAW,KAAK,MAAM,QAAQ;;;;;CAMrC,cAAuC;EACrC,OAAO,KAAK;;;;;CAMd,YAAY,UAAkC;EAC5C,KAAK,WAAW;;;;;;;CAQlB,MAAM,SAAS,QAA8C;EAC3D,MAAM,SAAS,gBAAgB,UAAU,OAAO;EAChD,IAAI,CAAC,OAAO,SACV,OAAO;GAAE,SAAS;GAAO,OAAO,oBAAoB,OAAO,MAAM;GAAW;EAE9E,MAAM,EAAE,UAAU,WAAW,KAAK,SAAS,eAAe,OAAO;EACjE,MAAM,WAAW,KAAK,KAAK,KAAK,aAAa,OAAO,WAAW,UAAU,GAAG,UAAU,OAAO;EAE7F,IAAI;EACJ,IAAI;GACF,MAAM,MAAM,GAAG,SAAS,UAAU,QAAQ;WACnC,KAAK;GACZ,OAAO;IAAE,SAAS;IAAO,OAAO,+BAA+B;IAAY;;EAG7E,IAAI;EACJ,IAAI;GACF,OAAO,KAAK,MAAM,IAAI;WACf,KAAK;GACZ,OAAO;IAAE,SAAS;IAAO,OAAO,gCAAgC;IAAY;;EAI9E,IAAI,OAAO,QAAQ,OAAO,KAAK,SAAS,UACtC,KAAK,OAAO;OACP;GAEL,MAAM,QAAQ,IAAI,MAAM,IAAI;GAC5B,IAAI,UAAmC;GACvC,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,SAAS,GAAG,KAAK;IACzC,MAAM,OAAO,MAAM;IACnB,IAAI,QAAQ,SAAS,QAAQ,OAAO,QAAQ,UAAU,UACpD,OAAO;KAAE,SAAS;KAAO,OAAO,QAAQ,IAAI,6BAA6B;KAAY;IAEvF,UAAU,QAAQ;;GAGpB,MAAM,UAAU,MAAM,MAAM,SAAS;GACrC,IAAI,EAAE,WAAW,YAAY,OAAO,QAAQ,aAAa,UACvD,OAAO;IAAE,SAAS;IAAO,OAAO,QAAQ,IAAI,6BAA6B;IAAY;GAEvF,QAAQ,WAAW;;EAGrB,IAAI;GACF,MAAM,GAAG,UAAU,UAAU,KAAK,UAAU,MAAM,MAAM,EAAE,GAAG,KAAK;WAC3D,KAAK;GACZ,OAAO;IAAE,SAAS;IAAO,OAAO,gCAAgC;IAAY;;EAG9E,OAAO;GAAE,SAAS;GAAM;GAAU;;;;;;CAOpC,MAAM,eAAe,QAAoD;EACvE,MAAM,SAAS,sBAAsB,UAAU,OAAO;EACtD,IAAI,CAAC,OAAO,SACV,OAAO;GAAE,SAAS;GAAO,OAAO,oBAAoB,OAAO,MAAM;GAAW;EAE9E,MAAM,EAAE,IAAI,YAAY,OAAO;EAC/B,IAAI,CAAC,KAAK,UACR,IAAI;GACF,MAAM,KAAK,cAAc;WAClB,KAAK;GACZ,OAAO;IAAE,SAAS;IAAO,OAAO,4BAA4B;IAAO;;EAGvE,MAAM,QAAQ,KAAK,SAAU,SAAS;EACtC,IAAI,CAAC,OACH,OAAO;GAAE,SAAS;GAAO,OAAO,WAAW,GAAG;GAAyB;EAEzE,IAAI,MAAM,SAAS,UAAU,MAAM,SAAS,eAAe,MAAM,SAAS,cACxE,OAAO;GAAE,SAAS;GAAO,OAAO,WAAW,GAAG,gCAAgC,MAAM,KAAK;GAAI;EAE/F,IAAI,MAAM,SAAS,cACjB,OAAO,KAAK,mBAAmB,IAAI,OAAO,QAAQ;EAIpD,MAAM,eAAe,MAAM,QAAQ,wBAAwB,SAAS,MAAM,MAAM,GAAG;EACnF,OAAO,KAAK,UAAU,IAAI,OAAO,aAAa;;;;;CAMhD,MAAM,cAAc,QAAmD;EACrE,MAAM,SAAS,qBAAqB,UAAU,OAAO;EACrD,IAAI,CAAC,OAAO,SACV,OAAO;GAAE,SAAS;GAAO,OAAO,oBAAoB,OAAO,MAAM;GAAW;EAE9E,MAAM,EAAE,IAAI,WAAW,iBAAiB,OAAO;EAC/C,IAAI,CAAC,KAAK,UACR,IAAI;GACF,MAAM,KAAK,cAAc;WAClB,KAAK;GACZ,OAAO;IAAE,SAAS;IAAO,OAAO,4BAA4B;IAAO;;EAIvE,MAAM,QAAQ,KAAK,SAAU,SAAS;EACtC,IAAI,CAAC,OACH,OAAO;GAAE,SAAS;GAAO,OAAO,WAAW,GAAG;GAAyB;EAGzE,IAAI,MAAM,SAAS,aACjB,OAAO;GAAE,SAAS;GAAO,OAAO,WAAW,GAAG,qCAAqC,MAAM,KAAK;GAAI;EAGpG,OAAO,KAAK,UAAU,IAAI,OAAO,aAAa;;;;;;;;CAShD,MAAM,UAAU,QAA+C;EAC7D,MAAM,SAAS,iBAAiB,UAAU,OAAO;EACjD,IAAI,CAAC,OAAO,SACV,OAAO;GAAE,SAAS;GAAO,OAAO,oBAAoB,OAAO,MAAM;GAAW;EAE9E,MAAM,EAAE,IAAI,QAAQ,OAAO;EAC3B,IAAI,CAAC,KAAK,UACR,IAAI;GACF,MAAM,KAAK,cAAc;WAClB,KAAK;GACZ,OAAO;IAAE,SAAS;IAAO,OAAO,4BAA4B;IAAO;;EAIvE,MAAM,QAAQ,KAAK,SAAU,SAAS;EACtC,IAAI,CAAC,OACH,OAAO;GAAE,SAAS;GAAO,OAAO,WAAW,GAAG;GAAyB;EAGzE,IAAI,MAAM,SAAS,SACjB,OAAO;GAAE,SAAS;GAAO,OAAO,WAAW,GAAG,kCAAkC,MAAM,KAAK;GAAI;EAGjG,OAAO,KAAK,UAAU,IAAI,OAAO,IAAI;;;;;;;;CASvC,MAAM,aAAa,QAAkD;EACnE,MAAM,SAAS,oBAAoB,UAAU,OAAO;EACpD,IAAI,CAAC,OAAO,SACV,OAAO;GAAE,SAAS;GAAO,OAAO,oBAAoB,OAAO,MAAM;GAAW;EAE9E,MAAM,EAAE,SAAS,YAAY,OAAO;EACpC,MAAM,MAAM,KAAK,eAAe,QAAQ;EACxC,IAAI,CAAC,KAAK,OAAO;GAAE,SAAS;GAAO,OAAO,oBAAoB;GAAW;EAEzE,IAAI;GACF,MAAM,OAAO,MAAM,GAAG,SAAS,IAAI,UAAU,QAAQ;GACrD,MAAM,MAAM,sBAAsB,MAAM,IAAI,cAAc,IAAI,OAAO;GACrE,IAAI,CAAC,KACH,OAAO;IAAE,SAAS;IAAO,OAAO,sBAAsB,QAAQ;IAAqC;GAErG,MAAM,MAAM,IAAI,SAAS,QAAQ,MAAoB,MAAM,KAAK;GAChE,MAAM,QAAQ,gBAAgB,MAAM,IAAI;GACxC,MAAM,UAAU,GAAG,QAAQ,wBAAwB,SAAS,MAAM,GAAG;GAErE,MAAM,IAAI,IAAI,YAAY,KAAK;GAC/B,IAAI,IAAI,WAAW,GACjB,EAAE,WAAW,IAAI,QAAQ,GAAG,QAAQ;QAC/B;IACL,MAAM,OAAO,IAAI,IAAI,SAAS;IAG9B,MAAM,MAAM,IAAI,UAAU,IAAI,KAAK,MAAM,IAAI,IAAI,SAAS,GAAG,KAAK,KAAK,MAAM,GAAG;IAChF,EAAE,WAAW,KAAK,KAAK,GAAG,MAAM,UAAU;;GAG5C,MAAM,GAAG,UAAU,IAAI,UAAU,EAAE,UAAU,CAAC;GAG9C,KAAK,WAAW;GAChB,OAAO;IAAE,SAAS;IAAM,UAAU,IAAI;IAAU;WACzC,KAAK;GACZ,OAAO;IAAE,SAAS;IAAO,OAAO,OAAO,IAAI;IAAE;;;;;;;CAQjD,MAAM,gBAAgB,QAAqD;EACzE,MAAM,SAAS,uBAAuB,UAAU,OAAO;EACvD,IAAI,CAAC,OAAO,SACV,OAAO;GAAE,SAAS;GAAO,OAAO,oBAAoB,OAAO,MAAM;GAAW;EAE9E,MAAM,EAAE,SAAS,UAAU,OAAO;EAClC,MAAM,MAAM,KAAK,eAAe,QAAQ;EACxC,IAAI,CAAC,KAAK,OAAO;GAAE,SAAS;GAAO,OAAO,oBAAoB;GAAW;EAEzE,IAAI;GACF,MAAM,OAAO,MAAM,GAAG,SAAS,IAAI,UAAU,QAAQ;GACrD,MAAM,MAAM,sBAAsB,MAAM,IAAI,cAAc,IAAI,OAAO;GACrE,IAAI,CAAC,KACH,OAAO;IAAE,SAAS;IAAO,OAAO,sBAAsB,QAAQ;IAAqC;GAErG,MAAM,MAAM,IAAI,SAAS,QAAQ,MAAoB,MAAM,KAAK;GAChE,IAAI,SAAS,IAAI,QACf,OAAO;IAAE,SAAS;IAAO,OAAO,SAAS,MAAM,6BAA6B;IAAW;GAEzF,IAAI,IAAI,UAAU,GAChB,OAAO;IAAE,SAAS;IAAO,OAAO;IAA+C;GAGjF,MAAM,IAAI,IAAI,YAAY,KAAK;GAC/B,MAAM,KAAK,IAAI;GACf,IAAI,QAAQ,GAEV,EAAE,OAAO,IAAI,QAAQ,GAAG,KAAK,GAAG,IAAI;QAGpC,EAAE,OAAO,GAAG,OAAO,IAAI,GAAG,MAAM;GAGlC,MAAM,GAAG,UAAU,IAAI,UAAU,EAAE,UAAU,CAAC;GAC9C,KAAK,WAAW;GAChB,OAAO;IAAE,SAAS;IAAM,UAAU,IAAI;IAAU;WACzC,KAAK;GACZ,OAAO;IAAE,SAAS;IAAO,OAAO,OAAO,IAAI;IAAE;;;;;;;;CASjD,MAAM,SAAS,QAA8C;EAC3D,MAAM,SAAS,gBAAgB,UAAU,OAAO;EAChD,IAAI,CAAC,OAAO,SACV,OAAO;GAAE,SAAS;GAAO,OAAO,oBAAoB,OAAO,MAAM;GAAW;EAE9E,MAAM,EAAE,SAAS,UAAU,OAAO;EAClC,MAAM,MAAM,KAAK,eAAe,QAAQ;EACxC,IAAI,CAAC,KAAK,OAAO;GAAE,SAAS;GAAO,OAAO,oBAAoB;GAAW;EAEzE,IAAI;GACF,MAAM,OAAO,MAAM,GAAG,SAAS,IAAI,UAAU,QAAQ;GACrD,MAAM,MAAM,sBAAsB,MAAM,IAAI,cAAc,IAAI,OAAO;GACrE,IAAI,CAAC,KACH,OAAO;IAAE,SAAS;IAAO,OAAO,sBAAsB,QAAQ;IAAqC;GAGrG,MAAM,QAAQ,gBAAgB,MADlB,IAAI,SAAS,QAAQ,MAAoB,MAAM,KACpB,CAAC;GACxC,MAAM,WAAW,MAAc,GAAG,QAAQ,wBAAwB,GAAG,MAAM,GAAG;GAE9E,MAAM,aAAa,IAAI,QAAQ;GAC/B,MAAM,WAAW,IAAI,MAAM;GAC3B,MAAM,WAAW,KAAK,MAAM,YAAY,SAAS;GAEjD,IAAI;GACJ,IAAI,SAAS,SAAS,KAAK,EAAE;IAG3B,MAAM,aAAa,SAAS,MAAM,eAAe,GAAG,MAAM;IAC1D,MAAM,cAAc,SAAS,MAAM,cAAc,GAAG,MAAM;IAC1D,QAAQ,KAAK,MAAM,KAAK,OAAO,aAAa,QAAQ,GAAG,CAAC,CAAC,KAAK,MAAM,CAAC,KAAK;UAE1E,QAAQ,MAAM,IAAI,QAAQ,CAAC,KAAK,KAAK;GAGvC,MAAM,IAAI,IAAI,YAAY,KAAK;GAC/B,IAAI,eAAe,UACjB,EAAE,WAAW,YAAY,MAAM;QAE/B,EAAE,UAAU,YAAY,UAAU,MAAM;GAE1C,MAAM,GAAG,UAAU,IAAI,UAAU,EAAE,UAAU,CAAC;GAC9C,KAAK,WAAW;GAChB,OAAO;IAAE,SAAS;IAAM,UAAU,IAAI;IAAU;WACzC,KAAK;GACZ,OAAO;IAAE,SAAS;IAAO,OAAO,OAAO,IAAI;IAAE;;;;CAKjD,eAAuB,SAAoF;EACzG,MAAM,MAAM,QAAQ,YAAY,IAAI;EACpC,IAAI,MAAM,GAAG,OAAO;EACpB,MAAM,eAAe,QAAQ,MAAM,GAAG,IAAI;EAC1C,MAAM,SAAS,OAAO,QAAQ,MAAM,MAAM,EAAE,CAAC;EAC7C,IAAI,CAAC,gBAAgB,CAAC,OAAO,UAAU,OAAO,IAAI,SAAS,GAAG,OAAO;EACrE,OAAO;GAAE,UAAU,KAAK,KAAK,KAAK,aAAa,aAAa;GAAE;GAAc;GAAQ;;;;;CAMtF,MAAc,UAAU,IAAY,OAAsB,YAAyC;EACjG,MAAM,WAAW,KAAK,KAAK,KAAK,aAAa,MAAM,KAAK;EAExD,IAAI;GACF,MAAM,OAAO,MAAM,GAAG,SAAS,UAAU,QAAQ;GAGjD,MAAM,iBAAiB,KAAK,MAAM,MAAM,aAAa,MAAM,UAAU;GAErE,IAAI,cAAc,MAAM;GACxB,IAAI,YAAY,MAAM;GAEtB,IAAI,mBAAmB,MAAM,iBAAiB;IAI5C,MAAM,cAAc,MAAM,oBAAoB,KAAK,KAAK,QAAQ,MAAM,gBAAgB,GAAG;IACzF,IAAI,gBAAgB,IAClB,OAAO;KACL,SAAS;KACT,OAAO,qBAAqB,MAAM,gBAAgB,sBAAsB,MAAM,KAAK;KACpF;IAEH,cAAc;IACd,YAAY,cAAc,MAAM,gBAAgB;;GAKlD,MAAM,IAAI,IAAI,YAAY,KAAK;GAC/B,IAAI,gBAAgB,WAClB,EAAE,WAAW,aAAa,WAAW;QAErC,EAAE,UAAU,aAAa,WAAW,WAAW;GAIjD,MAAM,GAAG,UAAU,UAAU,EAAE,UAAU,CAAC;GAG1C,MAAM,aAAa,WAAW,SAAS,MAAM,gBAAgB;GAG7D,MAAM,cAAc;GACpB,MAAM,YAAY,cAAc,WAAW;GAC3C,MAAM,kBAAkB;GAGxB,KAAK,MAAM,CAAC,SAAS,eAAe,OAAO,QAAQ,KAAK,SAAU,SAAS,EACzE,IAAI,YAAY,MAAM,WAAW,SAAS,MAAM,QAAQ,WAAW,cAAc,aAAa;IAC5F,WAAW,eAAe;IAC1B,WAAW,aAAa;;GAK5B,MAAM,GAAG,UAAU,KAAK,cAAc,KAAK,UAAU,KAAK,UAAU,MAAM,EAAE,CAAC;GAE7E,OAAO;IAAE,SAAS;IAAM;IAAU;WAC3B,KAAK;GACZ,OAAO;IAAE,SAAS;IAAO,OAAO,OAAO,IAAI;IAAE;;;;;;;;;;;CAYjD,MAAc,mBAAmB,IAAY,OAAsB,UAAuC;EACxG,MAAM,gBAAgB,MAAM,YAAY,EAAE,EAAE,QAAQ,MAAM,EAAE,SAAS,OAAO;EAG5E,MAAM,QAAQ,SAAS,MAAM,gBAAgB;EAG7C,IAAI,QAAQ;EACZ,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAChC,IAAI,IAAI,MAAM,GACZ,SAAS,MAAM;OACV;GACL,MAAM,UAAU,aAAa,SAAS,MAAM,GAAG;GAC/C,SAAS,UAAU,QAAQ,MAAM,KAAK,MAAM,GAAG;;EAKnD,MAAM,YAAY,MAAM,gBAAgB,MAAM,SAAS,GAAG,MAAM;EAChE,MAAM,aAAa,MAAM,gBAAgB,MAAM,SAAS,GAAG,MAAM;EACjE,MAAM,aAAa,YAAY,QAAQ;EAEvC,OAAO,KAAK,UAAU,IAAI,OAAO,WAAW;;;;;CAM9C,WAAW,IAAuC;EAChD,OAAO,KAAK,UAAU,SAAS;;;;;CAMjC,kBAAkB,MAA2D;EAC3E,IAAI,CAAC,KAAK,UACR,OAAO,EAAE;EAGX,MAAM,SAAwC,EAAE;EAChD,KAAK,MAAM,CAAC,IAAI,UAAU,OAAO,QAAQ,KAAK,SAAS,SAAS,EAC9D,IAAI,MAAM,SAAS,MACjB,OAAO,MAAM;EAGjB,OAAO;;;;;CAMT,kBAAkB,MAA6C;EAC7D,IAAI,CAAC,KAAK,UACR,OAAO,EAAE;EAGX,MAAM,SAAwC,EAAE;EAChD,KAAK,MAAM,CAAC,IAAI,UAAU,OAAO,QAAQ,KAAK,SAAS,SAAS,EAC9D,IAAI,MAAM,SAAS,MACjB,OAAO,MAAM;EAGjB,OAAO"}
1
+ {"version":3,"file":"upstart-editor-api.js","names":[],"sources":["../src/upstart-editor-api.ts"],"sourcesContent":["import MagicString from \"magic-string\";\nimport { parseSync } from \"oxc-parser\";\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport z from \"zod\";\nimport type { EditableEntry } from \"./vite-plugin-upstart-attrs\";\nimport { analyzeRouteMeta, type MetaElement, type MetaSegment, type ModuleLoader } from \"./page-meta\";\nimport { ensureRootSiteMeta } from \"./site-meta\";\n\n/**\n * Escape user-typed text so it can be safely written as the BODY of a JS string\n * literal delimited by `quote` (\" ' or `). Escapes the delimiter, backslashes,\n * line terminators, and — for template literals — `${` interpolation starts.\n * The surrounding quotes themselves are NOT included.\n */\nexport function escapeStringLiteralBody(value: string, quote: string): string {\n let out = value.replace(/\\\\/g, \"\\\\\\\\\");\n if (quote === \"`\") {\n out = out.replace(/`/g, \"\\\\`\").replace(/\\$\\{/g, \"\\\\${\");\n } else {\n out = out.split(quote).join(\"\\\\\" + quote);\n }\n return out.replace(/\\n/g, \"\\\\n\").replace(/\\r/g, \"\\\\r\");\n}\n\ninterface AstNode {\n type: string;\n start: number;\n end: number;\n [key: string]: unknown;\n}\n\n/**\n * Locate the inline ArrayExpression that starts at `offset` by re-parsing the file.\n * Returns its bounds and element nodes (elisions become null), or null if not found.\n */\nfunction findArrayExpressionAt(\n code: string,\n filePath: string,\n offset: number,\n): { start: number; end: number; elements: (AstNode | null)[] } | null {\n const ast = parseSync(filePath, code, { sourceType: \"module\" });\n if (!ast.program) return null;\n\n let found: AstNode | null = null;\n const visit = (node: unknown): void => {\n if (found || !node || typeof node !== \"object\") return;\n if (Array.isArray(node)) {\n for (const child of node) visit(child);\n return;\n }\n const n = node as AstNode;\n if (n.type === \"ArrayExpression\" && n.start === offset) {\n found = n;\n return;\n }\n for (const key in n) {\n if (key === \"type\" || key === \"start\" || key === \"end\") continue;\n const value = n[key];\n if (value && typeof value === \"object\") visit(value);\n }\n };\n visit(ast.program);\n\n if (!found) return null;\n return {\n start: (found as AstNode).start,\n end: (found as AstNode).end,\n elements: ((found as AstNode).elements as (AstNode | null)[]) ?? [],\n };\n}\n\n/** Infer the quote char used by the array's string literals (defaults to \"). */\nfunction inferArrayQuote(code: string, elements: AstNode[]): string {\n for (const el of elements) {\n if (el.type === \"Literal\" && typeof el.value === \"string\") {\n const q = code[el.start];\n if (q === '\"' || q === \"'\" || q === \"`\") return q;\n }\n }\n return '\"';\n}\n\n/** One `{ key: \"value\" }` entry of a route's `meta` array, keys in source order. */\ntype StaticMetaEntry = { key: string; value: string }[];\n\ntype MetaExportLookup =\n | { kind: \"none\" }\n | { kind: \"dynamic\"; reason: string }\n | { kind: \"static\"; arrayStart: number; arrayEnd: number; entries: StaticMetaEntry[] };\n\n/** Read the ArrayExpression a `meta` export resolves to, or explain why we can't. */\nfunction readMetaArray(node: AstNode): { ok: true; array: AstNode } | { ok: false; reason: string } {\n if (node.type === \"ArrayExpression\") return { ok: true, array: node };\n if (node.type === \"ArrowFunctionExpression\" || node.type === \"FunctionExpression\") {\n const body = node.body as AstNode;\n if (body.type === \"ArrayExpression\") return { ok: true, array: body };\n if (body.type === \"BlockStatement\") {\n const statements = (body.body as AstNode[]) ?? [];\n const ret = statements.find((st) => st.type === \"ReturnStatement\");\n const argument = ret?.argument as AstNode | null | undefined;\n if (argument?.type === \"ArrayExpression\") return { ok: true, array: argument };\n }\n }\n return { ok: false, reason: \"The page meta is computed by code\" };\n}\n\n/** Turn a static array of object literals into plain key/value entries, or null if any part is dynamic. */\nfunction readStaticEntries(elements: (AstNode | null)[]): StaticMetaEntry[] | null {\n const entries: StaticMetaEntry[] = [];\n for (const element of elements) {\n if (!element || element.type !== \"ObjectExpression\") return null;\n const entry: StaticMetaEntry = [];\n for (const prop of (element.properties as AstNode[]) ?? []) {\n if (prop.type !== \"Property\" || prop.computed || prop.shorthand || prop.kind !== \"init\") return null;\n const key = prop.key as AstNode;\n const value = prop.value as AstNode;\n const keyName =\n key.type === \"Identifier\"\n ? (key.name as string)\n : key.type === \"Literal\" && typeof key.value === \"string\"\n ? key.value\n : null;\n if (keyName === null) return null;\n if (value.type !== \"Literal\" || typeof value.value !== \"string\") return null;\n entry.push({ key: keyName, value: value.value });\n }\n entries.push(entry);\n }\n return entries;\n}\n\n/** Locate the route's `meta` export and read it if it is fully static. */\nfunction findMetaExport(code: string, filePath: string): MetaExportLookup {\n const ast = parseSync(filePath, code, { sourceType: \"module\" });\n const body = (ast.program?.body as AstNode[] | undefined) ?? [];\n\n for (const statement of body) {\n if (statement.type !== \"ExportNamedDeclaration\") continue;\n const declaration = statement.declaration as AstNode | null;\n\n // export { meta } / export { x as meta } — we can't follow the reference.\n if (!declaration) {\n const specifiers = (statement.specifiers as AstNode[]) ?? [];\n const exported = specifiers.some((spec) => {\n const name = spec.exported as AstNode | undefined;\n return name?.type === \"Identifier\" && name.name === \"meta\";\n });\n if (exported) return { kind: \"dynamic\", reason: \"The page meta is exported indirectly\" };\n continue;\n }\n\n let source: AstNode | null = null;\n if (declaration.type === \"VariableDeclaration\") {\n for (const decl of (declaration.declarations as AstNode[]) ?? []) {\n const id = decl.id as AstNode;\n if (id.type === \"Identifier\" && id.name === \"meta\") source = (decl.init as AstNode) ?? null;\n }\n } else if (declaration.type === \"FunctionDeclaration\") {\n const id = declaration.id as AstNode | null;\n if (id?.type === \"Identifier\" && id.name === \"meta\") source = declaration;\n }\n if (!source) continue;\n\n const array = readMetaArray(source);\n if (!array.ok) return { kind: \"dynamic\", reason: array.reason };\n\n const entries = readStaticEntries((array.array.elements as (AstNode | null)[]) ?? []);\n if (!entries) return { kind: \"dynamic\", reason: \"The page meta contains dynamic values\" };\n\n return { kind: \"static\", arrayStart: array.array.start, arrayEnd: array.array.end, entries };\n }\n\n return { kind: \"none\" };\n}\n\n/** Offset right after the last top-level import, or the end of the file when there is none. */\nfunction findMetaInsertOffset(code: string, filePath: string): number {\n const ast = parseSync(filePath, code, { sourceType: \"module\" });\n const body = (ast.program?.body as AstNode[] | undefined) ?? [];\n let offset: number | null = null;\n for (const statement of body) {\n if (statement.type === \"ImportDeclaration\") offset = statement.end;\n }\n return offset ?? code.length;\n}\n\n/** Serialize entries back to source, one object literal per line. */\nfunction printMetaArray(entries: StaticMetaEntry[], indent: string): string {\n if (entries.length === 0) return \"[]\";\n const lines = entries.map((entry) => {\n const props = entry\n .map(({ key, value }) => {\n const printedKey = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)\n ? key\n : `\"${escapeStringLiteralBody(key, '\"')}\"`;\n return `${printedKey}: \"${escapeStringLiteralBody(value, '\"')}\"`;\n })\n .join(\", \");\n return `${indent} { ${props} },`;\n });\n return `[\\n${lines.join(\"\\n\")}\\n${indent}]`;\n}\n\n/** Read the value of `{ name: <name>, content: X }` (or `{ title: X }` when name is \"title\"). */\nfunction findMetaValue(entries: StaticMetaEntry[], name: string): string | null {\n for (const entry of entries) {\n if (name === \"title\") {\n const title = entry.find((p) => p.key === \"title\");\n if (title && entry.length === 1) return title.value;\n continue;\n }\n if (entry.some((p) => p.key === \"name\" && p.value === name)) {\n return entry.find((p) => p.key === \"content\")?.value ?? null;\n }\n }\n return null;\n}\n\n/**\n * Update/insert/remove the entry for `name`, leaving every other entry (og:*, twitter:*,\n * canonical…) untouched and in place. An empty `value` removes the entry.\n */\nfunction upsertMetaValue(entries: StaticMetaEntry[], name: string, value: string): StaticMetaEntry[] {\n const matches = (entry: StaticMetaEntry) =>\n name === \"title\"\n ? entry.length === 1 && entry[0].key === \"title\"\n : entry.some((p) => p.key === \"name\" && p.value === name);\n\n const index = entries.findIndex(matches);\n const next = entries.filter((entry, i) => i === index || !matches(entry));\n\n if (value === \"\") {\n return next.filter((entry) => !matches(entry));\n }\n\n const entry: StaticMetaEntry =\n name === \"title\"\n ? [{ key: \"title\", value }]\n : [\n { key: \"name\", value: name },\n { key: \"content\", value },\n ];\n\n if (index === -1) {\n // Title first, everything else appended — matches how meta arrays are usually written.\n return name === \"title\" ? [entry, ...next] : [...next, entry];\n }\n return next.map((existing, i) => (i === index ? entry : existing));\n}\n\n/** Number of times a translation key appears in the app sources. */\nfunction countKeyUsages(sources: string, key: string): number {\n let count = 0;\n let index = sources.indexOf(key);\n while (index !== -1) {\n // Only count whole keys: \"about.meta\" must not match inside \"about.meta.title\".\n const before = sources[index - 1];\n const after = sources[index + key.length];\n const isBoundary = (char: string | undefined) =>\n char === undefined || char === '\"' || char === \"'\" || char === \"`\" || char === \":\";\n if (isBoundary(before) && isBoundary(after)) count++;\n index = sources.indexOf(key, index + key.length);\n }\n return count;\n}\n\n/**\n * Read a key from a locale document. i18next accepts both a flat key (\"a.b\" as a literal\n * property) and a nested path, so both are tried — flat first, as the generated sites use it.\n */\nexport function readLocaleValue(data: Record<string, unknown>, key: string): string | null {\n const flat = data[key];\n if (typeof flat === \"string\") return flat;\n let current: unknown = data;\n for (const part of key.split(\".\")) {\n if (!current || typeof current !== \"object\") return null;\n current = (current as Record<string, unknown>)[part];\n }\n return typeof current === \"string\" ? current : null;\n}\n\n/** Write a key, keeping the shape it already has; new keys are created flat. */\nexport function writeLocaleValue(data: Record<string, unknown>, key: string, value: string): void {\n if (typeof data[key] === \"string\") {\n data[key] = value;\n return;\n }\n const parts = key.split(\".\");\n let current: Record<string, unknown> = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const next = current[parts[i]];\n if (!next || typeof next !== \"object\") {\n data[key] = value;\n return;\n }\n current = next as Record<string, unknown>;\n }\n const leaf = parts[parts.length - 1];\n if (typeof current[leaf] === \"string\") {\n current[leaf] = value;\n return;\n }\n data[key] = value;\n}\n\n/**\n * Render a meta tag to insert in a route that doesn't have one yet. Values that JSX can hold\n * verbatim are written as plain text/attributes; anything else goes through an expression\n * container so quotes and braces survive.\n */\nfunction printMetaTag(fieldName: string, value: string): string {\n if (fieldName === \"title\") {\n const plain = !/[{}<>&\\n]/.test(value);\n return plain ? `<title>${value}</title>` : `<title>{\"${escapeStringLiteralBody(value, '\"')}\"}</title>`;\n }\n const attribute = /[\"\\n]/.test(value)\n ? `content={\"${escapeStringLiteralBody(value, '\"')}\"}`\n : `content=\"${value}\"`;\n return `<meta name=\"${fieldName}\" ${attribute} />`;\n}\n\n/** Start of the line `offset` sits on, so a removed tag doesn't leave a blank line behind. */\nfunction lineRangeStart(code: string, offset: number): number {\n const lineStart = code.lastIndexOf(\"\\n\", offset);\n if (lineStart === -1) return offset;\n return code.slice(lineStart + 1, offset).trim() === \"\" ? lineStart : offset;\n}\n\nexport const payloadEditText = z.object({\n action: z.literal(\"editText\"),\n language: z\n .string()\n .length(2)\n .regex(/^[a-z]{2}$/),\n namespace: z.string().regex(/^[a-z0-9_-]+$/),\n key: z.string().regex(/^[a-zA-Z0-9_.-]+$/),\n content: z.string(),\n});\n\nexport type PayloadEditText = z.infer<typeof payloadEditText>;\n\nexport const payloadEditTextDirect = z.object({\n action: z.literal(\"editTextDirect\"),\n id: z.string().min(1),\n content: z.string(),\n});\n\nexport type PayloadEditTextDirect = z.infer<typeof payloadEditTextDirect>;\n\nexport const payloadEditClassName = z.object({\n action: z.literal(\"editClassName\"),\n id: z.string().min(1),\n className: z.string(),\n});\n\nexport type PayloadEditClassName = z.infer<typeof payloadEditClassName>;\n\nexport const payloadEditImage = z.object({\n action: z.literal(\"editImage\"),\n id: z.string().min(1),\n // New src value to write into the <img> source, e.g. \"/images/fashion-10.webp\"\n src: z.string().min(1),\n});\n\nexport type PayloadEditImage = z.infer<typeof payloadEditImage>;\n\n// arrayId is \"<relativeFile>:<arrayStartOffset>\" — identifies the inline array literal.\nexport const payloadArrayItemAdd = z.object({\n action: z.literal(\"arrayItemAdd\"),\n arrayId: z.string().min(1),\n // Default content for the inserted element (the editor sends \"New item\").\n content: z.string().default(\"New item\"),\n});\n\nexport type PayloadArrayItemAdd = z.infer<typeof payloadArrayItemAdd>;\n\nexport const payloadArrayItemDelete = z.object({\n action: z.literal(\"arrayItemDelete\"),\n arrayId: z.string().min(1),\n index: z.number().int().min(0),\n});\n\nexport type PayloadArrayItemDelete = z.infer<typeof payloadArrayItemDelete>;\n\n// Replace the entire contents of an inline array literal in one edit — used by the\n// editor's batched \"apply\" (✓) control so a whole add/delete session is a single\n// source change + rebuild instead of one per item.\nexport const payloadArraySet = z.object({\n action: z.literal(\"arraySet\"),\n arrayId: z.string().min(1),\n items: z.array(z.string()).min(1),\n});\n\nexport type PayloadArraySet = z.infer<typeof payloadArraySet>;\n\n// React-router route id, as reported by the client runtime (e.g. \"routes/_layout._index\",\n// \"root\"). Resolved to `<projectRoot>/app/<routeId>.<ext>` — see `resolveRouteFile`.\nconst routeIdSchema = z\n .string()\n .min(1)\n // Flat-route ids carry the file-name conventions: optional segments \"($lang)\", dynamic\n // \"$slug\", escaped characters \"sitemap[.]xml\", layout prefixes \"_layout.\".\n .regex(/^[a-zA-Z0-9._$/()[\\]+~-]+$/)\n .refine((id) => !id.split(\"/\").includes(\"..\") && !id.startsWith(\"/\"), \"Invalid route id\");\n\nexport const payloadGetPageMeta = z.object({\n action: z.literal(\"getPageMeta\"),\n routeId: routeIdSchema,\n /** Locale to read the translated values in. Defaults to the site's default language. */\n language: z\n .string()\n .regex(/^[a-zA-Z-]{2,10}$/)\n .optional(),\n});\n\nexport type PayloadGetPageMeta = z.infer<typeof payloadGetPageMeta>;\n\nexport const payloadSetPageMeta = z.object({\n action: z.literal(\"setPageMeta\"),\n routeId: routeIdSchema,\n language: z\n .string()\n .regex(/^[a-zA-Z-]{2,10}$/)\n .optional(),\n // Empty string removes the corresponding meta tag (keywords, robots) or clears the text.\n title: z.string().default(\"\"),\n description: z.string().default(\"\"),\n keywords: z.string().default(\"\"),\n robotsIndexing: z.boolean().default(true),\n});\n\nexport type PayloadSetPageMeta = z.infer<typeof payloadSetPageMeta>;\n\nexport const payloadGetSiteMeta = z.object({\n action: z.literal(\"getSiteMeta\"),\n});\n\nexport type PayloadGetSiteMeta = z.infer<typeof payloadGetSiteMeta>;\n\n// Site-root-relative path of an image already copied into the workspace, e.g. \"/images/x.webp\".\nconst publicImagePath = z\n .string()\n .regex(/^\\/[A-Za-z0-9._\\-/]+$/)\n .refine((value) => !value.includes(\"..\"), \"Invalid image path\");\n\nexport const payloadSetSiteMeta = z.object({\n action: z.literal(\"setSiteMeta\"),\n // `null` clears the setting, `undefined` leaves it untouched.\n favicon: publicImagePath.nullable().optional(),\n socialImage: publicImagePath.nullable().optional(),\n});\n\nexport type PayloadSetSiteMeta = z.infer<typeof payloadSetSiteMeta>;\n\nexport type GetSiteMetaResult =\n | {\n success: true;\n favicon: string | null;\n socialImage: string | null;\n /** False when root.tsx was customized and can no longer be upgraded automatically. */\n editable: boolean;\n reason?: string;\n }\n | { success: false; error: string };\n\nexport type SetSiteMetaResult = { success: true; filePaths: string[] } | { success: false; error: string };\n\nexport interface PageMetaField {\n value: string;\n editable: boolean;\n /** Why the field is read-only (dynamic value, computed title…). */\n reason?: string;\n /** True when the text lives in the locale files rather than in the route source. */\n translated: boolean;\n i18nKey?: string;\n /**\n * True when the translation key is also used elsewhere (typically as a heading on the\n * page). Saving then moves this page's meta onto its own key instead of rewriting the\n * shared one.\n */\n shared: boolean;\n /** Static text rendered around the value, e.g. `{`${title} | Acme`}`. */\n prefix: string;\n suffix: string;\n}\n\nexport type GetPageMetaResult =\n | {\n success: true;\n filePath: string;\n relativeFile: string;\n /** \"jsx\" for React-rendered meta tags, \"meta-export\" for a `export const meta` route. */\n mode: \"jsx\" | \"meta-export\";\n languages: string[];\n language: string;\n title: PageMetaField;\n description: PageMetaField;\n keywords: PageMetaField;\n robotsIndexing: boolean;\n robotsEditable: boolean;\n robotsReason?: string;\n }\n | { success: false; error: string };\n\nexport type SetPageMetaResult = { success: true; filePaths: string[] } | { success: false; error: string };\n\nexport interface EditableRegistry {\n version: number;\n generatedAt: string;\n elements: Record<string, EditableEntry>;\n}\n\nexport type EditResult =\n | {\n success: true;\n error?: never;\n filePath: string;\n }\n | {\n success: false;\n error: string;\n filePath?: never;\n };\n\nexport class UpstartEditorAPI {\n private registry: EditableRegistry | null = null;\n private projectRoot: string;\n private registryPath: string;\n\n constructor(projectRoot: string, registryPath: string) {\n this.projectRoot = projectRoot;\n this.registryPath = registryPath;\n }\n\n /**\n * Load the registry from disk\n */\n async loadRegistry(): Promise<void> {\n const content = await fs.readFile(this.registryPath, \"utf-8\");\n this.registry = JSON.parse(content);\n }\n\n /**\n * Get the current registry (for testing/debugging)\n */\n getRegistry(): EditableRegistry | null {\n return this.registry;\n }\n\n /**\n * Set the registry directly (for testing)\n */\n setRegistry(registry: EditableRegistry): void {\n this.registry = registry;\n }\n\n /**\n * Edit a translation value in an i18next locale file.\n * Auto-detects flat keys (e.g. \"nav.home\" as literal key) vs nested keys (e.g. nav -> home).\n * Only updates existing keys — returns an error if the key is not found.\n */\n async editText(params: PayloadEditText): Promise<EditResult> {\n const parsed = payloadEditText.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { language, namespace, key, content: newContent } = parsed.data;\n const filePath = path.join(this.projectRoot, \"app\", \"locales\", language, `${namespace}.json`);\n\n let raw: string;\n try {\n raw = await fs.readFile(filePath, \"utf-8\");\n } catch (err) {\n return { success: false, error: `Failed to read locale file: ${filePath}` };\n }\n\n let data: Record<string, unknown>;\n try {\n data = JSON.parse(raw);\n } catch (err) {\n return { success: false, error: `Failed to parse locale file: ${filePath}` };\n }\n\n // Strategy 1: check for flat/literal key at top level\n if (key in data && typeof data[key] === \"string\") {\n data[key] = newContent;\n } else {\n // Strategy 2: nested traversal via dot notation\n const parts = key.split(\".\");\n let current: Record<string, unknown> = data;\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i];\n if (current[part] == null || typeof current[part] !== \"object\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current = current[part] as Record<string, unknown>;\n }\n\n const leafKey = parts[parts.length - 1];\n if (!(leafKey in current) || typeof current[leafKey] !== \"string\") {\n return { success: false, error: `Key \"${key}\" not found in locale file ${filePath}` };\n }\n current[leafKey] = newContent;\n }\n\n try {\n await fs.writeFile(filePath, JSON.stringify(data, null, 2) + \"\\n\");\n } catch (err) {\n return { success: false, error: `Failed to write locale file: ${filePath}` };\n }\n\n return { success: true, filePath };\n }\n\n /**\n * Edit a plain-text or rich-text JSX node directly in the source TSX file.\n * For rich-text entries, `content` should be the inner JSX/HTML of the element's children.\n */\n async editTextDirect(params: PayloadEditTextDirect): Promise<EditResult> {\n const parsed = payloadEditTextDirect.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { id, content } = parsed.data;\n if (!this.registry) {\n try {\n await this.loadRegistry();\n } catch (err) {\n return { success: false, error: `Failed to load registry: ${err}` };\n }\n }\n const entry = this.registry!.elements[id];\n if (!entry) {\n return { success: false, error: `Element ${id} not found in registry` };\n }\n if (entry.type !== \"text\" && entry.type !== \"rich-text\" && entry.type !== \"mixed-text\") {\n return { success: false, error: `Element ${id} is not a text element (type: ${entry.type})` };\n }\n if (entry.type === \"mixed-text\") {\n return this.applyMixedTextEdit(id, entry, content);\n }\n // Text entries that map to a JS string literal (e.g. inline array items) must be\n // escaped for the literal's quote style before being written between the quotes.\n const finalContent = entry.quote ? escapeStringLiteralBody(content, entry.quote) : content;\n return this.applyEdit(id, entry, finalContent);\n }\n\n /**\n * Edit the className of an element\n */\n async editClassName(params: PayloadEditClassName): Promise<EditResult> {\n const parsed = payloadEditClassName.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { id, className: newClassName } = parsed.data;\n if (!this.registry) {\n try {\n await this.loadRegistry();\n } catch (err) {\n return { success: false, error: `Failed to load registry: ${err}` };\n }\n }\n\n const entry = this.registry!.elements[id];\n if (!entry) {\n return { success: false, error: `Element ${id} not found in registry` };\n }\n\n if (entry.type !== \"className\") {\n return { success: false, error: `Element ${id} is not a className element (type: ${entry.type})` };\n }\n\n return this.applyEdit(id, entry, newClassName);\n }\n\n /**\n * Edit the `src` of an <img> element directly in the source TSX file.\n * The byte range points at the string literal between the quotes, so we only\n * rewrite the path itself. Copying the asset into the project is handled by\n * the caller (the sandbox server, which has bucket access).\n */\n async editImage(params: PayloadEditImage): Promise<EditResult> {\n const parsed = payloadEditImage.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { id, src } = parsed.data;\n if (!this.registry) {\n try {\n await this.loadRegistry();\n } catch (err) {\n return { success: false, error: `Failed to load registry: ${err}` };\n }\n }\n\n const entry = this.registry!.elements[id];\n if (!entry) {\n return { success: false, error: `Element ${id} not found in registry` };\n }\n\n if (entry.type !== \"image\") {\n return { success: false, error: `Element ${id} is not an image element (type: ${entry.type})` };\n }\n\n return this.applyEdit(id, entry, src);\n }\n\n /**\n * Add a new element to an inline array literal (e.g. `[\"a\", \"b\"]`), used by the\n * editor's \"+\" control on editable .map() lists. The inserted element matches\n * the quote style of the existing string elements and reuses their separator so\n * multi-line indentation is preserved.\n */\n async arrayItemAdd(params: PayloadArrayItemAdd): Promise<EditResult> {\n const parsed = payloadArrayItemAdd.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { arrayId, content } = parsed.data;\n const loc = this.resolveArrayId(arrayId);\n if (!loc) return { success: false, error: `Invalid arrayId: ${arrayId}` };\n\n try {\n const code = await fs.readFile(loc.filePath, \"utf-8\");\n const arr = findArrayExpressionAt(code, loc.relativeFile, loc.offset);\n if (!arr) {\n return { success: false, error: `Array not found at ${arrayId}. The file may have been modified.` };\n }\n const els = arr.elements.filter((e): e is AstNode => e !== null);\n const quote = inferArrayQuote(code, els);\n const literal = `${quote}${escapeStringLiteralBody(content, quote)}${quote}`;\n\n const s = new MagicString(code);\n if (els.length === 0) {\n s.appendLeft(arr.start + 1, literal);\n } else {\n const last = els[els.length - 1];\n // Reuse the separator between the last two elements (keeps multi-line\n // indentation); fall back to \", \" for a single-element array.\n const sep = els.length >= 2 ? code.slice(els[els.length - 2].end, last.start) : \", \";\n s.appendLeft(last.end, `${sep}${literal}`);\n }\n\n await fs.writeFile(loc.filePath, s.toString());\n // Element count changed — drop the cached registry so it reloads after the\n // dev pipeline re-transforms the file.\n this.registry = null;\n return { success: true, filePath: loc.filePath };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /**\n * Delete the element at `index` from an inline array literal. Refuses to remove\n * the last remaining element (which would leave an empty, un-addressable array).\n */\n async arrayItemDelete(params: PayloadArrayItemDelete): Promise<EditResult> {\n const parsed = payloadArrayItemDelete.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { arrayId, index } = parsed.data;\n const loc = this.resolveArrayId(arrayId);\n if (!loc) return { success: false, error: `Invalid arrayId: ${arrayId}` };\n\n try {\n const code = await fs.readFile(loc.filePath, \"utf-8\");\n const arr = findArrayExpressionAt(code, loc.relativeFile, loc.offset);\n if (!arr) {\n return { success: false, error: `Array not found at ${arrayId}. The file may have been modified.` };\n }\n const els = arr.elements.filter((e): e is AstNode => e !== null);\n if (index >= els.length) {\n return { success: false, error: `Index ${index} out of range for array at ${arrayId}` };\n }\n if (els.length <= 1) {\n return { success: false, error: \"Cannot delete the last remaining array item\" };\n }\n\n const s = new MagicString(code);\n const el = els[index];\n if (index > 0) {\n // Remove the separator before the element + the element itself.\n s.remove(els[index - 1].end, el.end);\n } else {\n // First element: remove it + the separator after it.\n s.remove(el.start, els[1].start);\n }\n\n await fs.writeFile(loc.filePath, s.toString());\n this.registry = null;\n return { success: true, filePath: loc.filePath };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /**\n * Replace the entire contents of an inline array literal with `items`, preserving\n * the source's quote style and multi-line indentation. Used by the editor's\n * batched ✓ apply so a whole add/delete session is a single edit + rebuild.\n */\n async arraySet(params: PayloadArraySet): Promise<EditResult> {\n const parsed = payloadArraySet.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { arrayId, items } = parsed.data;\n const loc = this.resolveArrayId(arrayId);\n if (!loc) return { success: false, error: `Invalid arrayId: ${arrayId}` };\n\n try {\n const code = await fs.readFile(loc.filePath, \"utf-8\");\n const arr = findArrayExpressionAt(code, loc.relativeFile, loc.offset);\n if (!arr) {\n return { success: false, error: `Array not found at ${arrayId}. The file may have been modified.` };\n }\n const els = arr.elements.filter((e): e is AstNode => e !== null);\n const quote = inferArrayQuote(code, els);\n const literal = (v: string) => `${quote}${escapeStringLiteralBody(v, quote)}${quote}`;\n\n const innerStart = arr.start + 1;\n const innerEnd = arr.end - 1;\n const innerSrc = code.slice(innerStart, innerEnd);\n\n let inner: string;\n if (innerSrc.includes(\"\\n\")) {\n // Multi-line: reuse the item indentation and the closing-bracket indent,\n // and keep a trailing comma (matches the common prettier/biome style).\n const itemIndent = innerSrc.match(/\\n([ \\t]*)\\S/)?.[1] ?? \" \";\n const outerIndent = innerSrc.match(/\\n([ \\t]*)$/)?.[1] ?? \"\";\n inner = `\\n${items.map((it) => itemIndent + literal(it)).join(\",\\n\")},\\n${outerIndent}`;\n } else {\n inner = items.map(literal).join(\", \");\n }\n\n const s = new MagicString(code);\n if (innerStart === innerEnd) {\n s.appendLeft(innerStart, inner);\n } else {\n s.overwrite(innerStart, innerEnd, inner);\n }\n await fs.writeFile(loc.filePath, s.toString());\n this.registry = null;\n return { success: true, filePath: loc.filePath };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /**\n * Read the page metadata shown in the browser tab and in search results.\n *\n * Two shapes are supported, in this order: React-rendered tags (`<title>{title}</title>`\n * fed by the loader, the shape the AI assistant generates — the text then lives in the\n * locale files), and a static `export const meta` array.\n */\n async getPageMeta(params: PayloadGetPageMeta): Promise<GetPageMetaResult> {\n const parsed = payloadGetPageMeta.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const route = await this.resolveRouteFile(parsed.data.routeId);\n if (!route) {\n return { success: false, error: `Route file not found for \"${parsed.data.routeId}\"` };\n }\n\n let code: string;\n try {\n code = await fs.readFile(route.filePath, \"utf-8\");\n } catch {\n return { success: false, error: `Failed to read route file: ${route.relativeFile}` };\n }\n\n const languages = await this.listLanguages();\n const language = await this.resolveLanguage(parsed.data.language, languages);\n const files = await this.readAppSources();\n const analysis = analyzeRouteMeta(\n code,\n route.relativeFile,\n this.createModuleLoader(files, route.filePath),\n );\n\n if (analysis.hasJsxMeta) {\n const sources = [...files.values()].join(\"\\n\");\n const [title, description, keywords] = await Promise.all([\n this.describeField(analysis.title, language, sources),\n this.describeField(analysis.description, language, sources),\n this.describeField(analysis.keywords, language, sources),\n ]);\n const robots = analysis.robots.origin;\n const robotsEditable = robots.kind === \"absent\" || robots.kind === \"literal\";\n return {\n success: true,\n ...route,\n mode: \"jsx\",\n languages,\n language,\n title,\n description,\n keywords,\n robotsIndexing: robots.kind === \"literal\" ? !/noindex/i.test(robots.value) : true,\n robotsEditable,\n ...(robotsEditable ? {} : { robotsReason: \"This tag is computed by code\" }),\n };\n }\n\n const plain = (value: string, editable: boolean, reason?: string): PageMetaField => ({\n value,\n editable,\n ...(reason ? { reason } : {}),\n translated: false,\n shared: false,\n prefix: \"\",\n suffix: \"\",\n });\n\n const lookup = findMetaExport(code, route.relativeFile);\n if (lookup.kind === \"dynamic\") {\n return {\n success: true,\n ...route,\n mode: \"meta-export\",\n languages,\n language,\n title: plain(\"\", false, lookup.reason),\n description: plain(\"\", false, lookup.reason),\n keywords: plain(\"\", false, lookup.reason),\n robotsIndexing: true,\n robotsEditable: false,\n robotsReason: lookup.reason,\n };\n }\n\n const entries = lookup.kind === \"static\" ? lookup.entries : [];\n const robotsValue = findMetaValue(entries, \"robots\");\n return {\n success: true,\n ...route,\n mode: \"meta-export\",\n languages,\n language,\n title: plain(findMetaValue(entries, \"title\") ?? \"\", true),\n description: plain(findMetaValue(entries, \"description\") ?? \"\", true),\n keywords: plain(findMetaValue(entries, \"keywords\") ?? \"\", true),\n robotsIndexing: robotsValue === null ? true : !/noindex/i.test(robotsValue),\n robotsEditable: true,\n };\n }\n\n /**\n * Write the page metadata back. Values backed by a translation key are written to the\n * locale file of `language`; everything else is written into the route source. Returns\n * every file that changed so the caller can commit them together.\n */\n async setPageMeta(params: PayloadSetPageMeta): Promise<SetPageMetaResult> {\n const parsed = payloadSetPageMeta.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { routeId, title, description, keywords, robotsIndexing } = parsed.data;\n const route = await this.resolveRouteFile(routeId);\n if (!route) {\n return { success: false, error: `Route file not found for \"${routeId}\"` };\n }\n\n try {\n const code = await fs.readFile(route.filePath, \"utf-8\");\n const languages = await this.listLanguages();\n const language = await this.resolveLanguage(parsed.data.language, languages);\n const files = await this.readAppSources();\n const analysis = analyzeRouteMeta(\n code,\n route.relativeFile,\n this.createModuleLoader(files, route.filePath),\n );\n\n if (!analysis.hasJsxMeta) {\n return this.setMetaExport(route, code, { title, description, keywords, robotsIndexing });\n }\n\n const sources = [...files.values()].join(\"\\n\");\n const s = new MagicString(code);\n // Locale documents are loaded once, mutated by every field, then written back.\n const locales = new Map<string, Record<string, unknown>>();\n const changedLocales = new Set<string>();\n let sourceChanged = false;\n\n const loadLocale = async (lang: string, namespace: string) => {\n const id = `${lang}/${namespace}`;\n const cached = locales.get(id);\n if (cached) return cached;\n const data = await this.readLocale(lang, namespace);\n locales.set(id, data);\n return data;\n };\n\n const applyField = async (element: MetaElement, next: string, fieldName: string) => {\n const { origin } = element;\n // \"prop\" is resolved while analyzing; it should never reach here, and writing it\n // would have nowhere to go.\n if (origin.kind === \"unsupported\" || origin.kind === \"prop\") return;\n\n if (origin.kind === \"absent\") {\n if (next === \"\" || analysis.insertOffset === null) return;\n s.appendLeft(analysis.insertOffset, `\\n${analysis.insertIndent}${printMetaTag(fieldName, next)}`);\n sourceChanged = true;\n return;\n }\n\n if (origin.kind === \"literal\") {\n if (origin.value === next) return;\n s.overwrite(origin.start, origin.end, escapeStringLiteralBody(next, origin.quote));\n sourceChanged = true;\n return;\n }\n\n // Translation-backed value: write to the locale file rather than to the route.\n const data = await loadLocale(language, origin.namespace);\n const current = readLocaleValue(data, origin.key) ?? \"\";\n if (current === next) return;\n\n if (countKeyUsages(sources, origin.key) > 1) {\n // The key is also used elsewhere (usually a heading rendered on the page), so\n // give this page its own key instead of silently rewriting the shared text.\n const newKey = await this.allocateMetaKey(origin.key, fieldName, sources, origin.namespace);\n for (const lang of languages) {\n const langData = await loadLocale(lang, origin.namespace);\n const previous = readLocaleValue(langData, origin.key);\n // A language that never translated the shared key stays untranslated: writing an\n // empty string there would render an empty title instead of falling back.\n if (previous === null) continue;\n writeLocaleValue(langData, newKey, previous);\n changedLocales.add(`${lang}/${origin.namespace}`);\n }\n const raw = code.slice(origin.keyStart, origin.keyEnd);\n const prefix = raw.includes(\":\") ? `${raw.slice(0, raw.indexOf(\":\") + 1)}` : \"\";\n s.overwrite(origin.keyStart, origin.keyEnd, `${prefix}${newKey}`);\n sourceChanged = true;\n writeLocaleValue(await loadLocale(language, origin.namespace), newKey, next);\n } else {\n writeLocaleValue(data, origin.key, next);\n }\n changedLocales.add(`${language}/${origin.namespace}`);\n };\n\n await applyField(analysis.title, title, \"title\");\n await applyField(analysis.description, description, \"description\");\n await applyField(analysis.keywords, keywords, \"keywords\");\n\n // Robots is a plain on/off tag: absent means \"indexable\".\n const robots = analysis.robots;\n if (robotsIndexing) {\n if (robots.origin.kind === \"literal\" && robots.elementStart !== undefined) {\n s.remove(lineRangeStart(code, robots.elementStart), robots.elementEnd as number);\n sourceChanged = true;\n }\n } else if (robots.origin.kind === \"literal\") {\n if (!/noindex/i.test(robots.origin.value)) {\n s.overwrite(robots.origin.start, robots.origin.end, \"noindex, nofollow\");\n sourceChanged = true;\n }\n } else if (robots.origin.kind === \"absent\" && analysis.insertOffset !== null) {\n s.appendLeft(\n analysis.insertOffset,\n `\\n${analysis.insertIndent}${printMetaTag(\"robots\", \"noindex, nofollow\")}`,\n );\n sourceChanged = true;\n }\n\n const filePaths: string[] = [];\n if (sourceChanged) {\n await fs.writeFile(route.filePath, s.toString());\n filePaths.push(route.filePath);\n // Byte offsets in the registry are stale after a source rewrite.\n this.registry = null;\n }\n for (const id of changedLocales) {\n const [lang, namespace] = id.split(\"/\");\n const data = locales.get(id);\n if (!data) continue;\n const localePath = this.localePath(lang, namespace);\n await fs.writeFile(localePath, `${JSON.stringify(data, null, 2)}\\n`);\n filePaths.push(localePath);\n }\n return { success: true, filePaths };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /**\n * Read the site-wide settings rendered by `app/root.tsx`: the browser-tab icon and the\n * image shown when a page is shared on social networks.\n */\n async getSiteMeta(params: PayloadGetSiteMeta): Promise<GetSiteMetaResult> {\n const parsed = payloadGetSiteMeta.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const config = await this.readSiteConfig();\n if (!config) return { success: false, error: \"This site has no app/config/site.json\" };\n\n // Report up front whether saving would be able to upgrade root.tsx, so the panel can\n // stay read-only instead of failing at save time.\n const root = await this.readRootFile();\n const upgrade = root ? ensureRootSiteMeta(root.code, root.relativeFile) : null;\n\n return {\n success: true,\n favicon: typeof config.favicon === \"string\" ? config.favicon : null,\n socialImage: typeof config.socialImage === \"string\" ? config.socialImage : null,\n editable: !!upgrade?.ok,\n ...(upgrade && !upgrade.ok ? { reason: upgrade.reason } : {}),\n ...(root ? {} : { reason: \"This site has no app/root.tsx\" }),\n };\n }\n\n /**\n * Write the site-wide settings to `app/config/site.json`, upgrading `app/root.tsx` to\n * render them if it does not already. The image files themselves are copied into the\n * workspace by the caller, which has bucket access.\n */\n async setSiteMeta(params: PayloadSetSiteMeta): Promise<SetSiteMetaResult> {\n const parsed = payloadSetSiteMeta.safeParse(params);\n if (!parsed.success) {\n return { success: false, error: `Invalid payload: ${parsed.error.message}` };\n }\n const { favicon, socialImage } = parsed.data;\n if (favicon === undefined && socialImage === undefined) {\n return { success: true, filePaths: [] };\n }\n\n try {\n const config = await this.readSiteConfig();\n if (!config) return { success: false, error: \"This site has no app/config/site.json\" };\n\n const filePaths: string[] = [];\n const root = await this.readRootFile();\n if (!root) return { success: false, error: \"This site has no app/root.tsx\" };\n const upgrade = ensureRootSiteMeta(root.code, root.relativeFile);\n if (!upgrade.ok) {\n return { success: false, error: `${upgrade.reason} — ask Upsie to update it` };\n }\n if (upgrade.changed) {\n await fs.writeFile(root.filePath, upgrade.code);\n filePaths.push(root.filePath);\n // Byte offsets recorded for inline editing no longer match the rewritten file.\n this.registry = null;\n }\n\n const next = { ...config };\n const apply = (key: \"favicon\" | \"socialImage\", value: string | null | undefined) => {\n if (value === undefined) return;\n if (value === null) delete next[key];\n else next[key] = value;\n };\n apply(\"favicon\", favicon);\n apply(\"socialImage\", socialImage);\n\n const configPath = path.join(this.projectRoot, \"app\", \"config\", \"site.json\");\n await fs.writeFile(configPath, `${JSON.stringify(next, null, 2)}\\n`);\n filePaths.push(configPath);\n return { success: true, filePaths };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n private async readSiteConfig(): Promise<Record<string, unknown> | null> {\n try {\n const raw = await fs.readFile(path.join(this.projectRoot, \"app\", \"config\", \"site.json\"), \"utf-8\");\n const parsed = JSON.parse(raw);\n return parsed && typeof parsed === \"object\" ? (parsed as Record<string, unknown>) : null;\n } catch {\n return null;\n }\n }\n\n private async readRootFile(): Promise<{ filePath: string; relativeFile: string; code: string } | null> {\n for (const ext of [\"tsx\", \"jsx\"]) {\n const filePath = path.join(this.projectRoot, \"app\", `root.${ext}`);\n try {\n return {\n filePath,\n relativeFile: path.relative(this.projectRoot, filePath),\n code: await fs.readFile(filePath, \"utf-8\"),\n };\n } catch {\n // try next extension\n }\n }\n return null;\n }\n\n /** Rewrite a static `export const meta` array (routes that don't render meta as JSX). */\n private async setMetaExport(\n route: { filePath: string; relativeFile: string },\n code: string,\n values: { title: string; description: string; keywords: string; robotsIndexing: boolean },\n ): Promise<SetPageMetaResult> {\n const lookup = findMetaExport(code, route.relativeFile);\n if (lookup.kind === \"dynamic\") {\n return { success: false, error: `${lookup.reason} and cannot be edited here` };\n }\n\n let entries = lookup.kind === \"static\" ? lookup.entries : [];\n entries = upsertMetaValue(entries, \"title\", values.title);\n entries = upsertMetaValue(entries, \"description\", values.description);\n entries = upsertMetaValue(entries, \"keywords\", values.keywords);\n entries = upsertMetaValue(entries, \"robots\", values.robotsIndexing ? \"\" : \"noindex, nofollow\");\n\n const s = new MagicString(code);\n if (lookup.kind === \"static\") {\n // Indentation of the line the array starts on, so the rewritten literal lines up.\n const lineStart = code.lastIndexOf(\"\\n\", lookup.arrayStart) + 1;\n const indent = code.slice(lineStart, lookup.arrayStart).match(/^[ \\t]*/)?.[0] ?? \"\";\n s.overwrite(lookup.arrayStart, lookup.arrayEnd, printMetaArray(entries, indent));\n } else {\n // No `meta` export yet: insert one after the imports (or at the end of the file).\n const insertAt = findMetaInsertOffset(code, route.relativeFile);\n s.appendLeft(insertAt, `\\nexport const meta = () => ${printMetaArray(entries, \"\")};\\n`);\n }\n\n await fs.writeFile(route.filePath, s.toString());\n this.registry = null;\n return { success: true, filePaths: [route.filePath] };\n }\n\n /**\n * Render the read-only parts around an editable value: literal chunks as-is, other\n * translations resolved to the text they currently produce, anything dynamic as \"…\".\n */\n private async renderSegments(segments: MetaSegment[], language: string): Promise<string> {\n const parts = await Promise.all(\n segments.map(async (segment) => {\n if (\"text\" in segment) return segment.text;\n const { origin } = segment;\n if (origin.kind === \"literal\") return origin.value;\n if (origin.kind === \"i18n\") {\n const data = await this.readLocale(language, origin.namespace);\n return readLocaleValue(data, origin.key) ?? \"\";\n }\n return \"…\";\n }),\n );\n return parts.join(\"\");\n }\n\n /** Turn an analyzed meta element into what the settings panel needs to render. */\n private async describeField(\n element: MetaElement,\n language: string,\n sources: string,\n ): Promise<PageMetaField> {\n const { origin } = element;\n const [prefix, suffix] = await Promise.all([\n this.renderSegments(element.prefixSegments, language),\n this.renderSegments(element.suffixSegments, language),\n ]);\n const base = { prefix, suffix };\n if (origin.kind === \"unsupported\" || origin.kind === \"prop\") {\n const reason = origin.kind === \"prop\" ? \"This value is set by the page's SEO component\" : origin.reason;\n return { ...base, value: \"\", editable: false, reason, translated: false, shared: false };\n }\n if (origin.kind === \"absent\") {\n return { ...base, value: \"\", editable: true, translated: false, shared: false };\n }\n if (origin.kind === \"literal\") {\n return { ...base, value: origin.value, editable: true, translated: false, shared: false };\n }\n const data = await this.readLocale(language, origin.namespace);\n return {\n ...base,\n value: readLocaleValue(data, origin.key) ?? \"\",\n editable: true,\n translated: true,\n i18nKey: origin.key,\n shared: countKeyUsages(sources, origin.key) > 1,\n };\n }\n\n /** Languages the site ships translations for, from `app/locales/<lang>/`. */\n private async listLanguages(): Promise<string[]> {\n try {\n const entries = await fs.readdir(path.join(this.projectRoot, \"app\", \"locales\"), {\n withFileTypes: true,\n });\n return entries\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name)\n .sort();\n } catch {\n return [];\n }\n }\n\n /** Requested language when it exists, else the site default, else the first available one. */\n private async resolveLanguage(requested: string | undefined, languages: string[]): Promise<string> {\n if (requested && languages.includes(requested)) return requested;\n try {\n const raw = await fs.readFile(path.join(this.projectRoot, \"app\", \"config\", \"site.json\"), \"utf-8\");\n const config = JSON.parse(raw) as { defaultLanguage?: string | null };\n if (config.defaultLanguage && languages.includes(config.defaultLanguage)) {\n return config.defaultLanguage;\n }\n } catch {\n // no site config — fall through\n }\n return languages[0] ?? \"en\";\n }\n\n private localePath(language: string, namespace: string): string {\n return path.join(this.projectRoot, \"app\", \"locales\", language, `${namespace}.json`);\n }\n\n private async readLocale(language: string, namespace: string): Promise<Record<string, unknown>> {\n try {\n const raw = await fs.readFile(this.localePath(language, namespace), \"utf-8\");\n const parsed = JSON.parse(raw);\n return parsed && typeof parsed === \"object\" ? (parsed as Record<string, unknown>) : {};\n } catch {\n return {};\n }\n }\n\n /**\n * Every source file of the app, keyed by absolute path. Serves two purposes at once:\n * telling whether a translation key is referenced more than once, and following a route\n * to the shared component it hands its metadata to.\n */\n private async readAppSources(): Promise<Map<string, string>> {\n const files = new Map<string, string>();\n const visit = async (dir: string): Promise<void> => {\n const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (entry.name === \"node_modules\" || entry.name === \"locales\" || entry.name.startsWith(\".\")) {\n continue;\n }\n await visit(full);\n } else if (/\\.(tsx?|jsx?)$/.test(entry.name)) {\n try {\n files.set(full, await fs.readFile(full, \"utf-8\"));\n } catch {\n // unreadable file — ignore\n }\n }\n }\n };\n await visit(path.join(this.projectRoot, \"app\"));\n return files;\n }\n\n /**\n * Resolve the import specifiers a route can use for a local component — \"~/components/X\"\n * (the template's alias for `app/`) and relative paths — against the files already read.\n */\n private createModuleLoader(files: Map<string, string>, fromFile: string): ModuleLoader {\n return (specifier) => {\n let base: string;\n if (specifier.startsWith(\"~/\")) {\n base = path.join(this.projectRoot, \"app\", specifier.slice(2));\n } else if (specifier.startsWith(\".\")) {\n base = path.resolve(path.dirname(fromFile), specifier);\n } else {\n return null; // a package, not a file of the site\n }\n for (const candidate of [\n base,\n `${base}.tsx`,\n `${base}.ts`,\n `${base}.jsx`,\n `${base}.js`,\n path.join(base, \"index.tsx\"),\n path.join(base, \"index.ts\"),\n ]) {\n const code = files.get(candidate);\n if (code !== undefined) return { code, filePath: candidate };\n }\n return null;\n };\n }\n\n /**\n * Pick a free key dedicated to this page's meta, derived from the shared key\n * (\"atelier.title\" + \"title\" -> \"atelier.meta.title\"). Never reuses a key that already\n * exists in the locales or is referenced by the code.\n */\n private async allocateMetaKey(\n sharedKey: string,\n fieldName: string,\n sources: string,\n namespace: string,\n ): Promise<string> {\n const base = sharedKey.split(\".\")[0] || \"page\";\n const languages = await this.listLanguages();\n const documents = await Promise.all(languages.map((lang) => this.readLocale(lang, namespace)));\n for (let attempt = 0; attempt < 50; attempt++) {\n const candidate =\n attempt === 0 ? `${base}.meta.${fieldName}` : `${base}.meta.${fieldName}${attempt + 1}`;\n const taken =\n countKeyUsages(sources, candidate) > 0 ||\n documents.some((doc) => readLocaleValue(doc, candidate) !== null);\n if (!taken) return candidate;\n }\n return `${base}.meta.${fieldName}.${Date.now()}`;\n }\n\n /**\n * Resolve a react-router route id (\"routes/_layout._index\", \"root\") to its source file\n * inside the workspace. Never escapes `<projectRoot>/app`.\n */\n private async resolveRouteFile(\n routeId: string,\n ): Promise<{ filePath: string; relativeFile: string } | null> {\n const appDir = path.join(this.projectRoot, \"app\");\n for (const ext of [\"tsx\", \"ts\", \"jsx\", \"js\"]) {\n const filePath = path.join(appDir, `${routeId}.${ext}`);\n const resolved = path.resolve(filePath);\n if (resolved !== appDir && !resolved.startsWith(appDir + path.sep)) continue;\n try {\n await fs.access(resolved);\n return { filePath: resolved, relativeFile: path.relative(this.projectRoot, resolved) };\n } catch {\n // try next extension\n }\n }\n return null;\n }\n\n /** Parse \"<relativeFile>:<offset>\" into an absolute path + numeric offset. */\n private resolveArrayId(arrayId: string): { filePath: string; relativeFile: string; offset: number } | null {\n const sep = arrayId.lastIndexOf(\":\");\n if (sep < 0) return null;\n const relativeFile = arrayId.slice(0, sep);\n const offset = Number(arrayId.slice(sep + 1));\n if (!relativeFile || !Number.isInteger(offset) || offset < 0) return null;\n return { filePath: path.join(this.projectRoot, relativeFile), relativeFile, offset };\n }\n\n /**\n * Apply an edit to a source file\n */\n private async applyEdit(id: string, entry: EditableEntry, newContent: string): Promise<EditResult> {\n const filePath = path.join(this.projectRoot, entry.file);\n\n try {\n const code = await fs.readFile(filePath, \"utf-8\");\n\n // Verify content at expected location\n const currentContent = code.slice(entry.startOffset, entry.endOffset);\n\n let actualStart = entry.startOffset;\n let actualEnd = entry.endOffset;\n\n if (currentContent !== entry.originalContent) {\n // Content has shifted - try to find it by searching. An empty original\n // can't be located by search (indexOf(\"\") === 0 would be a false match),\n // so treat it as not found rather than inserting at offset 0.\n const searchIndex = entry.originalContent !== \"\" ? code.indexOf(entry.originalContent) : -1;\n if (searchIndex === -1) {\n return {\n success: false,\n error: `Original content \"${entry.originalContent}\" not found in file ${entry.file}. The file may have been modified.`,\n };\n }\n actualStart = searchIndex;\n actualEnd = searchIndex + entry.originalContent.length;\n }\n\n // Apply the edit using MagicString. A zero-length range (e.g. an empty\n // string literal \"\") cannot be overwritten — insert the content instead.\n const s = new MagicString(code);\n if (actualStart === actualEnd) {\n s.appendLeft(actualStart, newContent);\n } else {\n s.overwrite(actualStart, actualEnd, newContent);\n }\n\n // Write the modified file\n await fs.writeFile(filePath, s.toString());\n\n // Calculate the length difference for offset adjustments\n const lengthDiff = newContent.length - entry.originalContent.length;\n\n // Update the registry entry\n entry.startOffset = actualStart;\n entry.endOffset = actualStart + newContent.length;\n entry.originalContent = newContent;\n\n // Shift all subsequent entries in the same file\n for (const [otherId, otherEntry] of Object.entries(this.registry!.elements)) {\n if (otherId !== id && otherEntry.file === entry.file && otherEntry.startOffset > actualStart) {\n otherEntry.startOffset += lengthDiff;\n otherEntry.endOffset += lengthDiff;\n }\n }\n\n // Save the updated registry\n await fs.writeFile(this.registryPath, JSON.stringify(this.registry, null, 2));\n\n return { success: true, filePath };\n } catch (err) {\n return { success: false, error: String(err) };\n }\n }\n\n /**\n * Reconstruct JSX children from a user-edited template and the original expression segments,\n * then write the result back to the TSX source file.\n *\n * `template` uses `{{N}}` placeholders for expressions, e.g.:\n * \"© {{0}} Alex's Kitchen. All rights reserved.\"\n * The server replaces each placeholder with the original expression source from the registry.\n */\n private async applyMixedTextEdit(id: string, entry: EditableEntry, template: string): Promise<EditResult> {\n const exprSegments = (entry.segments ?? []).filter((s) => s.type === \"expr\");\n\n // Split template by {{N}} placeholders — odd indices are expression indices\n const parts = template.split(/\\{\\{(\\d+)\\}\\}/);\n\n // Reconstruct inner JSX: text parts interleaved with original expression sources\n let inner = \"\";\n for (let i = 0; i < parts.length; i++) {\n if (i % 2 === 0) {\n inner += parts[i];\n } else {\n const exprSeg = exprSegments[parseInt(parts[i])];\n inner += exprSeg ? exprSeg.raw : `{{${parts[i]}}}`;\n }\n }\n\n // Preserve leading/trailing JSX whitespace (indentation) from the original source\n const leadingWS = entry.originalContent.match(/^(\\s*)/)?.[1] ?? \"\";\n const trailingWS = entry.originalContent.match(/(\\s*)$/)?.[1] ?? \"\";\n const newContent = leadingWS + inner + trailingWS;\n\n return this.applyEdit(id, entry, newContent);\n }\n\n /**\n * Get element info by ID\n */\n getElement(id: string): EditableEntry | undefined {\n return this.registry?.elements[id];\n }\n\n /**\n * Get all elements of a specific type\n */\n getElementsByType(type: \"text\" | \"className\"): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.type === type) {\n result[id] = entry;\n }\n }\n return result;\n }\n\n /**\n * Get all elements in a specific file\n */\n getElementsByFile(file: string): Record<string, EditableEntry> {\n if (!this.registry) {\n return {};\n }\n\n const result: Record<string, EditableEntry> = {};\n for (const [id, entry] of Object.entries(this.registry.elements)) {\n if (entry.file === file) {\n result[id] = entry;\n }\n }\n return result;\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/index.ts"],"mappings":";;;;;;;;;;;;iBAiBgB,OAAA,CAAQ,IAAA,EAAM,UAAA;AAAA,iBAed,gBAAA,CAAiB,QAAA;;AAfjC;;iBAkDgB,iBAAA,CAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/index.ts"],"mappings":";;;;;;;;;;;;iBAiBgB,OAAA,CAAQ,IAAA,EAAM,UAAA;AAAA,iBAed,gBAAA,CAAiB,QAAA;;AAfjC;;iBAuFgB,iBAAA,CAAA"}
@@ -47,6 +47,40 @@ function waitForHydration(callback) {
47
47
  else window.addEventListener("load", onReady, { once: true });
48
48
  }
49
49
  /**
50
+ * Report the react-router id of the rendered route to the parent editor, so it can edit
51
+ * that route's `meta` export (page title, description…).
52
+ *
53
+ * The id is read from react-router's own data router rather than from `history.pushState`:
54
+ * the router pushes the new URL BEFORE it commits the new matches, so a route id read at
55
+ * pushState time would still be the previous page's. Subscribing gives us the id once the
56
+ * navigation is complete. The router global only appears during hydration, hence the retry.
57
+ */
58
+ function initRouteReporter() {
59
+ let lastReported;
60
+ const report = () => {
61
+ const matches = window.__reactRouterDataRouter?.state?.matches;
62
+ const routeId = Array.isArray(matches) ? matches[matches.length - 1]?.route?.id : void 0;
63
+ if (typeof routeId !== "string" || routeId === lastReported) return;
64
+ lastReported = routeId;
65
+ sendToParent({
66
+ type: "editor-route",
67
+ routeId
68
+ });
69
+ };
70
+ const attach = () => {
71
+ const router = window.__reactRouterDataRouter;
72
+ if (typeof router?.subscribe !== "function") return false;
73
+ router.subscribe(() => report());
74
+ report();
75
+ return true;
76
+ };
77
+ if (attach()) return;
78
+ let attempts = 0;
79
+ const timer = setInterval(() => {
80
+ if (attach() || ++attempts > 25) clearInterval(timer);
81
+ }, 200);
82
+ }
83
+ /**
50
84
  * Initialize the Upstart editor runtime.
51
85
  */
52
86
  function initUpstartEditor() {
@@ -92,6 +126,7 @@ function initUpstartEditor() {
92
126
  initArrayControls();
93
127
  initFormGuard();
94
128
  initErrorHandler();
129
+ initRouteReporter();
95
130
  sendToParent({
96
131
  type: "editor-ready",
97
132
  path: currentPath()
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/index.ts"],"sourcesContent":["import { initClickHandler } from \"./click-handler.js\";\nimport { initHoverOverlay, hideOverlays } from \"./hover-overlay.js\";\nimport { initErrorHandler } from \"./error-handler.js\";\nimport { initTextEditor, activateAllEditors, destroyAllActiveEditors } from \"./text-editor.js\";\nimport { initArrayControls, refreshArrayControls, hideArrayControls } from \"./array-controls.js\";\nimport { initFormGuard } from \"./form-guard.js\";\nimport { sendToParent } from \"./utils.js\";\nimport { getCurrentMode, setCurrentMode, clearSelection, removeSelection, getSelection } from \"./state.js\";\nimport type { EditorMode, UpstartParentMessage } from \"./types.js\";\n\nlet isInitialized = false;\n\nexport { getCurrentMode };\n\n/**\n * Set the current editor mode.\n */\nexport function setMode(mode: EditorMode): void {\n setCurrentMode(mode);\n console.log(`[Upstart Editor] Setting mode to: ${mode}`);\n if (mode === \"edit\") {\n disableSelectMode();\n enableEditMode();\n } else if (mode === \"select\") {\n disableEditMode();\n enableSelectMode();\n } else {\n disableEditMode();\n disableSelectMode();\n }\n}\n\nexport function waitForHydration(callback: () => void): void {\n // Remix/React 18 wraps hydrateRoot in startTransition, making hydration a\n // concurrent (low-priority) operation that can span many frames after the\n // load event. Instead of guessing a delay, we wait for the DOM to stabilise:\n // once 200 ms pass without any childList mutations, hydration is done.\n const STABILITY_MS = 200;\n\n const onReady = () => {\n let timer: number | null = null;\n\n const settle = () => {\n if (timer) clearTimeout(timer);\n timer = window.setTimeout(() => {\n observer.disconnect();\n callback();\n }, STABILITY_MS);\n };\n\n const observer = new MutationObserver(settle);\n observer.observe(document.documentElement, { childList: true, subtree: true });\n\n // Kick off the first timer (covers case where no mutations occur after load)\n settle();\n };\n\n if (document.readyState === \"complete\") {\n onReady();\n } else {\n window.addEventListener(\"load\", onReady, { once: true });\n }\n}\n\n/**\n * Initialize the Upstart editor runtime.\n */\nexport function initUpstartEditor(): void {\n if (isInitialized) {\n console.log(\"[Upstart Editor] Editor is already initialized\");\n return;\n }\n\n try {\n console.log(\"[Upstart Editor] Initializing...\");\n\n isInitialized = true;\n\n window.addEventListener(\"message\", handleParentMessage);\n\n // Current path (pathname + search), reported to the parent so it can reload\n // the iframe on the page being viewed instead of always the home page.\n const currentPath = () => location.pathname + location.search;\n\n // Notify parent on SPA navigation so it can resend the current editMode\n // and track the current page for the next reload.\n window.addEventListener(\"popstate\", () => {\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n });\n const originalPushState = history.pushState.bind(history);\n history.pushState = (...args) => {\n originalPushState(...args);\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n };\n const originalReplaceState = history.replaceState.bind(history);\n history.replaceState = (...args) => {\n originalReplaceState(...args);\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n };\n\n // i18next integration: if the app exposes it on window.__i18next (set before hydration),\n // wire it up so template variables (e.g. {{year}}) render as non-editable atoms.\n const i18next = (globalThis as any).__i18next;\n const getRawI18nTemplate = i18next\n ? (namespace: string, key: string) =>\n i18next.getResource(i18next.language, namespace, key) as string | undefined\n : undefined;\n\n initTextEditor(getRawI18nTemplate ? { getRawI18nTemplate } : {});\n initClickHandler();\n initHoverOverlay();\n initArrayControls();\n initFormGuard();\n initErrorHandler();\n\n sendToParent({ type: \"editor-ready\", path: currentPath() });\n } catch (error) {\n console.error(\"[Upstart Editor] Initialization failed:\", error);\n sendToParent({\n type: \"editor-error\",\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n }\n}\n\nconst ALLOWED_ORIGINS = [\"http://localhost:8080\", /upstart.gg$/];\n\nconst matchAllowedOrigins = (origin: string) => {\n return ALLOWED_ORIGINS.some((allowedOrigin) => {\n if (typeof allowedOrigin === \"string\") {\n return origin === allowedOrigin;\n } else if (allowedOrigin instanceof RegExp) {\n return allowedOrigin.test(origin);\n }\n return false;\n });\n};\n\nfunction handleParentMessage(event: MessageEvent): void {\n const message = event.data as UpstartParentMessage | undefined;\n\n console.log(\"[Upstart Editor] Received message from parent:\", { event, message });\n\n if (!message || !matchAllowedOrigins(event.origin)) {\n console.warn(\"[Upstart Editor] Ignoring message from unknown source:\", event.origin);\n return;\n }\n\n if (message.type === \"set-mode\") {\n console.log(\"Setting editor mode to:\", message.mode);\n setMode(message.mode);\n } else if (message.type === \"preview-classname\") {\n const el = document.querySelector<HTMLElement>(`[data-upstart-classname-id=\"${message.classNameId}\"]`);\n if (el) {\n el.className = message.className;\n }\n const STYLE_ID = \"upstart-preview-style\";\n let styleEl = document.getElementById(STYLE_ID) as HTMLStyleElement | null;\n if (message.previewCSS) {\n if (!styleEl) {\n styleEl = document.createElement(\"style\");\n styleEl.id = STYLE_ID;\n document.head.appendChild(styleEl);\n }\n styleEl.textContent = message.previewCSS;\n } else if (styleEl) {\n styleEl.textContent = \"\";\n }\n } else if (message.type === \"preview-image\") {\n const el = document.querySelector<HTMLImageElement>(`[data-upstart-image-id=\"${message.imageId}\"]`);\n if (el) {\n el.src = message.src;\n }\n } else if (message.type === \"request-scroll-position\") {\n sendToParent({ type: \"scroll-position\", x: window.scrollX, y: window.scrollY });\n } else if (message.type === \"restore-scroll-position\") {\n window.scrollTo({ left: message.x, top: message.y, behavior: \"auto\" });\n } else if (message.type === \"clear-selection\") {\n resetSelection();\n // Echo the now-empty selection so the parent store (chips / counter) updates.\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n } else if (message.type === \"deselect\") {\n const el = removeSelection(message.key);\n el?.removeAttribute(\"data-upstart-selected\");\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n }\n}\n\n/**\n * Drop the whole \"select\" mode selection: clear the in-memory set and remove the\n * visual marker from any element still carrying it.\n */\nfunction resetSelection(): void {\n clearSelection();\n for (const el of document.querySelectorAll(\"[data-upstart-selected]\")) {\n el.removeAttribute(\"data-upstart-selected\");\n }\n}\n\nfunction enableEditMode(): void {\n console.log(\"[Upstart Editor] Edit mode enabled\");\n document.documentElement.setAttribute(\"data-upstart-edit-mode\", \"\");\n activateAllEditors();\n refreshArrayControls();\n}\n\nfunction disableEditMode(): void {\n console.log(\"[Upstart Editor] Preview mode enabled\");\n document.documentElement.removeAttribute(\"data-upstart-edit-mode\");\n destroyAllActiveEditors();\n hideOverlays();\n hideArrayControls();\n}\n\nfunction enableSelectMode(): void {\n console.log(\"[Upstart Editor] Select mode enabled\");\n // Selectable hover (hover-overlay) and the persistent selected outline are\n // gated on this attribute. The selection Map / data-upstart-selected attrs are\n // kept across mode switches, so toggling back to select restores the outlines.\n document.documentElement.setAttribute(\"data-upstart-select-mode\", \"\");\n}\n\nfunction disableSelectMode(): void {\n document.documentElement.removeAttribute(\"data-upstart-select-mode\");\n hideOverlays();\n}\n\nexport { initTextEditor } from \"./text-editor.js\";\nexport { initClickHandler } from \"./click-handler.js\";\nexport { initErrorHandler } from \"./error-handler.js\";\nexport { initHoverOverlay } from \"./hover-overlay.js\";\nexport { sendToParent } from \"./utils.js\";\nexport type { EditorMessage, UpstartEditorMessage } from \"./types.js\";\n"],"mappings":";;;;;;;;;AAUA,IAAI,gBAAgB;;;;AAOpB,SAAgB,QAAQ,MAAwB;CAC9C,eAAe,KAAK;CACpB,QAAQ,IAAI,qCAAqC,OAAO;CACxD,IAAI,SAAS,QAAQ;EACnB,mBAAmB;EACnB,gBAAgB;QACX,IAAI,SAAS,UAAU;EAC5B,iBAAiB;EACjB,kBAAkB;QACb;EACL,iBAAiB;EACjB,mBAAmB;;;AAIvB,SAAgB,iBAAiB,UAA4B;CAK3D,MAAM,eAAe;CAErB,MAAM,gBAAgB;EACpB,IAAI,QAAuB;EAE3B,MAAM,eAAe;GACnB,IAAI,OAAO,aAAa,MAAM;GAC9B,QAAQ,OAAO,iBAAiB;IAC9B,SAAS,YAAY;IACrB,UAAU;MACT,aAAa;;EAGlB,MAAM,WAAW,IAAI,iBAAiB,OAAO;EAC7C,SAAS,QAAQ,SAAS,iBAAiB;GAAE,WAAW;GAAM,SAAS;GAAM,CAAC;EAG9E,QAAQ;;CAGV,IAAI,SAAS,eAAe,YAC1B,SAAS;MAET,OAAO,iBAAiB,QAAQ,SAAS,EAAE,MAAM,MAAM,CAAC;;;;;AAO5D,SAAgB,oBAA0B;CACxC,IAAI,eAAe;EACjB,QAAQ,IAAI,iDAAiD;EAC7D;;CAGF,IAAI;EACF,QAAQ,IAAI,mCAAmC;EAE/C,gBAAgB;EAEhB,OAAO,iBAAiB,WAAW,oBAAoB;EAIvD,MAAM,oBAAoB,SAAS,WAAW,SAAS;EAIvD,OAAO,iBAAiB,kBAAkB;GACxC,gBAAgB;GAChB,aAAa;IAAE,MAAM;IAAoB,MAAM,aAAa;IAAE,CAAC;IAC/D;EACF,MAAM,oBAAoB,QAAQ,UAAU,KAAK,QAAQ;EACzD,QAAQ,aAAa,GAAG,SAAS;GAC/B,kBAAkB,GAAG,KAAK;GAC1B,gBAAgB;GAChB,aAAa;IAAE,MAAM;IAAoB,MAAM,aAAa;IAAE,CAAC;;EAEjE,MAAM,uBAAuB,QAAQ,aAAa,KAAK,QAAQ;EAC/D,QAAQ,gBAAgB,GAAG,SAAS;GAClC,qBAAqB,GAAG,KAAK;GAC7B,gBAAgB;GAChB,aAAa;IAAE,MAAM;IAAoB,MAAM,aAAa;IAAE,CAAC;;EAKjE,MAAM,UAAW,WAAmB;EACpC,MAAM,qBAAqB,WACtB,WAAmB,QAClB,QAAQ,YAAY,QAAQ,UAAU,WAAW,IAAI,GACvD,KAAA;EAEJ,eAAe,qBAAqB,EAAE,oBAAoB,GAAG,EAAE,CAAC;EAChE,kBAAkB;EAClB,kBAAkB;EAClB,mBAAmB;EACnB,eAAe;EACf,kBAAkB;EAElB,aAAa;GAAE,MAAM;GAAgB,MAAM,aAAa;GAAE,CAAC;UACpD,OAAO;EACd,QAAQ,MAAM,2CAA2C,MAAM;EAC/D,aAAa;GACX,MAAM;GACN,OAAO,iBAAiB,QAAQ,MAAM,UAAU;GACjD,CAAC;;;AAIN,MAAM,kBAAkB,CAAC,yBAAyB,cAAc;AAEhE,MAAM,uBAAuB,WAAmB;CAC9C,OAAO,gBAAgB,MAAM,kBAAkB;EAC7C,IAAI,OAAO,kBAAkB,UAC3B,OAAO,WAAW;OACb,IAAI,yBAAyB,QAClC,OAAO,cAAc,KAAK,OAAO;EAEnC,OAAO;GACP;;AAGJ,SAAS,oBAAoB,OAA2B;CACtD,MAAM,UAAU,MAAM;CAEtB,QAAQ,IAAI,kDAAkD;EAAE;EAAO;EAAS,CAAC;CAEjF,IAAI,CAAC,WAAW,CAAC,oBAAoB,MAAM,OAAO,EAAE;EAClD,QAAQ,KAAK,0DAA0D,MAAM,OAAO;EACpF;;CAGF,IAAI,QAAQ,SAAS,YAAY;EAC/B,QAAQ,IAAI,2BAA2B,QAAQ,KAAK;EACpD,QAAQ,QAAQ,KAAK;QAChB,IAAI,QAAQ,SAAS,qBAAqB;EAC/C,MAAM,KAAK,SAAS,cAA2B,+BAA+B,QAAQ,YAAY,IAAI;EACtG,IAAI,IACF,GAAG,YAAY,QAAQ;EAEzB,MAAM,WAAW;EACjB,IAAI,UAAU,SAAS,eAAe,SAAS;EAC/C,IAAI,QAAQ,YAAY;GACtB,IAAI,CAAC,SAAS;IACZ,UAAU,SAAS,cAAc,QAAQ;IACzC,QAAQ,KAAK;IACb,SAAS,KAAK,YAAY,QAAQ;;GAEpC,QAAQ,cAAc,QAAQ;SACzB,IAAI,SACT,QAAQ,cAAc;QAEnB,IAAI,QAAQ,SAAS,iBAAiB;EAC3C,MAAM,KAAK,SAAS,cAAgC,2BAA2B,QAAQ,QAAQ,IAAI;EACnG,IAAI,IACF,GAAG,MAAM,QAAQ;QAEd,IAAI,QAAQ,SAAS,2BAC1B,aAAa;EAAE,MAAM;EAAmB,GAAG,OAAO;EAAS,GAAG,OAAO;EAAS,CAAC;MAC1E,IAAI,QAAQ,SAAS,2BAC1B,OAAO,SAAS;EAAE,MAAM,QAAQ;EAAG,KAAK,QAAQ;EAAG,UAAU;EAAQ,CAAC;MACjE,IAAI,QAAQ,SAAS,mBAAmB;EAC7C,gBAAgB;EAEhB,aAAa;GAAE,MAAM;GAAqB,WAAW,cAAc;GAAE,CAAC;QACjE,IAAI,QAAQ,SAAS,YAAY;EAEtC,gBAD2B,QAAQ,IACjC,EAAE,gBAAgB,wBAAwB;EAC5C,aAAa;GAAE,MAAM;GAAqB,WAAW,cAAc;GAAE,CAAC;;;;;;;AAQ1E,SAAS,iBAAuB;CAC9B,gBAAgB;CAChB,KAAK,MAAM,MAAM,SAAS,iBAAiB,0BAA0B,EACnE,GAAG,gBAAgB,wBAAwB;;AAI/C,SAAS,iBAAuB;CAC9B,QAAQ,IAAI,qCAAqC;CACjD,SAAS,gBAAgB,aAAa,0BAA0B,GAAG;CACnE,oBAAoB;CACpB,sBAAsB;;AAGxB,SAAS,kBAAwB;CAC/B,QAAQ,IAAI,wCAAwC;CACpD,SAAS,gBAAgB,gBAAgB,yBAAyB;CAClE,yBAAyB;CACzB,cAAc;CACd,mBAAmB;;AAGrB,SAAS,mBAAyB;CAChC,QAAQ,IAAI,uCAAuC;CAInD,SAAS,gBAAgB,aAAa,4BAA4B,GAAG;;AAGvE,SAAS,oBAA0B;CACjC,SAAS,gBAAgB,gBAAgB,2BAA2B;CACpE,cAAc"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/index.ts"],"sourcesContent":["import { initClickHandler } from \"./click-handler.js\";\nimport { initHoverOverlay, hideOverlays } from \"./hover-overlay.js\";\nimport { initErrorHandler } from \"./error-handler.js\";\nimport { initTextEditor, activateAllEditors, destroyAllActiveEditors } from \"./text-editor.js\";\nimport { initArrayControls, refreshArrayControls, hideArrayControls } from \"./array-controls.js\";\nimport { initFormGuard } from \"./form-guard.js\";\nimport { sendToParent } from \"./utils.js\";\nimport { getCurrentMode, setCurrentMode, clearSelection, removeSelection, getSelection } from \"./state.js\";\nimport type { EditorMode, UpstartParentMessage } from \"./types.js\";\n\nlet isInitialized = false;\n\nexport { getCurrentMode };\n\n/**\n * Set the current editor mode.\n */\nexport function setMode(mode: EditorMode): void {\n setCurrentMode(mode);\n console.log(`[Upstart Editor] Setting mode to: ${mode}`);\n if (mode === \"edit\") {\n disableSelectMode();\n enableEditMode();\n } else if (mode === \"select\") {\n disableEditMode();\n enableSelectMode();\n } else {\n disableEditMode();\n disableSelectMode();\n }\n}\n\nexport function waitForHydration(callback: () => void): void {\n // Remix/React 18 wraps hydrateRoot in startTransition, making hydration a\n // concurrent (low-priority) operation that can span many frames after the\n // load event. Instead of guessing a delay, we wait for the DOM to stabilise:\n // once 200 ms pass without any childList mutations, hydration is done.\n const STABILITY_MS = 200;\n\n const onReady = () => {\n let timer: number | null = null;\n\n const settle = () => {\n if (timer) clearTimeout(timer);\n timer = window.setTimeout(() => {\n observer.disconnect();\n callback();\n }, STABILITY_MS);\n };\n\n const observer = new MutationObserver(settle);\n observer.observe(document.documentElement, { childList: true, subtree: true });\n\n // Kick off the first timer (covers case where no mutations occur after load)\n settle();\n };\n\n if (document.readyState === \"complete\") {\n onReady();\n } else {\n window.addEventListener(\"load\", onReady, { once: true });\n }\n}\n\n/**\n * Report the react-router id of the rendered route to the parent editor, so it can edit\n * that route's `meta` export (page title, description…).\n *\n * The id is read from react-router's own data router rather than from `history.pushState`:\n * the router pushes the new URL BEFORE it commits the new matches, so a route id read at\n * pushState time would still be the previous page's. Subscribing gives us the id once the\n * navigation is complete. The router global only appears during hydration, hence the retry.\n */\nfunction initRouteReporter(): void {\n let lastReported: string | undefined;\n\n const report = () => {\n // biome-ignore lint/suspicious/noExplicitAny: react-router does not type its window globals\n const matches = (window as any).__reactRouterDataRouter?.state?.matches;\n const routeId = Array.isArray(matches) ? matches[matches.length - 1]?.route?.id : undefined;\n if (typeof routeId !== \"string\" || routeId === lastReported) return;\n lastReported = routeId;\n sendToParent({ type: \"editor-route\", routeId });\n };\n\n const attach = (): boolean => {\n // biome-ignore lint/suspicious/noExplicitAny: react-router does not type its window globals\n const router = (window as any).__reactRouterDataRouter;\n if (typeof router?.subscribe !== \"function\") return false;\n router.subscribe(() => report());\n report();\n return true;\n };\n\n if (attach()) return;\n let attempts = 0;\n const timer = setInterval(() => {\n if (attach() || ++attempts > 25) clearInterval(timer);\n }, 200);\n}\n\n/**\n * Initialize the Upstart editor runtime.\n */\nexport function initUpstartEditor(): void {\n if (isInitialized) {\n console.log(\"[Upstart Editor] Editor is already initialized\");\n return;\n }\n\n try {\n console.log(\"[Upstart Editor] Initializing...\");\n\n isInitialized = true;\n\n window.addEventListener(\"message\", handleParentMessage);\n\n // Current path (pathname + search), reported to the parent so it can reload\n // the iframe on the page being viewed instead of always the home page.\n const currentPath = () => location.pathname + location.search;\n\n // Notify parent on SPA navigation so it can resend the current editMode\n // and track the current page for the next reload.\n window.addEventListener(\"popstate\", () => {\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n });\n const originalPushState = history.pushState.bind(history);\n history.pushState = (...args) => {\n originalPushState(...args);\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n };\n const originalReplaceState = history.replaceState.bind(history);\n history.replaceState = (...args) => {\n originalReplaceState(...args);\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n };\n\n // i18next integration: if the app exposes it on window.__i18next (set before hydration),\n // wire it up so template variables (e.g. {{year}}) render as non-editable atoms.\n const i18next = (globalThis as any).__i18next;\n const getRawI18nTemplate = i18next\n ? (namespace: string, key: string) =>\n i18next.getResource(i18next.language, namespace, key) as string | undefined\n : undefined;\n\n initTextEditor(getRawI18nTemplate ? { getRawI18nTemplate } : {});\n initClickHandler();\n initHoverOverlay();\n initArrayControls();\n initFormGuard();\n initErrorHandler();\n initRouteReporter();\n\n sendToParent({ type: \"editor-ready\", path: currentPath() });\n } catch (error) {\n console.error(\"[Upstart Editor] Initialization failed:\", error);\n sendToParent({\n type: \"editor-error\",\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n }\n}\n\nconst ALLOWED_ORIGINS = [\"http://localhost:8080\", /upstart.gg$/];\n\nconst matchAllowedOrigins = (origin: string) => {\n return ALLOWED_ORIGINS.some((allowedOrigin) => {\n if (typeof allowedOrigin === \"string\") {\n return origin === allowedOrigin;\n } else if (allowedOrigin instanceof RegExp) {\n return allowedOrigin.test(origin);\n }\n return false;\n });\n};\n\nfunction handleParentMessage(event: MessageEvent): void {\n const message = event.data as UpstartParentMessage | undefined;\n\n console.log(\"[Upstart Editor] Received message from parent:\", { event, message });\n\n if (!message || !matchAllowedOrigins(event.origin)) {\n console.warn(\"[Upstart Editor] Ignoring message from unknown source:\", event.origin);\n return;\n }\n\n if (message.type === \"set-mode\") {\n console.log(\"Setting editor mode to:\", message.mode);\n setMode(message.mode);\n } else if (message.type === \"preview-classname\") {\n const el = document.querySelector<HTMLElement>(`[data-upstart-classname-id=\"${message.classNameId}\"]`);\n if (el) {\n el.className = message.className;\n }\n const STYLE_ID = \"upstart-preview-style\";\n let styleEl = document.getElementById(STYLE_ID) as HTMLStyleElement | null;\n if (message.previewCSS) {\n if (!styleEl) {\n styleEl = document.createElement(\"style\");\n styleEl.id = STYLE_ID;\n document.head.appendChild(styleEl);\n }\n styleEl.textContent = message.previewCSS;\n } else if (styleEl) {\n styleEl.textContent = \"\";\n }\n } else if (message.type === \"preview-image\") {\n const el = document.querySelector<HTMLImageElement>(`[data-upstart-image-id=\"${message.imageId}\"]`);\n if (el) {\n el.src = message.src;\n }\n } else if (message.type === \"request-scroll-position\") {\n sendToParent({ type: \"scroll-position\", x: window.scrollX, y: window.scrollY });\n } else if (message.type === \"restore-scroll-position\") {\n window.scrollTo({ left: message.x, top: message.y, behavior: \"auto\" });\n } else if (message.type === \"clear-selection\") {\n resetSelection();\n // Echo the now-empty selection so the parent store (chips / counter) updates.\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n } else if (message.type === \"deselect\") {\n const el = removeSelection(message.key);\n el?.removeAttribute(\"data-upstart-selected\");\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n }\n}\n\n/**\n * Drop the whole \"select\" mode selection: clear the in-memory set and remove the\n * visual marker from any element still carrying it.\n */\nfunction resetSelection(): void {\n clearSelection();\n for (const el of document.querySelectorAll(\"[data-upstart-selected]\")) {\n el.removeAttribute(\"data-upstart-selected\");\n }\n}\n\nfunction enableEditMode(): void {\n console.log(\"[Upstart Editor] Edit mode enabled\");\n document.documentElement.setAttribute(\"data-upstart-edit-mode\", \"\");\n activateAllEditors();\n refreshArrayControls();\n}\n\nfunction disableEditMode(): void {\n console.log(\"[Upstart Editor] Preview mode enabled\");\n document.documentElement.removeAttribute(\"data-upstart-edit-mode\");\n destroyAllActiveEditors();\n hideOverlays();\n hideArrayControls();\n}\n\nfunction enableSelectMode(): void {\n console.log(\"[Upstart Editor] Select mode enabled\");\n // Selectable hover (hover-overlay) and the persistent selected outline are\n // gated on this attribute. The selection Map / data-upstart-selected attrs are\n // kept across mode switches, so toggling back to select restores the outlines.\n document.documentElement.setAttribute(\"data-upstart-select-mode\", \"\");\n}\n\nfunction disableSelectMode(): void {\n document.documentElement.removeAttribute(\"data-upstart-select-mode\");\n hideOverlays();\n}\n\nexport { initTextEditor } from \"./text-editor.js\";\nexport { initClickHandler } from \"./click-handler.js\";\nexport { initErrorHandler } from \"./error-handler.js\";\nexport { initHoverOverlay } from \"./hover-overlay.js\";\nexport { sendToParent } from \"./utils.js\";\nexport type { EditorMessage, UpstartEditorMessage } from \"./types.js\";\n"],"mappings":"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"}
@@ -78,6 +78,9 @@ type EditorMessage = {
78
78
  } | {
79
79
  type: "editor-navigated";
80
80
  path?: string;
81
+ } /** React-router id of the route currently rendered (e.g. "routes/_layout._index"). */ | {
82
+ type: "editor-route";
83
+ routeId: string;
81
84
  } | {
82
85
  type: "scroll-position";
83
86
  x: number;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/types.ts"],"mappings":";;;;;;AAeA;;;;KAAY,UAAA;AAOZ;;;;;AAAA,UAAiB,YAAA;EACf,GAAA;EACA,IAAA;EACA,QAAA;EACA,aAAA;EACA,QAAA;EACA,IAAA;EAMS;AAWX;;;;EAXE,SAAA;AAAA;;;;;;;;;KAWU,cAAA;;;AAiBZ;UAZiB,aAAA;EACf,GAAA;EACA,IAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,MAAA;AAAA;;;;UAMe,cAAA;EACf,MAAA,EAAQ,MAAA;EACR,OAAA,EAAS,WAAA;EACT,IAAA;EACA,IAAA,EAAM,cAAA;AAAA;;;;;UAOS,gBAAA;EACf,EAAA;EACA,GAAA;EACA,GAAA;AAAA;;;;KAMU,aAAA;EAEN,IAAA;EACA,OAAA,EACI,eAAA,GACA,qBAAA,GACA,mBAAA,GACA,sBAAA,GACA,eAAA;AAAA;EAEJ,IAAA;EAAsB,IAAA;AAAA;EACtB,IAAA;EAA0B,IAAA;AAAA;EAC1B,IAAA;EAAyB,CAAA;EAAW,CAAA;AAAA;EACpC,IAAA;EAAsB,KAAA;AAAA;EACtB,IAAA;EAA2B,SAAA,EAAW,YAAA;AAAA;EAEtC,IAAA;EACA,IAAA;EACA,aAAA;EACA,QAAA;EACA,gBAAA;EACA,WAAA;EACA,YAAA;EACA,QAAA;EACA,WAAA,GAAc,MAAA;EACd,MAAA,EAAQ,aAAA;EACR,aAAA,WARA;EAUA,MAAA,GAAS,gBAAA;AAAA;;;;KAMH,aAAA;EACN,IAAA;EAAkB,IAAA,EAAM,UAAA;AAAA;EACxB,IAAA;EAA2B,WAAA;EAAqB,SAAA;EAAmB,UAAA;AAAA;EACnE,IAAA;EAAuB,OAAA;EAAiB,GAAA;AAAA;EACxC,IAAA;AAAA;EACA,IAAA;EAAiC,CAAA;EAAW,CAAA;AAAA;EAC5C,IAAA;AAAA;EACA,IAAA;EAAkB,GAAA;AAAA;;;;UAKP,oBAAA;EACf,MAAA;EACA,IAAA,EAAM,aAAA;EAAA,CACL,GAAA;AAAA;AAHH;;;AAAA,UASiB,oBAAA;EACf,MAAA;EACA,IAAA,EAAM,aAAA;EAAA,CACL,GAAA;AAAA;;;AAHH;UASiB,oBAAA;;;;;EAKf,gBAAA;EAXC;;;AAMH;;EAYE,sBAAA;EAZmC;;;;EAkBnC,iBAAA;EAYA;;;;EANA,UAAA;EAmBoD;;AAMtD;;EAnBE,WAAA;EAwBA;;;;EAlBA,aAAA;;;;;;EAOA,kBAAA,IAAsB,SAAA,UAAmB,GAAA;AAAA;;;;UAM1B,0BAAA;;;;;EAKf,OAAA;;;;;EAMA,UAAA;AAAA"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/types.ts"],"mappings":";;;;;;AAeA;;;;KAAY,UAAA;AAOZ;;;;;AAAA,UAAiB,YAAA;EACf,GAAA;EACA,IAAA;EACA,QAAA;EACA,aAAA;EACA,QAAA;EACA,IAAA;EAMS;AAWX;;;;EAXE,SAAA;AAAA;;;;;;;;;KAWU,cAAA;;;AAiBZ;UAZiB,aAAA;EACf,GAAA;EACA,IAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,MAAA;AAAA;;;;UAMe,cAAA;EACf,MAAA,EAAQ,MAAA;EACR,OAAA,EAAS,WAAA;EACT,IAAA;EACA,IAAA,EAAM,cAAA;AAAA;;;;;UAOS,gBAAA;EACf,EAAA;EACA,GAAA;EACA,GAAA;AAAA;;;;KAMU,aAAA;EAEN,IAAA;EACA,OAAA,EACI,eAAA,GACA,qBAAA,GACA,mBAAA,GACA,sBAAA,GACA,eAAA;AAAA;EAEJ,IAAA;EAAsB,IAAA;AAAA;EACtB,IAAA;EAA0B,IAAA;AAAA;EAE1B,IAAA;EAAsB,OAAA;AAAA;EACtB,IAAA;EAAyB,CAAA;EAAW,CAAA;AAAA;EACpC,IAAA;EAAsB,KAAA;AAAA;EACtB,IAAA;EAA2B,SAAA,EAAW,YAAA;AAAA;EAEtC,IAAA;EACA,IAAA;EACA,aAAA;EACA,QAAA;EACA,gBAAA;EACA,WAAA;EACA,YAAA;EACA,QAAA;EACA,WAAA,GAAc,MAAA;EACd,MAAA,EAAQ,aAAA;EACR,aAAA,WALA;EAOA,MAAA,GAAS,gBAAA;AAAA;;;;KAMH,aAAA;EACN,IAAA;EAAkB,IAAA,EAAM,UAAA;AAAA;EACxB,IAAA;EAA2B,WAAA;EAAqB,SAAA;EAAmB,UAAA;AAAA;EACnE,IAAA;EAAuB,OAAA;EAAiB,GAAA;AAAA;EACxC,IAAA;AAAA;EACA,IAAA;EAAiC,CAAA;EAAW,CAAA;AAAA;EAC5C,IAAA;AAAA;EACA,IAAA;EAAkB,GAAA;AAAA;;;;UAKP,oBAAA;EACf,MAAA;EACA,IAAA,EAAM,aAAA;EAAA,CACL,GAAA;AAAA;;;;UAMc,oBAAA;EACf,MAAA;EACA,IAAA,EAAM,aAAA;EAAA,CACL,GAAA;AAAA;;;;UAMc,oBAAA;EAPf;;;;EAYA,gBAAA;EALe;;;;;EAYf,sBAAA;EAMA;;;;EAAA,iBAAA;EAyBsB;;;;EAnBtB,UAAA;EAyByC;;;;EAnBzC,WAAA;;;;;EAMA,aAAA;;;;;;EAOA,kBAAA,IAAsB,SAAA,UAAmB,GAAA;AAAA;;;;UAM1B,0BAAA;;;;;EAKf,OAAA;;;;;EAMA,UAAA;AAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upstart.gg/vite-plugins",
3
- "version": "0.1.61",
3
+ "version": "0.1.62",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -23,7 +23,7 @@
23
23
  "oxc-parser": "0.101.0",
24
24
  "unplugin": "^2.3.11",
25
25
  "zimmerframe": "^1.1.4",
26
- "@upstart.gg/sdk": "^0.1.61"
26
+ "@upstart.gg/sdk": "^0.1.62"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@rolldown/binding-linux-arm64-gnu": "1.0.0",
@@ -109,11 +109,16 @@
109
109
  "import": "./dist/vite-plugin-upstart-theme.js",
110
110
  "types": "./dist/vite-plugin-upstart-theme.d.ts",
111
111
  "bun": "./src/vite-plugin-upstart-theme.ts"
112
+ },
113
+ "./site-meta": {
114
+ "import": "./dist/site-meta.js",
115
+ "types": "./dist/site-meta.d.ts",
116
+ "bun": "./src/site-meta.ts"
112
117
  }
113
118
  },
114
119
  "peerDependencies": {
115
120
  "zod": "4.3.6",
116
- "@upstart.gg/sdk": "^0.1.61"
121
+ "@upstart.gg/sdk": "^0.1.62"
117
122
  },
118
123
  "author": "Upstart",
119
124
  "publishConfig": {