···11+# Changesets
22+33+Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
44+with multi-package repos, or single-package repos to help you version and publish your code. You can
55+find the full documentation for it [in our repository](https://github.com/changesets/changesets)
66+77+We have a quick list of common questions to get you started engaging with this project in
88+[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
···11-# typed-lexicon
22-33-> [!WARNING]
44-> this project is in the middle of active initial development and not ready for
55-> use. there will be updates posted [here](https://bsky.app/profile/tylur.dev)
66-> if you'd like to follow along!
77-88-
99-1010-this will be a toolkit for writing lexicon json schema's in typescript and
1111-providing types for lexicon data shape. it will:
1212-1313-- remove boilerplate and improve ergonomics
1414-- type hint for
1515- [atproto type parameters](https://atproto.com/specs/lexicon#overview-of-types)
1616-- infer the typescript type definitions for the data shape to avoid duplication
1717- and skew
1818-- methods and a cli for generating json
1919-2020-With each of the above finished, i'll plan to write a `validate` method that
2121-will be published alongside this that takes any lexicon json definition and
2222-validates payloads off that.
2323-2424-My working hypothesis: it will be easier to write lexicons in typescript with a
2525-single api, then validate based off the json definition, than it would be to
2626-start with validation library types (standard-schema style) and attempt to use
2727-those as the authoring and validation tools.
2828-2929-**what you'd write:**
3030-3131-```typescript
3232-const profileNamespace = lx.lexicon("app.bsky.actor.profile", {
3333- main: lx.record({
3434- key: "self",
3535- record: lx.object({
3636- displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }),
3737- description: lx.string({ maxLength: 256, maxGraphemes: 256 }),
3838- }),
3939- }),
4040-});
4141-```
4242-4343-**generates to:**
4444-4545-```json
4646-{
4747- "lexicon": 1,
4848- "id": "app.bsky.actor.profile",
4949- "defs": {
5050- "main": {
5151- "type": "record",
5252- "key": "self",
5353- "record": {
5454- "type": "object",
5555- "properties": {
5656- "displayName": {
5757- "type": "string",
5858- "maxLength": 64,
5959- "maxGraphemes": 64
6060- },
6161- "description": {
6262- "type": "string",
6363- "maxLength": 256,
6464- "maxGraphemes": 256
6565- }
6666- }
6767- }
6868- }
6969- }
7070-}
7171-```
7272-7373----
7474-7575-<p align="center">
7676- <a href="https://github.com/tylersayshi/prototypey/blob/main/.github/CODE_OF_CONDUCT.md" target="_blank"><img alt="๐ค Code of Conduct: Kept" src="https://img.shields.io/badge/%F0%9F%A4%9D_code_of_conduct-kept-21bb42" /></a>
7777- <a href="https://github.com/tylersayshi/prototypey/blob/main/LICENSE.md" target="_blank"><img alt="๐ License: MIT" src="https://img.shields.io/badge/%F0%9F%93%9D_license-MIT-21bb42.svg" /></a>
7878- <img alt="๐ช TypeScript: Strict" src="https://img.shields.io/badge/%F0%9F%92%AA_typescript-strict-21bb42.svg" />
7979-</p>
8080-8181-## Usage
8282-8383-tbd
8484-8585-## Development
8686-8787-See [`.github/CONTRIBUTING.md`](./.github/CONTRIBUTING.md), then
8888-[`.github/DEVELOPMENT.md`](./.github/DEVELOPMENT.md). Thanks! ๐
8989-9090-<!-- You can remove this notice if you don't want it ๐ no worries! -->
9191-9292-> ๐ This package was templated with
9393-> [`create-typescript-app`](https://github.com/JoshuaKGoldberg/create-typescript-app)
9494-> using the [Bingo framework](https://create.bingo).
11+./packages/prototypey/README.md
···11+# prototypey
22+33+## 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+6363+## 0.2.5
6464+6565+### Patch Changes
6666+6767+- 15d5b7c: hide infer as ~infer
6868+6969+## 0.2.4
7070+7171+### Patch Changes
7272+7373+- 0b16bc3: use spaces for readme so npm doesn't format it weird
7474+7575+## 0.2.3
7676+7777+### Patch Changes
7878+7979+- 708fc60: fix loading of version in cli
8080+8181+## 0.2.2
8282+8383+### Patch Changes
8484+8585+- 0bb8603: moved cli into core library - no more @prototypey/cli separate
+228
packages/prototypey/README.md
···11+# prototypey
22+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
1616+1717+## Installation
1818+1919+```bash
2020+npm install prototypey
2121+```
2222+2323+## Usage
2424+2525+Prototypey provides both a TypeScript library for authoring lexicons and a CLI for code generation.
2626+2727+### Authoring Lexicons
2828+2929+**what you'll write:**
3030+3131+```ts
3232+const lex = lx.lexicon("app.bsky.actor.profile", {
3333+ main: lx.record({
3434+ key: "self",
3535+ record: lx.object({
3636+ displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }),
3737+ description: lx.string({ maxLength: 256, maxGraphemes: 256 }),
3838+ }),
3939+ }),
4040+});
4141+```
4242+4343+**generates to:**
4444+4545+```json
4646+{
4747+ "lexicon": 1,
4848+ "id": "app.bsky.actor.profile",
4949+ "defs": {
5050+ "main": {
5151+ "type": "record",
5252+ "key": "self",
5353+ "record": {
5454+ "type": "object",
5555+ "properties": {
5656+ "displayName": {
5757+ "type": "string",
5858+ "maxLength": 64,
5959+ "maxGraphemes": 64
6060+ },
6161+ "description": {
6262+ "type": "string",
6363+ "maxLength": 256,
6464+ "maxGraphemes": 256
6565+ }
6666+ }
6767+ }
6868+ }
6969+ }
7070+}
7171+```
7272+7373+you could also access the json definition with `lex.json()`.
7474+7575+### Runtime Validation
7676+7777+Prototypey provides runtime validation using [@atproto/lexicon](https://www.npmjs.com/package/@atproto/lexicon):
7878+7979+```ts
8080+const lex = lx.lexicon("app.bsky.actor.profile", {
8181+ main: lx.record({
8282+ key: "self",
8383+ record: lx.object({
8484+ displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }),
8585+ description: lx.string({ maxLength: 256, maxGraphemes: 256 }),
8686+ }),
8787+ }),
8888+});
8989+9090+// Validate data against the schema
9191+const result = lex.validate({
9292+ displayName: "Alice",
9393+ description: "Software engineer",
9494+});
9595+9696+if (result.success) {
9797+ console.log("Valid data:", result.value);
9898+} else {
9999+ console.error("Validation error:", result.error);
100100+}
101101+```
102102+103103+**Validating against specific definitions:**
104104+105105+If your lexicon has multiple definitions, you can validate against a specific one:
106106+107107+```ts
108108+const lex = lx.lexicon("app.bsky.feed.post", {
109109+ user: lx.object({
110110+ handle: lx.string({ required: true }),
111111+ displayName: lx.string(),
112112+ }),
113113+ main: lx.record({
114114+ key: "tid",
115115+ record: lx.object({
116116+ text: lx.string({ required: true }),
117117+ author: lx.ref("#user", { required: true }),
118118+ }),
119119+ }),
120120+});
121121+122122+// Validate against the "user" definition
123123+const userResult = lex.validate(
124124+ { handle: "alice.bsky.social", displayName: "Alice" },
125125+ "user",
126126+);
127127+128128+// Validate against "main" (default if not specified)
129129+const postResult = lex.validate({
130130+ text: "Hello world",
131131+ author: { handle: "bob.bsky.social" },
132132+});
133133+```
134134+135135+### CLI Commands
136136+137137+The `prototypey` package includes a CLI with two main commands:
138138+139139+#### `gen-emit` - Emit JSON schemas from TypeScript
140140+141141+```bash
142142+prototypey gen-emit <outdir> <sources...>
143143+```
144144+145145+Extracts JSON schemas from TypeScript lexicon definitions.
146146+147147+**Example:**
148148+149149+```bash
150150+prototypey gen-emit ./lexicons ./src/lexicons/**/*.ts
151151+```
152152+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
181181+182182+1. Author lexicons in TypeScript using the library
183183+2. Emit JSON schemas with `gen-emit` for runtime validation
184184+185185+**Recommended:** Add as a script to your `package.json`:
186186+187187+```json
188188+{
189189+ "scripts": {
190190+ "lexicon:emit": "prototypey gen-emit ./schemas ./src/lexicons/**/*.ts"
191191+ }
192192+}
193193+```
194194+195195+Then run:
196196+197197+```bash
198198+npm run lexicon:emit
199199+```
200200+201201+#### JSON-first workflow
202202+203203+1. Start with JSON lexicon schemas (e.g., from atproto)
204204+2. Generate TypeScript with `gen-from-json` for type-safe access
205205+206206+**Recommended:** Add as a script to your `package.json`:
207207+208208+```json
209209+{
210210+ "scripts": {
211211+ "lexicon:import": "prototypey gen-from-json ./src/lexicons ./lexicons/**/*.json"
212212+ }
213213+}
214214+```
215215+216216+Then run:
217217+218218+```bash
219219+npm run lexicon:import
220220+```
221221+222222+---
223223+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 :)
+102
packages/prototypey/cli/gen-emit.ts
···11+import { glob } from "tinyglobby";
22+import { mkdir, writeFile } from "node:fs/promises";
33+import { join } from "node:path";
44+import { pathToFileURL } from "node:url";
55+66+interface LexiconNamespace {
77+ json: {
88+ lexicon: number;
99+ id: string;
1010+ defs: Record<string, unknown>;
1111+ };
1212+}
1313+1414+export async function genEmit(
1515+ outdir: string,
1616+ sources: string | string[],
1717+): Promise<void> {
1818+ try {
1919+ const sourcePatterns = Array.isArray(sources) ? sources : [sources];
2020+2121+ // Find all source files matching the patterns
2222+ const sourceFiles = await glob(sourcePatterns, {
2323+ absolute: true,
2424+ onlyFiles: true,
2525+ });
2626+2727+ if (sourceFiles.length === 0) {
2828+ console.log("No source files found matching patterns:", sourcePatterns);
2929+ return;
3030+ }
3131+3232+ console.log(`Found ${String(sourceFiles.length)} source file(s)`);
3333+3434+ // Ensure output directory exists
3535+ await mkdir(outdir, { recursive: true });
3636+3737+ // Process each source file
3838+ for (const sourcePath of sourceFiles) {
3939+ await processSourceFile(sourcePath, outdir);
4040+ }
4141+4242+ console.log(`\nEmitted lexicon schemas to ${outdir}`);
4343+ } catch (error) {
4444+ console.error("Error emitting lexicon schemas:", error);
4545+ process.exit(1);
4646+ }
4747+}
4848+4949+async function processSourceFile(
5050+ sourcePath: string,
5151+ outdir: string,
5252+): Promise<void> {
5353+ try {
5454+ // Convert file path to file URL for dynamic import
5555+ const fileUrl = pathToFileURL(sourcePath).href;
5656+5757+ // Dynamically import the module
5858+ const module = await import(fileUrl);
5959+6060+ // Find all exported lexicons
6161+ const lexicons: LexiconNamespace[] = [];
6262+ for (const key of Object.keys(module)) {
6363+ const exported = module[key];
6464+ // Check if it's a lexicon with a json property
6565+ if (
6666+ exported &&
6767+ typeof exported === "object" &&
6868+ "json" in exported &&
6969+ exported.json &&
7070+ typeof exported.json === "object" &&
7171+ "lexicon" in exported.json &&
7272+ "id" in exported.json &&
7373+ "defs" in exported.json
7474+ ) {
7575+ lexicons.push(exported as LexiconNamespace);
7676+ }
7777+ }
7878+7979+ if (lexicons.length === 0) {
8080+ console.warn(` โ ${sourcePath}: No lexicons found`);
8181+ return;
8282+ }
8383+8484+ // Emit JSON for each lexicon
8585+ for (const lexicon of lexicons) {
8686+ const { id } = lexicon.json;
8787+ const outputPath = join(outdir, `${id}.json`);
8888+8989+ // Write the JSON file
9090+ await writeFile(
9191+ outputPath,
9292+ JSON.stringify(lexicon.json, null, "\t"),
9393+ "utf-8",
9494+ );
9595+9696+ console.log(` โ ${id} -> ${id}.json`);
9797+ }
9898+ } catch (error) {
9999+ console.error(` โ Error processing ${sourcePath}:`, error);
100100+ throw error;
101101+ }
102102+}
+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+}