a tool for shared writing and social publishing
at update/thread-viewer 284 lines 6.5 kB view raw view rendered
1# Lexicon System 2 3## Overview 4 5Lexicons define the schema for AT Protocol records. This project has two namespaces: 6- **`pub.leaflet.*`** - Leaflet-specific lexicons (documents, publications, blocks, etc.) 7- **`site.standard.*`** - Standard site lexicons for interoperability 8 9The lexicons are defined as TypeScript in `lexicons/src/`, built to JSON in `lexicons/pub/leaflet/` and `lexicons/site/standard/`, and TypeScript types are generated in `lexicons/api/`. 10 11## Key Files 12 13- **`lexicons/src/*.ts`** - Source definitions for `pub.leaflet.*` lexicons 14- **`lexicons/site/standard/**/*.json`** - JSON definitions for `site.standard.*` lexicons (manually maintained) 15- **`lexicons/build.ts`** - Builds TypeScript sources to JSON 16- **`lexicons/api/`** - Generated TypeScript types and client 17- **`package.json`** - Contains `lexgen` script 18 19## Running Lexicon Generation 20 21```bash 22npm run lexgen 23``` 24 25This runs: 261. `tsx ./lexicons/build.ts` - Builds `pub.leaflet.*` JSON from TypeScript 272. `lex gen-api` - Generates TypeScript types from all JSON lexicons 283. `tsx ./lexicons/fix-extensions.ts` - Fixes import extensions 29 30## Adding a New pub.leaflet Lexicon 31 32### 1. Create the Source Definition 33 34Create a file in `lexicons/src/` (e.g., `lexicons/src/myLexicon.ts`): 35 36```typescript 37import { LexiconDoc } from "@atproto/lexicon"; 38 39export const PubLeafletMyLexicon: LexiconDoc = { 40 lexicon: 1, 41 id: "pub.leaflet.myLexicon", 42 defs: { 43 main: { 44 type: "record", // or "object" for non-record types 45 key: "tid", 46 record: { 47 type: "object", 48 required: ["field1"], 49 properties: { 50 field1: { type: "string", maxLength: 1000 }, 51 field2: { type: "integer", minimum: 0 }, 52 optionalRef: { type: "ref", ref: "other.lexicon#def" }, 53 }, 54 }, 55 }, 56 // Additional defs for sub-objects 57 subType: { 58 type: "object", 59 properties: { 60 nested: { type: "string" }, 61 }, 62 }, 63 }, 64}; 65``` 66 67### 2. Add to Build 68 69Update `lexicons/build.ts`: 70 71```typescript 72import { PubLeafletMyLexicon } from "./src/myLexicon"; 73 74const lexicons = [ 75 // ... existing lexicons 76 PubLeafletMyLexicon, 77]; 78``` 79 80### 3. Update lexgen Command (if needed) 81 82If your lexicon is at the top level of `pub/leaflet/` (not in a subdirectory), add it to the `lexgen` script in `package.json`: 83 84```json 85"lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/document.json ./lexicons/pub/leaflet/myLexicon.json ./lexicons/pub/leaflet/*/* ..." 86``` 87 88Note: Files in subdirectories (`pub/leaflet/*/*`) are automatically included. 89 90### 4. Regenerate Types 91 92```bash 93npm run lexgen 94``` 95 96### 5. Use the Generated Types 97 98```typescript 99import { PubLeafletMyLexicon } from "lexicons/api"; 100 101// Type for the record 102type MyRecord = PubLeafletMyLexicon.Record; 103 104// Validation 105const result = PubLeafletMyLexicon.validateRecord(data); 106if (result.success) { 107 // result.value is typed 108} 109 110// Type guard 111if (PubLeafletMyLexicon.isRecord(data)) { 112 // data is typed as Record 113} 114``` 115 116## Adding a New site.standard Lexicon 117 118### 1. Create the JSON Definition 119 120Create a file in `lexicons/site/standard/` (e.g., `lexicons/site/standard/myType.json`): 121 122```json 123{ 124 "lexicon": 1, 125 "id": "site.standard.myType", 126 "defs": { 127 "main": { 128 "type": "record", 129 "key": "tid", 130 "record": { 131 "type": "object", 132 "required": ["field1"], 133 "properties": { 134 "field1": { 135 "type": "string", 136 "maxLength": 1000 137 } 138 } 139 } 140 } 141 } 142} 143``` 144 145### 2. Regenerate Types 146 147```bash 148npm run lexgen 149``` 150 151The `site/*/* site/*/*/*` globs in the lexgen command automatically pick up new files. 152 153## Common Lexicon Patterns 154 155### Referencing Other Lexicons 156 157```typescript 158// Reference another lexicon's main def 159{ type: "ref", ref: "pub.leaflet.publication" } 160 161// Reference a specific def within a lexicon 162{ type: "ref", ref: "pub.leaflet.publication#theme" } 163 164// Reference within the same lexicon 165{ type: "ref", ref: "#myDef" } 166``` 167 168### Union Types 169 170```typescript 171{ 172 type: "union", 173 refs: [ 174 "pub.leaflet.pages.linearDocument", 175 "pub.leaflet.pages.canvas", 176 ], 177} 178 179// Open union (allows unknown types) 180{ 181 type: "union", 182 closed: false, // default is true 183 refs: ["pub.leaflet.content"], 184} 185``` 186 187### Blob Types (for images/files) 188 189```typescript 190{ 191 type: "blob", 192 accept: ["image/*"], // or specific types like ["image/png", "image/jpeg"] 193 maxSize: 1000000, // bytes 194} 195``` 196 197### Color Types 198 199The project has color types defined: 200- `pub.leaflet.theme.color#rgb` / `#rgba` 201- `site.standard.theme.color#rgb` / `#rgba` 202 203```typescript 204// In lexicons/src/theme.ts 205export const ColorUnion = { 206 type: "union", 207 refs: [ 208 "pub.leaflet.theme.color#rgba", 209 "pub.leaflet.theme.color#rgb", 210 ], 211}; 212``` 213 214## Normalization Between Formats 215 216Use `lexicons/src/normalize.ts` to convert between `pub.leaflet` and `site.standard` formats: 217 218```typescript 219import { 220 normalizeDocument, 221 normalizePublication, 222 isLeafletDocument, 223 isStandardDocument, 224 getDocumentPages, 225} from "lexicons/src/normalize"; 226 227// Normalize a document from either format 228const normalized = normalizeDocument(record); 229if (normalized) { 230 // normalized is always in site.standard.document format 231 console.log(normalized.title, normalized.site); 232 233 // Get pages if content is pub.leaflet.content 234 const pages = getDocumentPages(normalized); 235} 236 237// Normalize a publication 238const pub = normalizePublication(record); 239if (pub) { 240 console.log(pub.name, pub.url); 241} 242``` 243 244## Handling in Appview (Firehose Consumer) 245 246When processing records from the firehose in `appview/index.ts`: 247 248```typescript 249import { ids } from "lexicons/api/lexicons"; 250import { PubLeafletMyLexicon } from "lexicons/api"; 251 252// In filterCollections: 253filterCollections: [ 254 ids.PubLeafletMyLexicon, 255 // ... 256], 257 258// In handleEvent: 259if (evt.collection === ids.PubLeafletMyLexicon) { 260 if (evt.event === "create" || evt.event === "update") { 261 let record = PubLeafletMyLexicon.validateRecord(evt.record); 262 if (!record.success) return; 263 264 // Store in database 265 await supabase.from("my_table").upsert({ 266 uri: evt.uri.toString(), 267 data: record.value as Json, 268 }); 269 } 270 if (evt.event === "delete") { 271 await supabase.from("my_table").delete().eq("uri", evt.uri.toString()); 272 } 273} 274``` 275 276## Publishing Lexicons 277 278To publish lexicons to an AT Protocol PDS: 279 280```bash 281npm run publish-lexicons 282``` 283 284This runs `lexicons/publish.ts` which publishes lexicons to the configured PDS.