Prettier formatting now happens server-side in source-inspection, so remove the browser-side format.ts hook and the prettier dependency from the web app. Add prettier to control-plane where it is actually used.
Unified · Split
| @@ -16,7 +16,6 @@ · 15 unmodified lines | |||
| 16 | "class-variance-authority": "^0.7.1", | 16 | "class-variance-authority": "^0.7.1", |
| 17 | "clsx": "^2.1.1", | 17 | "clsx": "^2.1.1", |
| 18 | "effect": "catalog:", | 18 | "effect": "catalog:", |
| 19 | "prettier": "^3.8.1", | ||
| 20 | "react": "^19.2.0", | 19 | "react": "^19.2.0", |
| 21 | "react-dom": "^19.2.0", | 20 | "react-dom": "^19.2.0", |
| 22 | "react-grab": "^0.1.26", | 21 | "react-grab": "^0.1.26", |
| @@ -1,5 +1,4 @@ | |||
| 1 | import { cn } from "../lib/utils"; | 1 | import { cn } from "../lib/utils"; |
| 2 | import { useFormattedCode } from "../lib/format"; | ||
| 3 | import { CodeBlock } from "./code-block"; | 2 | import { CodeBlock } from "./code-block"; |
| 4 | 3 | ||
| 5 | export function DocumentPanel(props: { | 4 | export function DocumentPanel(props: { |
| @@ -9,8 +8,6 @@ export function DocumentPanel(props: { · 3 unmodified lines | |||
| 9 | empty: string; | 8 | empty: string; |
| 10 | compact?: boolean; | 9 | compact?: boolean; |
| 11 | }) { | 10 | }) { |
| 12 | const formatted = useFormattedCode(props.body, props.lang); | ||
| 13 | |||
| 14 | return ( | 11 | return ( |
| 15 | <section | 12 | <section |
| 16 | className={cn( | 13 | className={cn( |
| @@ -21,8 +18,8 @@ export function DocumentPanel(props: { · 4 unmodified lines | |||
| 21 | <div className="border-b border-border px-3 py-2 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"> | 18 | <div className="border-b border-border px-3 py-2 text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70"> |
| 22 | {props.title} | 19 | {props.title} |
| 23 | </div> | 20 | </div> |
| 24 | {formatted ? ( | 21 | {props.body ? ( |
| 25 | <CodeBlock code={formatted} lang={props.lang} className="max-h-[32rem]" /> | 22 | <CodeBlock code={props.body} lang={props.lang} className="max-h-[32rem]" /> |
| 26 | ) : ( | 23 | ) : ( |
| 27 | <div className="flex items-center justify-center p-6 text-[13px] text-muted-foreground/40"> | 24 | <div className="flex items-center justify-center p-6 text-[13px] text-muted-foreground/40"> |
| 28 | {props.empty} | 25 | {props.empty} |
| @@ -1,133 +0,0 @@ | |||
| 1 | import { useState, useEffect } from "react"; | ||
| 2 | |||
| 3 | // Cache formatted results so we don't re-format on every render | ||
| 4 | const formatCache = new Map<string, string>(); | ||
| 5 | |||
| 6 | function formatCacheKey(code: string, parser: string): string { | ||
| 7 | return `${parser}::${code.length}::${code}`; | ||
| 8 | } | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Lazily load prettier + parsers and format code. | ||
| 12 | * Returns the original string if formatting fails. | ||
| 13 | */ | ||
| 14 | async function formatCode( | ||
| 15 | code: string, | ||
| 16 | parser: "json" | "typescript" | "babel", | ||
| 17 | ): Promise<string> { | ||
| 18 | const key = formatCacheKey(code, parser); | ||
| 19 | const cached = formatCache.get(key); | ||
| 20 | if (cached) return cached; | ||
| 21 | |||
| 22 | try { | ||
| 23 | const [prettier, parserPlugin] = await Promise.all([ | ||
| 24 | import("prettier/standalone"), | ||
| 25 | parser === "json" | ||
| 26 | ? import("prettier/plugins/babel") | ||
| 27 | : import("prettier/plugins/typescript"), | ||
| 28 | ]); | ||
| 29 | |||
| 30 | // both babel and typescript parsers need estree | ||
| 31 | const estree = await import("prettier/plugins/estree"); | ||
| 32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| 33 | const plugins: any[] = [parserPlugin, estree]; | ||
| 34 | |||
| 35 | let input = code; | ||
| 36 | let unwrap = false; | ||
| 37 | |||
| 38 | // Type signature strings (e.g. `{ body: { ... } }`) aren't valid TS | ||
| 39 | // on their own. Wrap as `type T = ...` so prettier can parse them. | ||
| 40 | if (parser === "typescript") { | ||
| 41 | input = `type __T = ${code}`; | ||
| 42 | unwrap = true; | ||
| 43 | } | ||
| 44 | |||
| 45 | const result = await prettier.format(input, { | ||
| 46 | parser: parser === "json" ? "json" : "typescript", | ||
| 47 | plugins, | ||
| 48 | printWidth: 60, | ||
| 49 | tabWidth: 2, | ||
| 50 | semi: true, | ||
| 51 | singleQuote: false, | ||
| 52 | trailingComma: "all", | ||
| 53 | }); | ||
| 54 | |||
| 55 | let trimmed = result.trimEnd(); | ||
| 56 | |||
| 57 | // Strip the `type __T = ` wrapper and trailing `;` | ||
| 58 | if (unwrap) { | ||
| 59 | trimmed = trimmed | ||
| 60 | .replace(/^type __T =\s*/, "") | ||
| 61 | .replace(/;$/, "") | ||
| 62 | .trimEnd(); | ||
| 63 | } | ||
| 64 | |||
| 65 | formatCache.set(key, trimmed); | ||
| 66 | return trimmed; | ||
| 67 | } catch { | ||
| 68 | // If formatting fails, return original | ||
| 69 | return code; | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Detect the right parser for a code string. | ||
| 75 | */ | ||
| 76 | function detectParser( | ||
| 77 | code: string, | ||
| 78 | langHint?: string, | ||
| 79 | ): "json" | "typescript" | null { | ||
| 80 | if (langHint === "json" || langHint === "jsonc") return "json"; | ||
| 81 | if (langHint === "typescript" || langHint === "ts") return "typescript"; | ||
| 82 | |||
| 83 | const trimmed = code.trimStart(); | ||
| 84 | if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json"; | ||
| 85 | |||
| 86 | return null; | ||
| 87 | } | ||
| 88 | |||
| 89 | /** | ||
| 90 | * Hook: formats code with prettier. Returns the formatted string | ||
| 91 | * (or the original while loading / if formatting fails). | ||
| 92 | */ | ||
| 93 | export function useFormattedCode( | ||
| 94 | code: string | null, | ||
| 95 | langHint?: string, | ||
| 96 | ): string | null { | ||
| 97 | const [formatted, setFormatted] = useState<string | null>(code); | ||
| 98 | |||
| 99 | useEffect(() => { | ||
| 100 | if (!code) { | ||
| 101 | setFormatted(null); | ||
| 102 | return; | ||
| 103 | } | ||
| 104 | |||
| 105 | const parser = detectParser(code, langHint); | ||
| 106 | if (!parser) { | ||
| 107 | setFormatted(code); | ||
| 108 | return; | ||
| 109 | } | ||
| 110 | |||
| 111 | // Check cache synchronously first | ||
| 112 | const key = formatCacheKey(code, parser); | ||
| 113 | const cached = formatCache.get(key); | ||
| 114 | if (cached) { | ||
| 115 | setFormatted(cached); | ||
| 116 | return; | ||
| 117 | } | ||
| 118 | |||
| 119 | // Set original immediately, then replace with formatted | ||
| 120 | setFormatted(code); | ||
| 121 | |||
| 122 | let cancelled = false; | ||
| 123 | formatCode(code, parser).then((result) => { | ||
| 124 | if (!cancelled) setFormatted(result); | ||
| 125 | }); | ||
| 126 | |||
| 127 | return () => { | ||
| 128 | cancelled = true; | ||
| 129 | }; | ||
| 130 | }, [code, langHint]); | ||
| 131 | |||
| 132 | return formatted; | ||
| 133 | } | ||
| @@ -56,7 +56,6 @@ · 55 unmodified lines | |||
| 56 | "class-variance-authority": "^0.7.1", | 56 | "class-variance-authority": "^0.7.1", |
| 57 | "clsx": "^2.1.1", | 57 | "clsx": "^2.1.1", |
| 58 | "effect": "catalog:", | 58 | "effect": "catalog:", |
| 59 | "prettier": "^3.8.1", | ||
| 60 | "react": "^19.2.0", | 59 | "react": "^19.2.0", |
| 61 | "react-dom": "^19.2.0", | 60 | "react-dom": "^19.2.0", |
| 62 | "react-grab": "^0.1.26", | 61 | "react-grab": "^0.1.26", |
| @@ -149,6 +148,7 @@ · 86 unmodified lines | |||
| 149 | "drizzle-orm": "1.0.0-beta.15-859cf75", | 148 | "drizzle-orm": "1.0.0-beta.15-859cf75", |
| 150 | "effect": "catalog:", | 149 | "effect": "catalog:", |
| 151 | "postgres": "^3.4.7", | 150 | "postgres": "^3.4.7", |
| 151 | "prettier": "^3.8.1", | ||
| 152 | }, | 152 | }, |
| 153 | "devDependencies": { | 153 | "devDependencies": { |
| 154 | "@effect/platform-node": "catalog:", | 154 | "@effect/platform-node": "catalog:", |
| @@ -31,7 +31,8 @@ · 30 unmodified lines | |||
| 31 | "@executor-v3/runtime-local-inproc": "workspace:*", | 31 | "@executor-v3/runtime-local-inproc": "workspace:*", |
| 32 | "drizzle-orm": "1.0.0-beta.15-859cf75", | 32 | "drizzle-orm": "1.0.0-beta.15-859cf75", |
| 33 | "effect": "catalog:", | 33 | "effect": "catalog:", |
| 34 | "postgres": "^3.4.7" | 34 | "postgres": "^3.4.7", |
| 35 | "prettier": "^3.8.1" | ||
| 35 | }, | 36 | }, |
| 36 | "devDependencies": { | 37 | "devDependencies": { |
| 37 | "@effect/vitest": "catalog:", | 38 | "@effect/vitest": "catalog:", |