prototypey.org - atproto lexicon typescript toolkit - mirror https://github.com/tylersayshi/prototypey
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Revert "more site readiness"

This reverts commit ce4128f76879e6a377cadb815673700e545fb2d9.

Tyler 1f280a0d ce4128f7

+66 -181
+9 -10
packages/prototypekit/src/infer.ts
··· 130 130 * Infers the TypeScript type for a lexicon namespace, returning only the 'main' definition 131 131 * with all local refs (#user, #post, etc.) resolved to their actual types. 132 132 */ 133 - export type Infer< 134 - T extends { json: { id: string; defs: Record<string, unknown> } }, 135 - > = Prettify< 136 - "main" extends keyof T["json"]["defs"] 137 - ? { $type: T["json"]["id"] } & ReplaceRefsInType< 138 - InferType<T["json"]["defs"]["main"]>, 139 - { [K in keyof T["json"]["defs"]]: InferType<T["json"]["defs"][K]> } 140 - > 141 - : never 142 - >; 133 + export type Infer<T extends { id: string; defs: Record<string, unknown> }> = 134 + Prettify< 135 + "main" extends keyof T["defs"] 136 + ? { $type: T["id"] } & ReplaceRefsInType< 137 + InferType<T["defs"]["main"]>, 138 + { [K in keyof T["defs"]]: InferType<T["defs"][K]> } 139 + > 140 + : never 141 + >;
+2 -5
packages/site/src/components/Editor.tsx
··· 5 5 interface EditorProps { 6 6 value: string; 7 7 onChange: (value: string) => void; 8 - onReady?: () => void; 9 8 } 10 9 11 - export function Editor({ value, onChange, onReady }: EditorProps) { 10 + export function Editor({ value, onChange }: EditorProps) { 12 11 const [isReady, setIsReady] = useState(false); 13 12 14 13 useEffect(() => { ··· 61 60 ); 62 61 63 62 setIsReady(true); 64 - onReady?.(); 65 63 }); 66 64 }); 67 - }, [onReady]); 65 + }, []); 68 66 69 67 if (!isReady) { 70 68 return ( ··· 113 111 <MonacoEditor 114 112 height="100%" 115 113 defaultLanguage="typescript" 116 - path="file:///main.ts" 117 114 value={value} 118 115 onChange={(value) => onChange(value || "")} 119 116 theme="vs-light"
+42 -7
packages/site/src/components/OutputPanel.tsx
··· 1 + import { useState } from "react"; 1 2 import MonacoEditor from "@monaco-editor/react"; 2 3 3 4 interface OutputPanelProps { ··· 9 10 } 10 11 11 12 export function OutputPanel({ output }: OutputPanelProps) { 13 + const [activeTab, setActiveTab] = useState<"json" | "types">("json"); 14 + 12 15 return ( 13 16 <div style={{ flex: 1, display: "flex", flexDirection: "column" }}> 14 17 <div 15 18 style={{ 16 - padding: "0.75rem 1rem", 19 + display: "flex", 17 20 backgroundColor: "#f9fafb", 18 21 borderBottom: "1px solid #e5e7eb", 19 - fontSize: "0.875rem", 20 - fontWeight: "600", 21 - color: "#374151", 22 22 }} 23 23 > 24 - Output 24 + <button 25 + onClick={() => setActiveTab("json")} 26 + style={{ 27 + padding: "0.75rem 1rem", 28 + fontSize: "0.875rem", 29 + fontWeight: "600", 30 + color: activeTab === "json" ? "#1f2937" : "#6b7280", 31 + backgroundColor: activeTab === "json" ? "#ffffff" : "transparent", 32 + border: "none", 33 + borderBottom: 34 + activeTab === "json" 35 + ? "2px solid #3b82f6" 36 + : "2px solid transparent", 37 + cursor: "pointer", 38 + }} 39 + > 40 + JSON Output 41 + </button> 42 + <button 43 + onClick={() => setActiveTab("types")} 44 + style={{ 45 + padding: "0.75rem 1rem", 46 + fontSize: "0.875rem", 47 + fontWeight: "600", 48 + color: activeTab === "types" ? "#1f2937" : "#6b7280", 49 + backgroundColor: activeTab === "types" ? "#ffffff" : "transparent", 50 + border: "none", 51 + borderBottom: 52 + activeTab === "types" 53 + ? "2px solid #3b82f6" 54 + : "2px solid transparent", 55 + cursor: "pointer", 56 + }} 57 + > 58 + Type Info 59 + </button> 25 60 </div> 26 61 <div style={{ flex: 1 }}> 27 62 {output.error ? ( ··· 39 74 ) : ( 40 75 <MonacoEditor 41 76 height="100%" 42 - defaultLanguage="json" 43 - value={output.json} 77 + defaultLanguage={activeTab === "json" ? "json" : "typescript"} 78 + value={activeTab === "json" ? output.json : output.typeInfo} 44 79 theme="vs-light" 45 80 options={{ 46 81 readOnly: true,
+12 -116
packages/site/src/components/Playground.tsx
··· 1 - import { useState, useEffect, useRef } from "react"; 1 + import { useState, useEffect } from "react"; 2 2 import { Editor } from "./Editor"; 3 3 import { OutputPanel } from "./OutputPanel"; 4 4 import { lx } from "prototypekit"; 5 - import { useMonaco } from "@monaco-editor/react"; 6 - import type * as Monaco from "monaco-editor"; 7 - 8 - let tsWorkerInstance: Monaco.languages.typescript.TypeScriptWorker | null = 9 - null; 10 5 11 6 export function Playground() { 12 7 const [code, setCode] = useState(DEFAULT_CODE); 13 8 const [output, setOutput] = useState({ json: "", typeInfo: "", error: "" }); 14 - const [editorReady, setEditorReady] = useState(false); 15 - const monaco = useMonaco(); 16 - const tsWorkerRef = 17 - useRef<Monaco.languages.typescript.TypeScriptWorker | null>(null); 18 9 19 10 const handleCodeChange = (newCode: string) => { 20 11 setCode(newCode); 21 12 }; 22 13 23 - const handleEditorReady = () => { 24 - setEditorReady(true); 25 - }; 26 - 27 14 useEffect(() => { 28 - if (monaco && editorReady && !tsWorkerRef.current && !tsWorkerInstance) { 29 - const initWorker = async () => { 30 - try { 31 - await new Promise((resolve) => setTimeout(resolve, 200)); 32 - const worker = 33 - await monaco.languages.typescript.getTypeScriptWorker(); 34 - const uri = monaco.Uri.parse("file:///main.ts"); 35 - const client = await worker(uri); 36 - tsWorkerRef.current = client; 37 - tsWorkerInstance = client; 38 - } catch (err) { 39 - console.error("Failed to initialize TypeScript worker:", err); 40 - } 41 - }; 42 - initWorker(); 43 - } 44 - }, [monaco, editorReady]); 45 - 46 - useEffect(() => { 47 - const timeoutId = setTimeout(async () => { 15 + const timeoutId = setTimeout(() => { 48 16 try { 49 - const cleanedCode = code 50 - .replace(/import\s+{[^}]*}\s+from\s+['"][^'"]+['"]\s*;?\s*/g, "") 51 - .replace(/^type\s+\w+\s*=\s*[^;]+;?\s*$/gm, ""); 17 + const cleanedCode = code.replace( 18 + /import\s+{[^}]*}\s+from\s+['"][^'"]+['"]\s*;?\s*/g, 19 + "", 20 + ); 52 21 53 22 const lastVarMatch = cleanedCode.match(/(?:const|let|var)\s+(\w+)\s*=/); 54 23 const lastVarName = lastVarMatch ? lastVarMatch[1] : null; ··· 59 28 60 29 const fn = new Function("lx", wrappedCode); 61 30 const result = fn(lx); 62 - let typeInfo = "// Hover over .infer in the editor to see the type"; 63 - 64 - if (lastVarName && monaco && tsWorkerRef.current) { 65 - try { 66 - const uri = monaco.Uri.parse("file:///main.ts"); 67 - const existingModel = monaco.editor.getModel(uri); 68 - 69 - if (existingModel) { 70 - const inferPosition = code.indexOf(`${lastVarName}.infer`); 71 - if (inferPosition !== -1) { 72 - const offset = 73 - inferPosition + `${lastVarName}.infer`.length - 1; 74 - 75 - const quickInfo = 76 - await tsWorkerRef.current.getQuickInfoAtPosition( 77 - uri.toString(), 78 - offset, 79 - ); 80 - 81 - if (quickInfo?.displayParts) { 82 - const typeText = quickInfo.displayParts 83 - .map((part: { text: string }) => part.text) 84 - .join(""); 85 - 86 - const propertyMatch = typeText.match( 87 - /\(property\)\s+.*?\.infer:\s*([\s\S]+?)$/, 88 - ); 89 - if (propertyMatch) { 90 - typeInfo = formatTypeString(propertyMatch[1]); 91 - } 92 - } 93 - } 94 - } 95 - } catch (err) { 96 - console.error("Type extraction error:", err); 97 - } 98 - } 99 31 100 32 if (result && typeof result === "object" && "json" in result) { 101 33 const jsonOutput = (result as { json: unknown }).json; 102 34 setOutput({ 103 35 json: JSON.stringify(jsonOutput, null, 2), 104 - typeInfo, 36 + typeInfo: "// Type inference not yet implemented in playground", 105 37 error: "", 106 38 }); 107 39 } else { 108 40 setOutput({ 109 41 json: JSON.stringify(result, null, 2), 110 - typeInfo, 42 + typeInfo: "// Type inference not yet implemented in playground", 111 43 error: "", 112 44 }); 113 45 } ··· 121 53 }, 500); 122 54 123 55 return () => clearTimeout(timeoutId); 124 - }, [code, monaco]); 56 + }, [code]); 125 57 126 58 return ( 127 59 <div ··· 138 70 borderRight: "1px solid #e5e7eb", 139 71 }} 140 72 > 141 - <Editor 142 - value={code} 143 - onChange={handleCodeChange} 144 - onReady={handleEditorReady} 145 - /> 73 + <Editor value={code} onChange={handleCodeChange} /> 146 74 </div> 147 75 <div style={{ flex: 1, display: "flex" }}> 148 76 <OutputPanel output={output} /> ··· 151 79 ); 152 80 } 153 81 154 - function formatTypeString(typeStr: string): string { 155 - let formatted = typeStr.trim(); 156 - 157 - formatted = formatted.replace(/\s+/g, " "); 158 - formatted = formatted.replace(/;\s*/g, "\n"); 159 - formatted = formatted.replace(/{\s*/g, "{\n"); 160 - formatted = formatted.replace(/\s*}/g, "\n}"); 161 - 162 - const lines = formatted.split("\n"); 163 - let indentLevel = 0; 164 - const indentedLines: string[] = []; 165 - 166 - for (const line of lines) { 167 - const trimmed = line.trim(); 168 - if (!trimmed) continue; 169 - 170 - if (trimmed.startsWith("}")) { 171 - indentLevel = Math.max(0, indentLevel - 1); 172 - } 173 - 174 - indentedLines.push(" ".repeat(indentLevel) + trimmed); 175 - 176 - if (trimmed.endsWith("{") && !trimmed.includes("}")) { 177 - indentLevel++; 178 - } 179 - } 180 - 181 - return indentedLines.join("\n"); 182 - } 183 - 184 - const DEFAULT_CODE = `import { lx, type Infer } from "prototypekit"; 82 + const DEFAULT_CODE = `import { lx } from "prototypekit"; 185 83 186 84 const profileNamespace = lx.namespace("app.bsky.actor.profile", { 187 85 main: lx.record({ ··· 191 89 description: lx.string({ maxLength: 256, maxGraphemes: 256 }), 192 90 }), 193 91 }), 194 - }); 195 - 196 - type ProfileInferred = Infer<typeof profileNamespace>;`; 92 + });`;
-34
packages/site/tests/components/Playground.test.tsx
··· 10 10 onChange={(e) => onChange(e.target.value)} 11 11 /> 12 12 ), 13 - useMonaco: () => null, 14 - loader: { 15 - init: vi.fn(() => 16 - Promise.resolve({ 17 - languages: { 18 - typescript: { 19 - typescriptDefaults: { 20 - setCompilerOptions: vi.fn(), 21 - setDiagnosticsOptions: vi.fn(), 22 - addExtraLib: vi.fn(), 23 - }, 24 - ScriptTarget: { ES2020: 5 }, 25 - ModuleResolutionKind: { NodeJs: 2 }, 26 - ModuleKind: { ESNext: 99 }, 27 - getTypeScriptWorker: vi.fn(() => 28 - Promise.resolve(() => 29 - Promise.resolve({ 30 - getQuickInfoAtPosition: vi.fn(() => Promise.resolve(null)), 31 - }), 32 - ), 33 - ), 34 - }, 35 - }, 36 - editor: { 37 - defineTheme: vi.fn(), 38 - createModel: vi.fn(() => ({ dispose: vi.fn() })), 39 - getModel: vi.fn(() => null), 40 - }, 41 - Uri: { 42 - parse: vi.fn((uri: string) => ({ toString: () => uri })), 43 - }, 44 - }), 45 - ), 46 - }, 47 13 })); 48 14 49 15 describe("Playground", () => {
-8
packages/site/tests/setup.ts
··· 1 - import { vi } from "vitest"; 2 - 3 - global.fetch = vi.fn( 4 - () => 5 - Promise.resolve({ 6 - text: () => Promise.resolve(""), 7 - }) as any, 8 - );
+1 -1
packages/site/vitest.config.ts
··· 6 6 test: { 7 7 environment: "jsdom", 8 8 globals: true, 9 - setupFiles: ["./tests/setup.ts"], 9 + setupFiles: [], 10 10 }, 11 11 });