···11# prototypey
2233+## 0.3.8
44+55+### Patch Changes
66+77+- 7a19f90: releast changes from dep updates and #66
88+99+## 0.3.7
1010+1111+### Patch Changes
1212+1313+- e75de54: update docs
1414+1515+## 0.3.6
1616+1717+### Patch Changes
1818+1919+- 2b55317: fix exported type bug
2020+2121+## 0.3.5
2222+2323+### Patch Changes
2424+2525+- abb4b31: updated docs
2626+2727+## 0.3.4
2828+2929+### Patch Changes
3030+3131+- 3329654: fix for type of record key and description hint
3232+3333+## 0.3.3
3434+3535+### Patch Changes
3636+3737+- e7a7497: documentation update
3838+3939+## 0.3.2
4040+4141+### Patch Changes
4242+4343+- 6a6cae5: update deps
4444+4545+## 0.3.1
4646+4747+### Patch Changes
4848+4949+- d5d3143: update docs - we're featured!
5050+5151+## 0.3.0
5252+5353+### Minor Changes
5454+5555+- 91a8c84: generate prototypey lexicon utils from json definitions
5656+5757+## 0.2.6
5858+5959+### Patch Changes
6060+6161+- 6c5569b: only export intended items
6262+363## 0.2.5
464565### Patch Changes
+61-17
packages/prototypey/README.md
···11# prototypey
2233-A (soon-to-be) fully-featured sdk for developing lexicons with typescript.
33+A fully-featured sdk for developing lexicons with typescript.
44+55+Below this is the docs and features of the library. If you'd like the story for why prototypey exists and what it's good for: [that's published here](https://notes.tylur.dev/3m5a3do4eus2w)
66+77+## Features
88+99+- atproto spec lexicon authoring with in IDE docs & hints for each attribute (ts => json)
1010+- CLI to generate json from ts definitions
1111+- CLI to generate ts from json definitions
1212+- inference of usage type from full lexicon definition
1313+ - the really cool part of this is that it fills in the refs from the defs all at the type level
1414+- `lx.lexicon(...).validate(data)` for validating data using `@atproto/lexicon`
1515+- `fromJSON()` helper for creating lexicons directly from JSON objects with full type inference
416517## Installation
618···138150prototypey gen-emit ./lexicons ./src/lexicons/**/*.ts
139151```
140152141141-### Typical Workflow
153153+#### `gen-from-json` - Generate TypeScript from JSON schemas
154154+155155+```bash
156156+prototypey gen-from-json <outdir> <sources...>
157157+```
158158+159159+Generates TypeScript files from JSON lexicon schemas using the `fromJSON` helper. This is useful when you have existing lexicon JSON files and want to work with them in TypeScript with full type inference.
160160+161161+**Example:**
162162+163163+```bash
164164+prototypey gen-from-json ./src/lexicons ./lexicons/**/*.json
165165+```
166166+167167+This will create TypeScript files that export typed lexicon objects:
168168+169169+```ts
170170+// Generated file: src/lexicons/app.bsky.feed.post.ts
171171+import { fromJSON } from "prototypey";
172172+173173+export const appBskyFeedPost = fromJSON({
174174+ // ... lexicon JSON
175175+});
176176+```
177177+178178+### Typical Workflows
179179+180180+#### TypeScript-first workflow
1421811431821. Author lexicons in TypeScript using the library
1441832. Emit JSON schemas with `gen-emit` for runtime validation
···159198npm run lexicon:emit
160199```
161200162162-## State of the Project
201201+#### JSON-first workflow
163202164164-**Done:**
203203+1. Start with JSON lexicon schemas (e.g., from atproto)
204204+2. Generate TypeScript with `gen-from-json` for type-safe access
165205166166-- Full atproto spec lexicon authoring with in IDE docs & hints for each attribute (ts => json)
167167-- CLI generates json from ts definitions
168168-- CLI generates ts from json definitions
169169-- Inferrance of valid type from full lexicon definition
170170- - the really cool part of this is that it fills in the refs from the defs all at the type level
171171-- `lx.lexicon(...).validate(data)` for validating data using `@atproto/lexicon` and your lexicon definitions
206206+**Recommended:** Add as a script to your `package.json`:
172207173173-**TODO/In Progress:**
208208+```json
209209+{
210210+ "scripts": {
211211+ "lexicon:import": "prototypey gen-from-json ./src/lexicons ./lexicons/**/*.json"
212212+ }
213213+}
214214+```
174215175175-- Library art! Please reach out if you'd be willing to contribute some drawings or anything!
176176-- Add CLI support for inferring and validating from json as the starting point
216216+Then run:
177217178178-Please give any and all feedback. I've not really written many lexicons much myself yet, so this project is at a point of "well I think this makes sense" ๐. Both the [issues page](https://github.com/tylersayshi/prototypey/issues) and [discussions](https://github.com/tylersayshi/prototypey/discussions) are open and ready for y'all ๐.
218218+```bash
219219+npm run lexicon:import
220220+```
179221180222---
181223182182-> ๐ This package was templated with
183183-> [`create-typescript-app`](https://github.com/JoshuaKGoldberg/create-typescript-app)
184184-> using the [Bingo framework](https://create.bingo).
224224+Please give any and all feedback. I've not really written many lexicons much myself yet, so this project is at a point of "well I think this makes sense". Both the [issues page](https://github.com/tylersayshi/prototypey/issues) and [discussions](https://github.com/tylersayshi/prototypey/discussions) are open and ready for y'all ๐.
225225+226226+**Call For Contribution:**
227227+228228+We need library art! Please reach out if you'd be willing to contribute some drawings or anything :)
+111
packages/prototypey/cli/gen-from-json.ts
···11+import { glob } from "tinyglobby";
22+import { mkdir, writeFile, readFile } from "node:fs/promises";
33+import { join, dirname, basename } from "node:path";
44+55+interface LexiconJSON {
66+ lexicon: number;
77+ id: string;
88+ defs: Record<string, unknown>;
99+}
1010+1111+/**
1212+ * Converts a lexicon ID to a valid TypeScript export name
1313+ * e.g., "app.bsky.feed.post" -> "appBskyFeedPost"
1414+ * "com.atproto.repo.createRecord" -> "comAtprotoRepoCreateRecord"
1515+ */
1616+function lexiconIdToExportName(id: string): string {
1717+ // Split by dots and handle camelCase conversion
1818+ const parts = id.split(".");
1919+2020+ // For the first part (e.g., "app", "com"), keep it lowercase
2121+ // For subsequent parts, capitalize the first letter of each word
2222+ // But preserve any existing camelCase within parts
2323+ return parts
2424+ .map((part, index) => {
2525+ if (index === 0) return part;
2626+ // Capitalize first letter of the part
2727+ return part.charAt(0).toUpperCase() + part.slice(1);
2828+ })
2929+ .join("");
3030+}
3131+3232+export async function genFromJSON(
3333+ outdir: string,
3434+ sources: string | string[],
3535+): Promise<void> {
3636+ try {
3737+ const sourcePatterns = Array.isArray(sources) ? sources : [sources];
3838+3939+ // Find all JSON files matching the patterns
4040+ const jsonFiles = await glob(sourcePatterns, {
4141+ absolute: true,
4242+ onlyFiles: true,
4343+ });
4444+4545+ if (jsonFiles.length === 0) {
4646+ console.log("No JSON files found matching patterns:", sourcePatterns);
4747+ return;
4848+ }
4949+5050+ console.log(`Found ${String(jsonFiles.length)} JSON file(s)`);
5151+5252+ // Ensure output directory exists
5353+ await mkdir(outdir, { recursive: true });
5454+5555+ // Process each JSON file
5656+ for (const jsonPath of jsonFiles) {
5757+ await processJSONFile(jsonPath, outdir);
5858+ }
5959+6060+ console.log(`\nGenerated TypeScript files in ${outdir}`);
6161+ } catch (error) {
6262+ console.error("Error generating TypeScript from JSON:", error);
6363+ process.exit(1);
6464+ }
6565+}
6666+6767+async function processJSONFile(
6868+ jsonPath: string,
6969+ outdir: string,
7070+): Promise<void> {
7171+ try {
7272+ // Read and parse the JSON file
7373+ const content = await readFile(jsonPath, "utf-8");
7474+ const lexiconJSON = JSON.parse(content);
7575+7676+ // Validate it's a lexicon
7777+ if (
7878+ !lexiconJSON.lexicon ||
7979+ !lexiconJSON.id ||
8080+ !lexiconJSON.defs ||
8181+ typeof lexiconJSON.defs !== "object"
8282+ ) {
8383+ console.warn(` โ ${jsonPath}: Not a valid lexicon JSON`);
8484+ return;
8585+ }
8686+8787+ const { id } = lexiconJSON as LexiconJSON;
8888+ const exportName = lexiconIdToExportName(id);
8989+9090+ // Generate TypeScript content
9191+ const tsContent = `import { fromJSON } from "prototypey";
9292+9393+export const ${exportName} = fromJSON(${JSON.stringify(lexiconJSON, null, "\t")});
9494+`;
9595+9696+ // Determine output path - use same structure but in outdir
9797+ const outputFileName = `${basename(jsonPath, ".json")}.ts`;
9898+ const outputPath = join(outdir, outputFileName);
9999+100100+ // Ensure output directory exists
101101+ await mkdir(dirname(outputPath), { recursive: true });
102102+103103+ // Write the TypeScript file
104104+ await writeFile(outputPath, tsContent, "utf-8");
105105+106106+ console.log(` โ ${id} -> ${outputFileName}`);
107107+ } catch (error) {
108108+ console.error(` โ Error processing ${jsonPath}:`, error);
109109+ throw error;
110110+ }
111111+}
+7
packages/prototypey/cli/main.ts
···2233import sade from "sade";
44import { genEmit } from "./gen-emit.ts";
55+import { genFromJSON } from "./gen-from-json.ts";
56import pkg from "../package.json" with { type: "json" };
6778const prog = sade("prototypey");
···1314 .describe("Emit JSON lexicon schemas from authored TypeScript")
1415 .example("gen-emit ./lexicons ./src/lexicons/**/*.ts")
1516 .action(genEmit);
1717+1818+prog
1919+ .command("gen-from-json <outdir> <sources...>")
2020+ .describe("Generate TypeScript files from JSON lexicon schemas")
2121+ .example("gen-from-json ./src/lexicons ./lexicons/**/*.json")
2222+ .action(genFromJSON);
16231724prog.parse(process.argv);
···11-export * from "./lib.ts";
22-export * from "./infer.ts";
11+export { lx, fromJSON } from "./lib.ts";
22+export { type Infer } from "./infer.ts";
33+export type * from "@atproto/lexicon";