An experimental TypeSpec syntax for Lexicon
56
fork

Configure Feed

Select the types of activity you want to include in your feed.

ok

+107 -163
+83 -56
packages/emitter/src/emitter.ts
··· 350 350 return; 351 351 } 352 352 353 + // Check if this model is actually an array type (via `is` declaration) 354 + // e.g., `model Preferences is SomeUnion[]` 355 + if (this.isArrayType(model)) { 356 + const arrayDef = this.modelToLexiconArray(model); 357 + if (arrayDef) { 358 + const description = getDoc(this.program, model); 359 + if (description && !arrayDef.description) { 360 + arrayDef.description = description; 361 + } 362 + lexicon.defs[defName] = arrayDef; 363 + return; 364 + } 365 + } 366 + 353 367 const modelDef = this.modelToLexiconObject(model); 354 368 355 369 const description = getDoc(this.program, model); ··· 404 418 const unionDef: any = this.typeToLexiconDefinition(union, undefined, true); 405 419 if (!unionDef) return; 406 420 407 - // Check if union contains only models (object types) 408 - let hasModelTypes = false; 409 - for (const variant of union.variants.values()) { 410 - if (variant.type.kind === "Model") { 411 - hasModelTypes = true; 412 - break; 413 - } 414 - } 415 - 416 421 const description = getDoc(this.program, union); 417 - if (hasModelTypes && unionDef.type === "union") { 418 - // Wrap open union of models in array type (for Preferences pattern) 419 - const arrayDef: any = { 420 - type: "array", 421 - items: unionDef, 422 - }; 423 - if (description) { 424 - arrayDef.description = description; 425 - } 426 - lexicon.defs[defName] = arrayDef; 427 - } else { 428 - // Emit union as-is (e.g., string with knownValues) 422 + 423 + // Emit union def if it's: 424 + // 1. A union of model refs (type: "union") 425 + // 2. A string primitive with knownValues (string literals + string type) 426 + if (unionDef.type === "union" || (unionDef.type === "string" && (unionDef as any).knownValues)) { 429 427 if (description && !unionDef.description) { 430 428 unionDef.description = description; 431 429 } ··· 1062 1060 return blobDef; 1063 1061 } 1064 1062 1065 - if (this.isArrayType(type as Model)) { 1066 - const array = this.modelToLexiconArray(type as Model, prop); 1067 - if (array && prop) { 1068 - const propDesc = getDoc(this.program, prop); 1069 - if (propDesc) { 1070 - array.description = propDesc; 1063 + // Check if this is a Closed<T> model instance 1064 + const isClosedModel = 1065 + model.name === "Closed" || 1066 + (model.node && (model.node as any).symbol?.name === "Closed") || 1067 + (isTemplateInstance(model) && 1068 + model.node && 1069 + (model.node as any).symbol?.name === "Closed"); 1070 + 1071 + if (isClosedModel && isTemplateInstance(model)) { 1072 + // Extract the union type parameter 1073 + const templateArgs = model.templateMapper?.args; 1074 + if (templateArgs && templateArgs.length > 0) { 1075 + const unionArg = templateArgs[0]; 1076 + if (isType(unionArg) && unionArg.kind === "Union") { 1077 + // Process the union with closed flag 1078 + const unionDef = this.typeToLexiconDefinition(unionArg, prop); 1079 + if (unionDef && unionDef.type === "union") { 1080 + (unionDef as LexiconUnion).closed = true; 1081 + return unionDef; 1082 + } 1071 1083 } 1072 1084 } 1073 - return array; 1074 1085 } 1075 - // Check if this is a reference to another model 1086 + 1087 + // Check if this is a reference to another model (including named array models) 1088 + // This must come BEFORE the array check to ensure named arrays are referenced, not inlined 1076 1089 const modelRef = this.getModelReference(type as Model); 1077 1090 if (modelRef) { 1078 1091 const refDef: LexiconRef = { ··· 1087 1100 } 1088 1101 return refDef; 1089 1102 } 1103 + 1104 + // Check for anonymous array types (inline arrays like `SomeModel[]` in property) 1105 + if (this.isArrayType(type as Model)) { 1106 + const array = this.modelToLexiconArray(type as Model, prop); 1107 + if (array && prop) { 1108 + const propDesc = getDoc(this.program, prop); 1109 + if (propDesc) { 1110 + array.description = propDesc; 1111 + } 1112 + } 1113 + return array; 1114 + } 1090 1115 const obj = this.modelToLexiconObject(type as Model); 1091 1116 if (prop) { 1092 1117 const propDesc = getDoc(this.program, prop); ··· 1102 1127 // Check if this is a named union that should be referenced 1103 1128 // (but not if we're defining the union itself) 1104 1129 if (!isDefining) { 1105 - // Check if this is a closed model union 1106 - let hasModels = false; 1107 - for (const variant of unionType.variants.values()) { 1108 - if (variant.type.kind === "Model") { 1109 - hasModels = true; 1110 - break; 1111 - } 1112 - } 1113 - 1114 - // Skip references for @closed model unions - they should be inlined 1115 - // All other named unions can be referenced 1116 - if (!(hasModels && isClosed(this.program, unionType))) { 1117 - const unionRef = this.getUnionReference(unionType); 1118 - if (unionRef) { 1119 - const refDef: LexiconRef = { 1120 - type: "ref", 1121 - ref: unionRef, 1122 - }; 1123 - if (prop) { 1124 - const propDesc = getDoc(this.program, prop); 1125 - if (propDesc) { 1126 - refDef.description = propDesc; 1127 - } 1130 + const unionRef = this.getUnionReference(unionType); 1131 + if (unionRef) { 1132 + const refDef: LexiconRef = { 1133 + type: "ref", 1134 + ref: unionRef, 1135 + }; 1136 + if (prop) { 1137 + const propDesc = getDoc(this.program, prop); 1138 + if (propDesc) { 1139 + refDef.description = propDesc; 1128 1140 } 1129 - return refDef; 1130 1141 } 1142 + return refDef; 1131 1143 } 1132 1144 } 1133 1145 ··· 1442 1454 } 1443 1455 1444 1456 private isArrayType(model: Model): boolean { 1445 - return model.name === "Array" && model.namespace?.name === "TypeSpec"; 1457 + // Direct check: is this the TypeSpec.Array model? 1458 + if (model.name === "Array" && model.namespace?.name === "TypeSpec") { 1459 + return true; 1460 + } 1461 + 1462 + // Follow the sourceModel chain (for `is` declarations) 1463 + // e.g., `model Preferences is SomeUnion[]` -> sourceModel is Array 1464 + if (model.sourceModel) { 1465 + return this.isArrayType(model.sourceModel); 1466 + } 1467 + 1468 + return false; 1446 1469 } 1447 1470 1448 1471 private getModelReference(model: Model): string | null { ··· 1531 1554 model: Model, 1532 1555 prop?: ModelProperty, 1533 1556 ): LexiconArray | null { 1557 + // For `is` arrays (e.g., `model Preferences is SomeUnion[]`), 1558 + // the template args are on the sourceModel, not the model itself 1559 + const arrayModel = model.sourceModel || model; 1560 + 1534 1561 // Handle TypeSpec array types 1535 - if (model.templateMapper?.args && model.templateMapper.args.length > 0) { 1536 - const itemType = model.templateMapper.args[0]; 1562 + if (arrayModel.templateMapper?.args && arrayModel.templateMapper.args.length > 0) { 1563 + const itemType = arrayModel.templateMapper.args[0]; 1537 1564 1538 1565 if (isType(itemType)) { 1539 1566 const itemDef = this.typeToLexiconDefinition(itemType);
+17 -17
packages/emitter/test/scenarios/atproto/input/app/bsky/actor/defs.tsp
··· 151 151 createdAt: datetime; 152 152 } 153 153 154 - union Preferences { 155 - AdultContentPref, 156 - ContentLabelPref, 157 - SavedFeedsPref, 158 - SavedFeedsPrefV2, 159 - PersonalDetailsPref, 160 - FeedViewPref, 161 - ThreadViewPref, 162 - InterestsPref, 163 - MutedWordsPref, 164 - HiddenPostsPref, 165 - BskyAppStatePref, 166 - LabelersPref, 167 - PostInteractionSettingsPref, 168 - VerificationPrefs, 169 - unknown, 170 - } 154 + model Preferences is ( 155 + AdultContentPref | 156 + ContentLabelPref | 157 + SavedFeedsPref | 158 + SavedFeedsPrefV2 | 159 + PersonalDetailsPref | 160 + FeedViewPref | 161 + ThreadViewPref | 162 + InterestsPref | 163 + MutedWordsPref | 164 + HiddenPostsPref | 165 + BskyAppStatePref | 166 + LabelersPref | 167 + PostInteractionSettingsPref | 168 + VerificationPrefs | 169 + unknown 170 + )[]; 171 171 172 172 model AdultContentPref { 173 173 @required enabled: boolean = false;
+2 -17
packages/emitter/test/scenarios/atproto/input/com/atproto/repo/applyWrites.tsp
··· 4 4 @doc("Indicates that the 'swapCommit' parameter did not match current commit.") 5 5 model InvalidSwap {} 6 6 7 - @closed 8 - union Write { 9 - Create, 10 - Update, 11 - Delete, 12 - } 13 - 14 - @closed 15 - union Result { 16 - CreateResult, 17 - UpdateResult, 18 - DeleteResult, 19 - } 20 - 21 7 @doc("Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.") 22 8 @procedure 23 9 @errors(InvalidSwap) ··· 30 16 validate?: boolean; 31 17 32 18 @required 33 - writes: Write[]; 19 + writes: Closed<Create | Update | Delete>[]; 34 20 35 21 @doc("If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.") 36 22 swapCommit?: cid; 37 23 }): { 38 24 commit?: com.atproto.repo.defs.CommitMeta; 39 - 40 - results?: Result[]; 25 + results?: Closed<CreateResult | UpdateResult | DeleteResult>[]; 41 26 }; 42 27 43 28 @doc("Operation which creates a new record.")
-20
packages/emitter/test/scenarios/union-ref-bug/input/test/defs.tsp
··· 1 - import "@tlex/emitter"; 2 - 3 - namespace test.defs { 4 - union MyUnion { 5 - string, 6 - 7 - Foo: "test.defs#foo", 8 - Bar: "test.defs#bar", 9 - } 10 - 11 - @token 12 - model Foo {} 13 - 14 - @token 15 - model Bar {} 16 - 17 - model Example { 18 - @required purpose: MyUnion; 19 - } 20 - }
-29
packages/emitter/test/scenarios/union-ref-bug/output/test/defs.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "test.defs", 4 - "defs": { 5 - "myUnion": { 6 - "type": "string", 7 - "knownValues": [ 8 - "test.defs#foo", 9 - "test.defs#bar" 10 - ] 11 - }, 12 - "foo": { 13 - "type": "token" 14 - }, 15 - "bar": { 16 - "type": "token" 17 - }, 18 - "example": { 19 - "type": "object", 20 - "required": ["purpose"], 21 - "properties": { 22 - "purpose": { 23 - "type": "ref", 24 - "ref": "#myUnion" 25 - } 26 - } 27 - } 28 - } 29 - }
+5 -10
packages/example/src/lexicons.ts
··· 69 69 }, 70 70 }, 71 71 }, 72 - notificationType: { 73 - type: 'string', 74 - knownValues: ['like', 'repost', 'follow', 'mention', 'reply'], 75 - description: 'Type of notification', 76 - }, 77 72 }, 78 73 }, 79 74 AppExampleFollow: { ··· 85 80 key: 'tid', 86 81 record: { 87 82 type: 'object', 88 - description: 'A follow relationship', 89 83 required: ['subject', 'createdAt'], 90 84 properties: { 91 85 subject: { ··· 99 93 }, 100 94 }, 101 95 }, 96 + description: 'A follow relationship', 102 97 }, 103 98 }, 104 99 }, ··· 111 106 key: 'tid', 112 107 record: { 113 108 type: 'object', 114 - description: 'A like on a post', 115 109 required: ['subject', 'createdAt'], 116 110 properties: { 117 111 subject: { ··· 126 120 }, 127 121 }, 128 122 }, 123 + description: 'A like on a post', 129 124 }, 130 125 }, 131 126 }, ··· 138 133 key: 'tid', 139 134 record: { 140 135 type: 'object', 141 - description: 'A post in the feed', 142 136 required: ['text', 'createdAt'], 143 137 properties: { 144 138 text: { ··· 172 166 }, 173 167 }, 174 168 }, 169 + description: 'A post in the feed', 175 170 }, 176 171 }, 177 172 }, ··· 184 179 key: 'self', 185 180 record: { 186 181 type: 'object', 187 - description: 'User profile information', 188 182 properties: { 189 183 displayName: { 190 184 type: 'string', ··· 204 198 }, 205 199 }, 206 200 }, 201 + description: 'User profile information', 207 202 }, 208 203 }, 209 204 }, ··· 216 211 key: 'tid', 217 212 record: { 218 213 type: 'object', 219 - description: 'A repost of another post', 220 214 required: ['subject', 'createdAt'], 221 215 properties: { 222 216 subject: { ··· 231 225 }, 232 226 }, 233 227 }, 228 + description: 'A repost of another post', 234 229 }, 235 230 }, 236 231 },
-9
packages/example/src/types/app/example/defs.ts
··· 68 68 export function validateEntity<V>(v: V) { 69 69 return validate<Entity & V>(v, id, hashEntity) 70 70 } 71 - 72 - /** Type of notification */ 73 - export type NotificationType = 74 - | 'like' 75 - | 'repost' 76 - | 'follow' 77 - | 'mention' 78 - | 'reply' 79 - | (string & {})
-1
packages/example/src/types/app/example/follow.ts
··· 10 10 validate = _validate 11 11 const id = 'app.example.follow' 12 12 13 - /** A follow relationship */ 14 13 export interface Record { 15 14 $type: 'app.example.follow' 16 15 /** DID of the account being followed */
-1
packages/example/src/types/app/example/like.ts
··· 11 11 validate = _validate 12 12 const id = 'app.example.like' 13 13 14 - /** A like on a post */ 15 14 export interface Record { 16 15 $type: 'app.example.like' 17 16 subject: AppExampleDefs.PostRef
-1
packages/example/src/types/app/example/post.ts
··· 11 11 validate = _validate 12 12 const id = 'app.example.post' 13 13 14 - /** A post in the feed */ 15 14 export interface Record { 16 15 $type: 'app.example.post' 17 16 /** Post text content */
-1
packages/example/src/types/app/example/profile.ts
··· 10 10 validate = _validate 11 11 const id = 'app.example.profile' 12 12 13 - /** User profile information */ 14 13 export interface Record { 15 14 $type: 'app.example.profile' 16 15 /** Display name */
-1
packages/example/src/types/app/example/repost.ts
··· 11 11 validate = _validate 12 12 const id = 'app.example.repost' 13 13 14 - /** A repost of another post */ 15 14 export interface Record { 16 15 $type: 'app.example.repost' 17 16 subject: AppExampleDefs.PostRef