Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,10 @@
"packageManager": "pnpm@10.4.0",
"engines": {
"node": ">=20.19.0"
},
"pnpm": {
"overrides": {
"vite": "npm:rolldown-vite@7.0.6"
}
}
}
3 changes: 2 additions & 1 deletion packages/prototypey/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"main": "lib/index.js",
"exports": {
".": "./lib/index.js",
"./infer": "./lib/infer.js"
"./infer": "./lib/infer.js",
"./lib/lib.d.ts": "./lib/lib.d.ts"
},
"files": [
"lib/",
Expand Down
19 changes: 10 additions & 9 deletions packages/prototypey/src/infer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,13 @@ type ReplaceRefsInType<T, Defs, Visited = never> =
* Infers the TypeScript type for a lexicon namespace, returning only the 'main' definition
* with all local refs (#user, #post, etc.) resolved to their actual types.
*/
export type Infer<T extends { id: string; defs: Record<string, unknown> }> =
Prettify<
"main" extends keyof T["defs"]
? { $type: T["id"] } & ReplaceRefsInType<
InferType<T["defs"]["main"]>,
{ [K in keyof T["defs"]]: InferType<T["defs"][K]> }
>
: never
>;
export type Infer<
T extends { json: { id: string; defs: Record<string, unknown> } },
> = Prettify<
"main" extends keyof T["json"]["defs"]
? { $type: T["json"]["id"] } & ReplaceRefsInType<
InferType<T["json"]["defs"]["main"]>,
{ [K in keyof T["json"]["defs"]]: InferType<T["json"]["defs"][K]> }
>
: never
>;
2 changes: 1 addition & 1 deletion packages/prototypey/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ interface SubscriptionOptions {

class Namespace<T extends LexiconNamespace> {
public json: T;
public infer: Infer<T> = null as unknown as Infer<T>;
public infer: Infer<{ json: T }> = null as unknown as Infer<{ json: T }>;

constructor(json: T) {
this.json = json;
Expand Down
1 change: 1 addition & 0 deletions packages/site/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
12 changes: 12 additions & 0 deletions packages/site/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>prototypey - Type-safe lexicon inference for ATProto</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
31 changes: 31 additions & 0 deletions packages/site/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@prototypey/site",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest"
},
"dependencies": {
"@monaco-editor/react": "^4.6.0",
"monaco-editor": "0.52.0",
"prototypey": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^5.0.4",
"jsdom": "^25.0.1",
"typescript": "5.8.3",
"vite": "^6.0.5",
"vitest": "^3.2.4"
}
}
11 changes: 11 additions & 0 deletions packages/site/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Header } from "./components/Header";
import { Playground } from "./components/Playground";

export function App() {
return (
<>
<Header />
<Playground />
</>
);
}
157 changes: 157 additions & 0 deletions packages/site/src/components/Editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import MonacoEditor, { useMonaco, loader } from "@monaco-editor/react";
import { useEffect, useState } from "react";
import * as monaco from "monaco-editor";
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";

// Configure Monaco Environment for web workers
if (typeof self !== "undefined") {
self.MonacoEnvironment = {
getWorker(_: string, label: string) {
if (label === "json") {
return new jsonWorker();
}
if (label === "typescript" || label === "javascript") {
return new tsWorker();
}
return new editorWorker();
},
};
}

// Configure loader to use local monaco-editor 0.52.0 instead of CDN 0.54.0
if (loader?.config) {
loader.config({ monaco });
}

interface EditorProps {
value: string;
onChange: (value: string) => void;
onReady?: () => void;
}

export function Editor({ value, onChange, onReady }: EditorProps) {
const [isReady, setIsReady] = useState(false);
const monaco = useMonaco();

useEffect(() => {
if (!monaco) return;

monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES2020,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.ESNext,
noEmit: true,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
strict: false,
});

monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
noSemanticValidation: false,
noSyntaxValidation: false,
});

Promise.all([
fetch("/types/type-utils.d.ts").then((r) => r.text()),
fetch("/types/infer.d.ts").then((r) => r.text()),
fetch("/types/lib.d.ts").then((r) => r.text()),
]).then(([typeUtilsDts, inferDts, libDts]) => {
const stripImportsExports = (content: string) =>
content
.replace(/import\s+{[^}]*}\s+from\s+['""][^'"]*['""];?\s*/g, "")
.replace(/import\s+.*\s+from\s+['""][^'"]*['""];?\s*/g, "")
.replace(/^export\s+{[^}]*};?\s*/gm, "")
.replace(/^export\s+/gm, "")
.replace(/\/\/# sourceMappingURL=.*/g, "")
.replace(/\/\/#region.*\n?/g, "")
.replace(/\/\/#endregion.*\n?/g, "");

const combinedTypes = `
${stripImportsExports(typeUtilsDts)}
${stripImportsExports(inferDts)}
${stripImportsExports(libDts)}
`;

const moduleDeclaration = `declare module "prototypey" {
${combinedTypes}
}`;

monaco.languages.typescript.typescriptDefaults.addExtraLib(
moduleDeclaration,
"prototypey.d.ts",
);

setIsReady(true);
onReady?.();
});
}, [monaco, onReady]);

if (!isReady) {
return (
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
<div
style={{
padding: "0.75rem 1rem",
backgroundColor: "#f9fafb",
borderBottom: "1px solid #e5e7eb",
fontSize: "0.875rem",
fontWeight: "600",
color: "#374151",
}}
>
Input
</div>
<div
style={{
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
Loading...
</div>
</div>
);
}

return (
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
<div
style={{
padding: "0.75rem 1rem",
backgroundColor: "#f9fafb",
borderBottom: "1px solid #e5e7eb",
fontSize: "0.875rem",
fontWeight: "600",
color: "#374151",
}}
>
Input
</div>
<div style={{ flex: 1 }}>
<MonacoEditor
height="100%"
defaultLanguage="typescript"
path="file:///main.ts"
value={value}
onChange={(value) => onChange(value || "")}
theme="vs-light"
options={{
minimap: { enabled: false },
fontSize: 14,
lineNumbers: "on",
renderLineHighlight: "all",
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
padding: { top: 16, bottom: 16 },
}}
/>
</div>
</div>
);
}
31 changes: 31 additions & 0 deletions packages/site/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export function Header() {
return (
<header
style={{
padding: "2rem 2rem 1rem 2rem",
borderBottom: "1px solid #e5e7eb",
}}
>
<div style={{ maxWidth: "1400px", margin: "0 auto" }}>
<h1
style={{
fontSize: "2.5rem",
fontWeight: "700",
marginBottom: "0.5rem",
}}
>
<span style={{ color: "#6b7280" }}>at://</span>prototypey
</h1>
<p
style={{
fontSize: "1.125rem",
color: "#6b7280",
marginTop: "0.5rem",
}}
>
Type-safe lexicon inference for ATProto schemas
</p>
</div>
</header>
);
}
60 changes: 60 additions & 0 deletions packages/site/src/components/OutputPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import MonacoEditor from "@monaco-editor/react";

interface OutputPanelProps {
output: {
json: string;
typeInfo: string;
error: string;
};
}

export function OutputPanel({ output }: OutputPanelProps) {
return (
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
<div
style={{
padding: "0.75rem 1rem",
backgroundColor: "#f9fafb",
borderBottom: "1px solid #e5e7eb",
fontSize: "0.875rem",
fontWeight: "600",
color: "#374151",
}}
>
Output
</div>
<div style={{ flex: 1 }}>
{output.error ? (
<div
style={{
padding: "1rem",
color: "#dc2626",
backgroundColor: "#fef2f2",
height: "100%",
overflow: "auto",
}}
>
<strong>Error:</strong> {output.error}
</div>
) : (
<MonacoEditor
height="100%"
defaultLanguage="json"
value={output.json}
theme="vs-light"
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 14,
lineNumbers: "on",
renderLineHighlight: "none",
scrollBeyondLastLine: false,
automaticLayout: true,
padding: { top: 16, bottom: 16 },
}}
/>
)}
</div>
</div>
);
}
Loading