An experimental TypeSpec syntax for Lexicon

add @external decorator

Changed files
+45
packages
+15
packages/emitter/lib/decorators.tsp
··· 1 1 import "../dist/tsp-index.js"; 2 2 3 + using TypeSpec.Reflection; 4 + 3 5 /** 4 6 * Specifies the maximum number of graphemes (user-perceived characters) allowed. 5 7 * Used alongside maxLength for proper Unicode text handling. ··· 159 161 * ``` 160 162 */ 161 163 extern dec errors(target: unknown, ...errors: unknown[]); 164 + 165 + /** 166 + * Marks a namespace as external, preventing it from emitting JSON output. 167 + * This decorator can only be applied to namespaces. 168 + * Useful for importing definitions from other lexicons without re-emitting them. 169 + * 170 + * @example 171 + * ```typespec 172 + * @external 173 + * namespace com.atproto.repo.defs; 174 + * ``` 175 + */ 176 + extern dec external(target: Namespace);
+22
packages/emitter/src/decorators.ts
··· 24 24 const inlineKey = Symbol("inline"); 25 25 const maxBytesKey = Symbol("maxBytes"); 26 26 const minBytesKey = Symbol("minBytes"); 27 + const externalKey = Symbol("external"); 27 28 28 29 /** 29 30 * @maxBytes decorator for maximum length of bytes type ··· 294 295 export function isReadOnly(program: Program, target: Type): boolean { 295 296 return program.stateSet(readOnlyKey).has(target); 296 297 } 298 + 299 + /** 300 + * @external decorator for marking a namespace as external 301 + * External namespaces are skipped during emission and don't produce JSON files 302 + */ 303 + export function $external(context: DecoratorContext, target: Type) { 304 + if (target.kind !== "Namespace") { 305 + context.program.reportDiagnostic({ 306 + code: "external-not-on-namespace", 307 + severity: "error", 308 + message: "@external decorator can only be applied to namespaces", 309 + target: target, 310 + }); 311 + return; 312 + } 313 + context.program.stateSet(externalKey).add(target); 314 + } 315 + 316 + export function isExternal(program: Program, target: Type): boolean { 317 + return program.stateSet(externalKey).has(target); 318 + }
+6
packages/emitter/src/emitter.ts
··· 67 67 isInline, 68 68 getMaxBytes, 69 69 getMinBytes, 70 + isExternal, 70 71 } from "./decorators.js"; 71 72 72 73 export interface EmitterOptions { ··· 118 119 for (const [_, childNs] of ns.namespaces) { 119 120 this.processNamespace(childNs); 120 121 } 122 + return; 123 + } 124 + 125 + // Skip external namespaces - they don't emit JSON files 126 + if (isExternal(this.program, ns)) { 121 127 return; 122 128 } 123 129
+2
packages/emitter/src/tsp-index.ts
··· 14 14 $inline, 15 15 $maxBytes, 16 16 $minBytes, 17 + $external, 17 18 } from "./decorators.js"; 18 19 19 20 /** @internal */ ··· 34 35 inline: $inline, 35 36 maxBytes: $maxBytes, 36 37 minBytes: $minBytes, 38 + external: $external, 37 39 }, 38 40 };