History

move prettier from web to control-plane, remove client-side formatting

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.

Rhys Sullivan committed 8 months ago

Commit bf1e7b21 parent c0a9ebd5 files · +5 −141Browse files

Unified · Split

5 files
  1. apps/web/package.json +0 −1
  2. apps/web/src/components/document-panel.tsx +2 −5
  3. apps/web/src/lib/format.ts +0 −133
  4. bun.lock +1 −1
  5. packages/control-plane/package.json +2 −1
apps/web/package.json+0−1
@@ -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",
apps/web/src/components/document-panel.tsx+2−5
@@ -1,5 +1,4 @@
1import { cn } from "../lib/utils";1import { cn } from "../lib/utils";
2import { useFormattedCode } from "../lib/format";
3import { CodeBlock } from "./code-block";2import { CodeBlock } from "./code-block";
4 3
5export function DocumentPanel(props: {4export 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 <section12 <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}
apps/web/src/lib/format.ts+0−133
@@ -1,133 +0,0 @@
1import { useState, useEffect } from "react";
2
3// Cache formatted results so we don't re-format on every render
4const formatCache = new Map<string, string>();
5
6function 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 */
14async 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 */
76function 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 */
93export 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}
bun.lock+1−1
@@ -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:",
packages/control-plane/package.json+2−1
@@ -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:",