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.

restore site readiness changes

Tyler 98c0fbc5 0e12fa9c

+181 -66
+10 -9
packages/prototypey/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<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 - >; 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 + >;
+5 -2
packages/site/src/components/Editor.tsx
··· 5 5 interface EditorProps { 6 6 value: string; 7 7 onChange: (value: string) => void; 8 + onReady?: () => void; 8 9 } 9 10 10 - export function Editor({ value, onChange }: EditorProps) { 11 + export function Editor({ value, onChange, onReady }: EditorProps) { 11 12 const [isReady, setIsReady] = useState(false); 12 13 13 14 useEffect(() => { ··· 60 61 ); 61 62 62 63 setIsReady(true); 64 + onReady?.(); 63 65 }); 64 66 }); 65 - }, []); 67 + }, [onReady]); 66 68 67 69 if (!isReady) { 68 70 return ( ··· 111 113 <MonacoEditor 112 114 height="100%" 113 115 defaultLanguage="typescript" 116 + path="file:///main.ts" 114 117 value={value} 115 118 onChange={(value) => onChange(value || "")} 116 119 theme="vs-light"
+7 -42
packages/site/src/components/OutputPanel.tsx
··· 1 - import { useState } from "react"; 2 1 import MonacoEditor from "@monaco-editor/react"; 3 2 4 3 interface OutputPanelProps { ··· 10 9 } 11 10 12 11 export function OutputPanel({ output }: OutputPanelProps) { 13 - const [activeTab, setActiveTab] = useState<"json" | "types">("json"); 14 - 15 12 return ( 16 13 <div style={{ flex: 1, display: "flex", flexDirection: "column" }}> 17 14 <div 18 15 style={{ 19 - display: "flex", 16 + padding: "0.75rem 1rem", 20 17 backgroundColor: "#f9fafb", 21 18 borderBottom: "1px solid #e5e7eb", 19 + fontSize: "0.875rem", 20 + fontWeight: "600", 21 + color: "#374151", 22 22 }} 23 23 > 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> 24 + Output 60 25 </div> 61 26 <div style={{ flex: 1 }}> 62 27 {output.error ? ( ··· 74 39 ) : ( 75 40 <MonacoEditor 76 41 height="100%" 77 - defaultLanguage={activeTab === "json" ? "json" : "typescript"} 78 - value={activeTab === "json" ? output.json : output.typeInfo} 42 + defaultLanguage="json" 43 + value={output.json} 79 44 theme="vs-light" 80 45 options={{ 81 46 readOnly: true,
+116 -12
packages/site/src/components/Playground.tsx
··· 1 - import { useState, useEffect } from "react"; 1 + import { useState, useEffect, useRef } from "react"; 2 2 import { Editor } from "./Editor"; 3 3 import { OutputPanel } from "./OutputPanel"; 4 4 import { lx } from "prototypey"; 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; 5 10 6 11 export function Playground() { 7 12 const [code, setCode] = useState(DEFAULT_CODE); 8 13 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); 9 18 10 19 const handleCodeChange = (newCode: string) => { 11 20 setCode(newCode); 12 21 }; 13 22 23 + const handleEditorReady = () => { 24 + setEditorReady(true); 25 + }; 26 + 14 27 useEffect(() => { 15 - const timeoutId = setTimeout(() => { 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 () => { 16 48 try { 17 - const cleanedCode = code.replace( 18 - /import\s+{[^}]*}\s+from\s+['"][^'"]+['"]\s*;?\s*/g, 19 - "", 20 - ); 49 + const cleanedCode = code 50 + .replace(/import\s+{[^}]*}\s+from\s+['"][^'"]+['"]\s*;?\s*/g, "") 51 + .replace(/^type\s+\w+\s*=\s*[^;]+;?\s*$/gm, ""); 21 52 22 53 const lastVarMatch = cleanedCode.match(/(?:const|let|var)\s+(\w+)\s*=/); 23 54 const lastVarName = lastVarMatch ? lastVarMatch[1] : null; ··· 28 59 29 60 const fn = new Function("lx", wrappedCode); 30 61 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 + } 31 99 32 100 if (result && typeof result === "object" && "json" in result) { 33 101 const jsonOutput = (result as { json: unknown }).json; 34 102 setOutput({ 35 103 json: JSON.stringify(jsonOutput, null, 2), 36 - typeInfo: "// Type inference not yet implemented in playground", 104 + typeInfo, 37 105 error: "", 38 106 }); 39 107 } else { 40 108 setOutput({ 41 109 json: JSON.stringify(result, null, 2), 42 - typeInfo: "// Type inference not yet implemented in playground", 110 + typeInfo, 43 111 error: "", 44 112 }); 45 113 } ··· 53 121 }, 500); 54 122 55 123 return () => clearTimeout(timeoutId); 56 - }, [code]); 124 + }, [code, monaco]); 57 125 58 126 return ( 59 127 <div ··· 70 138 borderRight: "1px solid #e5e7eb", 71 139 }} 72 140 > 73 - <Editor value={code} onChange={handleCodeChange} /> 141 + <Editor 142 + value={code} 143 + onChange={handleCodeChange} 144 + onReady={handleEditorReady} 145 + /> 74 146 </div> 75 147 <div style={{ flex: 1, display: "flex" }}> 76 148 <OutputPanel output={output} /> ··· 79 151 ); 80 152 } 81 153 82 - const DEFAULT_CODE = `import { lx } from "prototypey"; 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 "prototypey"; 83 185 84 186 const profileNamespace = lx.namespace("app.bsky.actor.profile", { 85 187 main: lx.record({ ··· 89 191 description: lx.string({ maxLength: 256, maxGraphemes: 256 }), 90 192 }), 91 193 }), 92 - });`; 194 + }); 195 + 196 + type ProfileInferred = Infer<typeof profileNamespace>;`;
+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 + }, 13 47 })); 14 48 15 49 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: [], 9 + setupFiles: ["./tests/setup.ts"], 10 10 }, 11 11 });