···11+---
22+"prototypey": minor
33+---
44+55+generate prototypey lexicon utils from json definitions
+53-11
packages/prototypey/README.md
···138138prototypey gen-emit ./lexicons ./src/lexicons/**/*.ts
139139```
140140141141-### Typical Workflow
141141+#### `gen-from-json` - Generate TypeScript from JSON schemas
142142+143143+```bash
144144+prototypey gen-from-json <outdir> <sources...>
145145+```
146146+147147+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.
148148+149149+**Example:**
150150+151151+```bash
152152+prototypey gen-from-json ./src/lexicons ./lexicons/**/*.json
153153+```
154154+155155+This will create TypeScript files that export typed lexicon objects:
156156+157157+```ts
158158+// Generated file: src/lexicons/app.bsky.feed.post.ts
159159+import { fromJSON } from "prototypey";
160160+161161+export const appBskyFeedPost = fromJSON({
162162+ // ... lexicon JSON
163163+});
164164+```
165165+166166+### Typical Workflows
167167+168168+#### TypeScript-first workflow
1421691431701. Author lexicons in TypeScript using the library
1441712. Emit JSON schemas with `gen-emit` for runtime validation
···159186npm run lexicon:emit
160187```
161188189189+#### JSON-first workflow
190190+191191+1. Start with JSON lexicon schemas (e.g., from atproto)
192192+2. Generate TypeScript with `gen-from-json` for type-safe access
193193+194194+**Recommended:** Add as a script to your `package.json`:
195195+196196+```json
197197+{
198198+ "scripts": {
199199+ "lexicon:import": "prototypey gen-from-json ./src/lexicons ./lexicons/**/*.json"
200200+ }
201201+}
202202+```
203203+204204+Then run:
205205+206206+```bash
207207+npm run lexicon:import
208208+```
209209+162210## State of the Project
163211164212**Done:**
···169217- Inferrance of valid type from full lexicon definition
170218 - the really cool part of this is that it fills in the refs from the defs all at the type level
171219- `lx.lexicon(...).validate(data)` for validating data using `@atproto/lexicon` and your lexicon definitions
172172-173173-**TODO/In Progress:**
174174-175175-- 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
220220+- `fromJSON()` helper for creating lexicons directly from JSON objects with full type inference
177221178178-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 🙂.
222222+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 🙂.
179223180180----
224224+**Call For Contribution:**
181225182182-> 💝 This package was templated with
183183-> [`create-typescript-app`](https://github.com/JoshuaKGoldberg/create-typescript-app)
184184-> using the [Bingo framework](https://create.bingo).
226226+- 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);