| @@ -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 | | −} |