An experimental TypeSpec syntax for Lexicon

Compare changes

Choose any two refs to compare.

Changed files
+8739 -976
packages
cli
src
test
helpers
scenarios
basic
expected
lexicons
com
atproto
label
test
example
typelex
project
lexicons
com
atproto
label
init-preserves-main
expected
lexicons
com
example
typelex
project
missing-dependency
expected
lexicons
com
external
media
myapp
typelex
project
nested-init
expected
lexicons
com
myservice
example
typelex
project
parent-lexicons
expected1
app
lexicons
com
atproto
label
myapp
example
expected2
app
lexicons
com
atproto
label
myapp
example
project
app
lexicons
com
atproto
label
reserved-keywords
expected
lexicons
app
bsky
feed
com
atproto
server
pub
leaflet
example
typelex
project
lexicons
app
bsky
feed
com
atproto
server
validation-errors
with-external-lexicons
expected1
lexicons
com
atproto
label
myapp
example
typelex
expected2
lexicons
com
atproto
label
myapp
typelex
project
lexicons
com
atproto
label
typelex
emitter
lib
src
test
integration
atproto
input
app
bsky
actor
bookmark
embed
feed
graph
labeler
notification
unspecced
video
chat
com
tools
lexicon-examples
spec
example
playground
website
scripts
-1
.gitignore
··· 5 5 dist/ 6 6 tsp-output/ 7 7 generated/ 8 - lexicons/ 9 8 *.tsbuildinfo 10 9 11 10 # Test outputs
+13
CHANGELOG.md
··· 1 + ### 0.3.1 2 + 3 + - Escape reserved keywords when generating code 4 + 5 + ### 0.3.0 6 + 7 + - New package `@typelex/cli` 8 + - See new recommended workflow on https://typelex.org/#install 9 + 10 + ### 0.2.0 11 + 12 + - Add `@external` support 13 + 1 14 ### 0.1.6 2 15 3 16 - Rebuild
+169 -14
DOCS.md
··· 238 238 239 239 ### External Stubs 240 240 241 - If you don't have TypeSpec definitions for external Lexicons, you can stub them out: 241 + If you don't have TypeSpec definitions for external Lexicons, you can stub them out using the `@external` decorator: 242 242 243 243 ```typescript 244 244 import "@typelex/emitter"; ··· 250 250 } 251 251 252 252 // Empty stub (like .d.ts in TypeScript) 253 + @external 253 254 namespace com.atproto.label.defs { 254 255 model SelfLabels { } 255 256 } 256 257 ``` 257 258 258 - You could collect stubs in one file and import them: 259 - 260 - ```typescript 261 - import "@typelex/emitter"; 262 - import "../atproto-stubs.tsp"; 259 + The `@external` decorator tells the emitter to skip JSON output for that namespace. This is useful when referencing definitions from other Lexicons that you don't want to re-emit. 263 260 264 - namespace app.bsky.actor.profile { 265 - model Main { 266 - labels?: (com.atproto.label.defs.SelfLabels | unknown); 267 - } 268 - } 269 - ``` 261 + Starting with 0.3.0, typelex will automatically generate a `typelex/externals.tsp` file based on the JSON files in your `lexicons/` folder, and enforce that it's imported into your `typelex/main.tsp` entry point. However, this will *not* include Lexicons from your app's namespace, but only external ones. 270 262 271 - You'll want to replace the stubbed lexicons in the output folder with their real JSON before running codegen. 263 + You'll want to ensure the real JSON for external Lexicons is available before running codegen. 272 264 273 265 ### Inline Models 274 266 ··· 319 311 ``` 320 312 321 313 Note that `Caption` won't exist as a separate defโ€”the abstraction is erased in the output. 314 + 315 + ### Scalars 316 + 317 + TypeSpec scalars let you create named types with constraints. **By default, scalars create standalone defs** (like models): 318 + 319 + ```typescript 320 + import "@typelex/emitter"; 321 + 322 + namespace com.example { 323 + model Main { 324 + handle?: Handle; 325 + bio?: Bio; 326 + } 327 + 328 + @maxLength(50) 329 + scalar Handle extends string; 330 + 331 + @maxLength(256) 332 + @maxGraphemes(128) 333 + scalar Bio extends string; 334 + } 335 + ``` 336 + 337 + This creates three defs: `main`, `handle`, and `bio`: 338 + 339 + ```json 340 + { 341 + "id": "com.example", 342 + "defs": { 343 + "main": { 344 + "type": "object", 345 + "properties": { 346 + "handle": { "type": "ref", "ref": "#handle" }, 347 + "bio": { "type": "ref", "ref": "#bio" } 348 + } 349 + }, 350 + "handle": { 351 + "type": "string", 352 + "maxLength": 50 353 + }, 354 + "bio": { 355 + "type": "string", 356 + "maxLength": 256, 357 + "maxGraphemes": 128 358 + } 359 + } 360 + } 361 + ``` 362 + 363 + Use `@inline` to expand a scalar inline instead: 364 + 365 + ```typescript 366 + import "@typelex/emitter"; 367 + 368 + namespace com.example { 369 + model Main { 370 + handle?: Handle; 371 + } 372 + 373 + @inline 374 + @maxLength(50) 375 + scalar Handle extends string; 376 + } 377 + ``` 378 + 379 + Now `Handle` is expanded inline (no separate def): 380 + 381 + ```json 382 + // ... 383 + "properties": { 384 + "handle": { "type": "string", "maxLength": 50 } 385 + } 386 + // ... 387 + ``` 322 388 323 389 ## Top-Level Lexicon Types 324 390 ··· 913 979 914 980 ## Defaults and Constants 915 981 916 - ### Defaults 982 + ### Property Defaults 983 + 984 + You can set default values on properties: 917 985 918 986 ```typescript 919 987 import "@typelex/emitter"; ··· 927 995 ``` 928 996 929 997 Maps to: `{"default": 1}`, `{"default": "en"}` 998 + 999 + ### Type Defaults 1000 + 1001 + You can also set defaults on scalar and union types using the `@default` decorator: 1002 + 1003 + ```typescript 1004 + import "@typelex/emitter"; 1005 + 1006 + namespace com.example { 1007 + model Main { 1008 + mode?: Mode; 1009 + priority?: Priority; 1010 + } 1011 + 1012 + @default("standard") 1013 + scalar Mode extends string; 1014 + 1015 + @default(1) 1016 + @closed 1017 + @inline 1018 + union Priority { 1, 2, 3 } 1019 + } 1020 + ``` 1021 + 1022 + This creates a default on the type definition itself: 1023 + 1024 + ```json 1025 + { 1026 + "defs": { 1027 + "mode": { 1028 + "type": "string", 1029 + "default": "standard" 1030 + } 1031 + } 1032 + } 1033 + ``` 1034 + 1035 + For unions with token references, pass the model directly: 1036 + 1037 + ```typescript 1038 + import "@typelex/emitter"; 1039 + 1040 + namespace com.example { 1041 + model Main { 1042 + eventType?: EventType; 1043 + } 1044 + 1045 + @default(InPerson) 1046 + union EventType { Hybrid, InPerson, Virtual, string } 1047 + 1048 + @token model Hybrid {} 1049 + @token model InPerson {} 1050 + @token model Virtual {} 1051 + } 1052 + ``` 1053 + 1054 + This resolves to the fully-qualified token NSID: 1055 + 1056 + ```json 1057 + { 1058 + "eventType": { 1059 + "type": "string", 1060 + "knownValues": [ 1061 + "com.example#hybrid", 1062 + "com.example#inPerson", 1063 + "com.example#virtual" 1064 + ], 1065 + "default": "com.example#inPerson" 1066 + } 1067 + } 1068 + ``` 1069 + 1070 + **Important:** When a scalar or union creates a standalone def (not `@inline`), property-level defaults must match the type's `@default`. Otherwise you'll get an error: 1071 + 1072 + ```typescript 1073 + @default("standard") 1074 + scalar Mode extends string; 1075 + 1076 + model Main { 1077 + mode?: Mode = "custom"; // ERROR: Conflicting defaults! 1078 + } 1079 + ``` 1080 + 1081 + Solutions: 1082 + 1. Make the defaults match: `mode?: Mode = "standard"` 1083 + 2. Mark the type `@inline`: Allows property-level defaults 1084 + 3. Remove the property default: Uses the type's default 930 1085 931 1086 ### Constants 932 1087
+24
LICENSE.md
··· 18 18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE. 22 + 23 + Contains lexicons from https://github.com/lexicon-community/lexicon under the following license: 24 + 25 + MIT License 26 + 27 + Copyright (c) 2024 Lexicon Community 28 + 29 + Permission is hereby granted, free of charge, to any person obtaining a copy 30 + of this software and associated documentation files (the "Software"), to deal 31 + in the Software without restriction, including without limitation the rights 32 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 33 + copies of the Software, and to permit persons to whom the Software is 34 + furnished to do so, subject to the following conditions: 35 + 36 + The above copyright notice and this permission notice shall be included in all 37 + copies or substantial portions of the Software. 38 + 39 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 45 SOFTWARE.
+3 -2
package.json
··· 5 5 "description": "TypeSpec-based IDL for ATProto Lexicons", 6 6 "scripts": { 7 7 "build": "pnpm -r build", 8 - "test": "pnpm --filter @typelex/emitter test", 8 + "test": "pnpm -r test", 9 9 "test:watch": "pnpm --filter @typelex/emitter test:watch", 10 10 "example": "pnpm --filter @typelex/example build", 11 11 "playground": "pnpm --filter @typelex/playground dev", 12 12 "validate": "pnpm build && pnpm run validate-lexicons && pnpm test", 13 - "validate-lexicons": "node scripts/validate-lexicons.js" 13 + "validate-lexicons": "node scripts/validate-lexicons.js", 14 + "cli": "pnpm --filter @typelex/cli" 14 15 }, 15 16 "repository": { 16 17 "type": "git",
+3
packages/cli/.gitignore
··· 1 + dist 2 + node_modules 3 + *.log
+66
packages/cli/README.md
··· 1 + # @typelex/cli 2 + 3 + Experimental CLI for typelex 4 + 5 + ## Installation 6 + 7 + ```bash 8 + pnpm add -D @typelex/cli @typelex/emitter 9 + ``` 10 + 11 + ## Usage 12 + 13 + ```bash 14 + typelex compile xyz.statusphere.* 15 + ``` 16 + 17 + This command: 18 + 1. Scans `lexicons/` for all external lexicons (not matching `xyz.statusphere`) 19 + 2. Generates `typelex/externals.tsp` with `@external` stubs 20 + 3. Compiles `typelex/main.tsp` to `lexicons/` (or custom output via `--out`) 21 + 22 + Fixed paths: 23 + - Entry point: `typelex/main.tsp` 24 + - Externals: `typelex/externals.tsp` 25 + 26 + ## Example 27 + 28 + ```typescript 29 + // typelex/main.tsp 30 + import "@typelex/emitter"; 31 + import "./externals.tsp"; 32 + 33 + namespace xyz.statusphere.defs { 34 + model StatusView { 35 + @required uri: atUri; 36 + @required status: string; 37 + @required profile: app.bsky.actor.defs.ProfileView; 38 + } 39 + } 40 + ``` 41 + 42 + ```bash 43 + typelex compile 'xyz.statusphere.*' 44 + ``` 45 + 46 + The CLI scans `lexicons/` for external types and auto-generates `typelex/externals.tsp` with stubs 47 + 48 + ### Integration 49 + 50 + ```json 51 + { 52 + "scripts": { 53 + "build:lexicons": "typelex compile 'xyz.statusphere.*'", 54 + "build:codegen": "lex gen-server --yes ./src lexicons/xyz/statusphere/*.json" 55 + } 56 + } 57 + ``` 58 + 59 + ## Options 60 + 61 + - `--out <directory>` - Output directory for generated Lexicon files (default: `./lexicons`) 62 + - `--watch` - Watch mode for continuous compilation 63 + 64 + ## License 65 + 66 + MIT
+45
packages/cli/package.json
··· 1 + { 2 + "name": "@typelex/cli", 3 + "version": "0.3.1", 4 + "main": "dist/index.js", 5 + "type": "module", 6 + "bin": { 7 + "typelex": "dist/cli.js" 8 + }, 9 + "files": [ 10 + "dist", 11 + "src" 12 + ], 13 + "scripts": { 14 + "build": "tsc", 15 + "clean": "rm -rf dist", 16 + "watch": "tsc --watch", 17 + "test": "npm run build && vitest run", 18 + "test:watch": "npm run build && vitest watch", 19 + "prepublishOnly": "npm run build" 20 + }, 21 + "keywords": [ 22 + "typespec", 23 + "atproto", 24 + "lexicon", 25 + "cli" 26 + ], 27 + "author": "Dan Abramov <dan.abramov@gmail.com>", 28 + "license": "MIT", 29 + "dependencies": { 30 + "@typespec/compiler": "^1.4.0", 31 + "globby": "^14.0.0", 32 + "picocolors": "^1.1.1", 33 + "yargs": "^18.0.0" 34 + }, 35 + "devDependencies": { 36 + "@types/node": "^20.0.0", 37 + "@types/yargs": "^17.0.33", 38 + "typescript": "^5.0.0", 39 + "vitest": "^1.0.0", 40 + "@typelex/emitter": "workspace:*" 41 + }, 42 + "peerDependencies": { 43 + "@typelex/emitter": "^0.3.1" 44 + } 45 + }
+113
packages/cli/src/cli.ts
··· 1 + #!/usr/bin/env node 2 + import yargs from "yargs"; 3 + import { hideBin } from "yargs/helpers"; 4 + import { compileCommand } from "./commands/compile.js"; 5 + import { initCommand } from "./commands/init.js"; 6 + 7 + async function main() { 8 + await yargs(hideBin(process.argv)) 9 + .scriptName("typelex") 10 + .command( 11 + "init", 12 + "Initialize a new typelex project", 13 + (yargs) => { 14 + return yargs.option("setup", { 15 + describe: "Internal: run setup after installation", 16 + type: "boolean", 17 + hidden: true, 18 + default: false, 19 + }); 20 + }, 21 + async (argv) => { 22 + // Extract any unknown flags to pass through to package manager 23 + const flags: string[] = []; 24 + const knownFlags = new Set(["setup", "_", "$0"]); 25 + 26 + for (const [key, value] of Object.entries(argv)) { 27 + if (!knownFlags.has(key)) { 28 + // Single letter = short flag, multiple letters = long flag 29 + const prefix = key.length === 1 ? "-" : "--"; 30 + if (typeof value === "boolean" && value) { 31 + flags.push(`${prefix}${key}`); 32 + } else if (value !== false && value !== undefined) { 33 + flags.push(`${prefix}${key}`, String(value)); 34 + } 35 + } 36 + } 37 + 38 + await initCommand(argv.setup, flags); 39 + } 40 + ) 41 + .command( 42 + "compile <namespace>", 43 + "Compile TypeSpec files to Lexicon JSON", 44 + (yargs) => { 45 + return yargs 46 + .positional("namespace", { 47 + describe: "Primary namespace pattern (e.g., com.example.*)", 48 + type: "string", 49 + demandOption: true, 50 + }) 51 + .option("out", { 52 + describe: "Output directory for generated Lexicon files (must end with 'lexicons')", 53 + type: "string", 54 + default: "./lexicons", 55 + }); 56 + }, 57 + async (argv) => { 58 + if (!argv.namespace) { 59 + console.error("Error: namespace is required"); 60 + console.error("Usage: typelex compile <namespace>"); 61 + console.error("Example: typelex compile com.example.*"); 62 + process.exit(1); 63 + } 64 + 65 + if (!argv.namespace.endsWith(".*")) { 66 + console.error("Error: namespace must end with .*"); 67 + console.error(`Got: ${argv.namespace}`); 68 + console.error("Example: typelex compile com.example.*"); 69 + process.exit(1); 70 + } 71 + 72 + const options: Record<string, unknown> = {}; 73 + if (argv.watch) { 74 + options.watch = true; 75 + } 76 + if (argv.out) { 77 + options.out = argv.out; 78 + } 79 + await compileCommand(argv.namespace, options); 80 + } 81 + ) 82 + .option("watch", { 83 + describe: "Watch mode", 84 + type: "boolean", 85 + default: false, 86 + }) 87 + .demandCommand(1) 88 + .help() 89 + .version() 90 + .fail((msg, err) => { 91 + if (err) { 92 + console.error(err); 93 + } else if (msg.includes("Not enough non-option arguments")) { 94 + console.error("Error: namespace is required"); 95 + console.error("Usage: typelex compile <namespace>"); 96 + console.error("Example: typelex compile com.example.*"); 97 + } else { 98 + console.error(msg); 99 + } 100 + process.exit(1); 101 + }).argv; 102 + } 103 + 104 + process.on("unhandledRejection", (error: unknown) => { 105 + console.error("Unhandled promise rejection!"); 106 + console.error(error); 107 + process.exit(1); 108 + }); 109 + 110 + main().catch((error) => { 111 + console.error(error); 112 + process.exit(1); 113 + });
+72
packages/cli/src/commands/compile.ts
··· 1 + import { resolve } from "path"; 2 + import { spawn } from "child_process"; 3 + import { generateExternalsFile } from "../utils/externals-generator.js"; 4 + import { ensureMainImports } from "../utils/ensure-imports.js"; 5 + 6 + /** 7 + * Compile TypeSpec files to Lexicon JSON 8 + * 9 + * @param namespace - Primary namespace pattern (e.g., "app.bsky.*") 10 + * @param options - Additional compiler options 11 + */ 12 + export async function compileCommand( 13 + namespace: string, 14 + options: Record<string, unknown> = {} 15 + ): Promise<void> { 16 + const cwd = process.cwd(); 17 + const outDir = (options.out as string) || "./lexicons"; 18 + 19 + // Validate that output directory ends with 'lexicons' 20 + const normalizedPath = outDir.replace(/\\/g, '/').replace(/\/+$/, ''); 21 + if (!normalizedPath.endsWith('/lexicons') && normalizedPath !== 'lexicons' && normalizedPath !== './lexicons') { 22 + console.error(`Error: Output directory must end with 'lexicons'`); 23 + console.error(`Got: ${outDir}`); 24 + console.error(`Valid examples: ./lexicons, ../../lexicons, /path/to/lexicons`); 25 + process.exit(1); 26 + } 27 + 28 + // Generate externals first (scans the output directory for external lexicons) 29 + await generateExternalsFile(namespace, cwd, outDir); 30 + 31 + // Ensure required imports are present in main.tsp 32 + await ensureMainImports(cwd); 33 + 34 + // Compile TypeSpec using the TypeSpec CLI 35 + const entrypoint = resolve(cwd, "typelex/main.tsp"); 36 + 37 + // Normalize path for TypeSpec (remove leading ./) 38 + const normalizedOutDir = outDir.replace(/^\.\//, ''); 39 + 40 + const args = [ 41 + "compile", 42 + entrypoint, 43 + "--emit", 44 + "@typelex/emitter", 45 + "--option", 46 + `@typelex/emitter.emitter-output-dir={project-root}/${normalizedOutDir}`, 47 + ]; 48 + 49 + if (options.watch) { 50 + args.push("--watch"); 51 + } 52 + 53 + return new Promise((resolve, reject) => { 54 + const tsp = spawn("tsp", args, { 55 + cwd, 56 + stdio: "inherit", 57 + }); 58 + 59 + tsp.on("close", (code) => { 60 + if (code === 0) { 61 + resolve(); 62 + } else { 63 + process.exit(code ?? 1); 64 + } 65 + }); 66 + 67 + tsp.on("error", (err) => { 68 + console.error("Failed to start TypeSpec compiler:", err); 69 + reject(err); 70 + }); 71 + }); 72 + }
+335
packages/cli/src/commands/init.ts
··· 1 + import { resolve, relative } from "path"; 2 + import { mkdir, writeFile, readFile, access, stat } from "fs/promises"; 3 + import { spawn } from "child_process"; 4 + import { createInterface } from "readline"; 5 + import pc from "picocolors"; 6 + import { generateExternalsFile } from "../utils/externals-generator.js"; 7 + import { escapeTypeSpecKeywords } from "../utils/escape-keywords.js"; 8 + 9 + function gradientText(text: string): string { 10 + const colors = [ 11 + "\x1b[38;5;33m", 12 + "\x1b[38;5;69m", 13 + "\x1b[38;5;99m", 14 + "\x1b[38;5;133m", 15 + "\x1b[38;5;170m", 16 + "\x1b[38;5;170m", 17 + "\x1b[38;5;133m", 18 + ]; 19 + const reset = "\x1b[0m"; 20 + 21 + return ( 22 + text 23 + .split("") 24 + .map((char, i) => { 25 + const colorIndex = Math.floor((i / text.length) * colors.length); 26 + return colors[colorIndex] + char; 27 + }) 28 + .join("") + reset 29 + ); 30 + } 31 + 32 + function createMainTemplate(namespace: string): string { 33 + const escapedNamespace = escapeTypeSpecKeywords(namespace); 34 + return `import "@typelex/emitter"; 35 + import "./externals.tsp"; 36 + 37 + namespace ${escapedNamespace}.example.profile { 38 + /** My profile. */ 39 + @rec("literal:self") 40 + model Main { 41 + /** Free-form profile description.*/ 42 + @maxGraphemes(256) 43 + description?: string; 44 + } 45 + } 46 + `; 47 + } 48 + 49 + const EXTERNALS_TSP_TEMPLATE = `import "@typelex/emitter"; 50 + 51 + // Generated by typelex 52 + // This file is auto-generated. Do not edit manually. 53 + `; 54 + 55 + async function promptNamespace(): Promise<string> { 56 + const rl = createInterface({ 57 + input: process.stdin, 58 + output: process.stdout, 59 + }); 60 + 61 + return new Promise((resolve) => { 62 + rl.question( 63 + `Which Lexicons do you want to write in typelex (e.g. ${pc.cyan("com.example.*")})? `, 64 + (answer) => { 65 + rl.close(); 66 + resolve(answer.trim()); 67 + }, 68 + ); 69 + }); 70 + } 71 + 72 + export async function initCommand( 73 + isSetup: boolean = false, 74 + flags: string[] = [], 75 + ): Promise<void> { 76 + const originalCwd = process.cwd(); 77 + 78 + // Find nearest package.json upward 79 + let projectRoot = originalCwd; 80 + let dir = originalCwd; 81 + while (dir !== resolve(dir, "..")) { 82 + try { 83 + await access(resolve(dir, "package.json")); 84 + projectRoot = dir; 85 + break; 86 + } catch { 87 + dir = resolve(dir, ".."); 88 + } 89 + } 90 + 91 + if (isSetup) { 92 + return initSetup(); 93 + } 94 + 95 + console.log(gradientText("Adding typelex...") + "\n"); 96 + 97 + // Detect package manager 98 + let packageManager = "npm"; 99 + dir = projectRoot; 100 + while (dir !== resolve(dir, "..") && packageManager === "npm") { 101 + try { 102 + await access(resolve(dir, "pnpm-lock.yaml")); 103 + packageManager = "pnpm"; 104 + break; 105 + } catch { 106 + // Not found 107 + } 108 + try { 109 + await access(resolve(dir, "yarn.lock")); 110 + packageManager = "yarn"; 111 + break; 112 + } catch { 113 + // Not found 114 + } 115 + dir = resolve(dir, ".."); 116 + } 117 + 118 + // Install dependencies 119 + await new Promise<void>((resolvePromise, reject) => { 120 + const args = 121 + packageManager === "npm" 122 + ? [ 123 + "install", 124 + "--save-dev", 125 + "@typelex/cli@latest", 126 + "@typelex/emitter@latest", 127 + ] 128 + : ["add", "-D", "@typelex/cli@latest", "@typelex/emitter@latest"]; 129 + 130 + // Add any additional flags 131 + args.push(...flags); 132 + 133 + const install = spawn(packageManager, args, { 134 + cwd: projectRoot, 135 + stdio: "inherit", 136 + }); 137 + 138 + install.on("close", (code) => { 139 + if (code === 0) { 140 + console.log( 141 + `\n${pc.green("โœ“")} Installed ${pc.dim("@typelex/cli")} and ${pc.dim("@typelex/emitter")}\n`, 142 + ); 143 + resolvePromise(); 144 + } else { 145 + console.error(pc.red("โœ— Failed to install dependencies")); 146 + process.exit(code ?? 1); 147 + } 148 + }); 149 + 150 + install.on("error", (err) => { 151 + console.error(pc.red("โœ— Failed to install dependencies:"), err); 152 + reject(err); 153 + }); 154 + }); 155 + 156 + // Find node_modules 157 + let nodeModulesDir = resolve(projectRoot, "node_modules"); 158 + let searchDir = projectRoot; 159 + while (searchDir !== resolve(searchDir, "..")) { 160 + try { 161 + const candidatePath = resolve(searchDir, "node_modules/.bin/typelex"); 162 + await access(candidatePath); 163 + nodeModulesDir = resolve(searchDir, "node_modules"); 164 + break; 165 + } catch { 166 + searchDir = resolve(searchDir, ".."); 167 + } 168 + } 169 + 170 + return new Promise((resolvePromise, reject) => { 171 + const localCli = resolve(nodeModulesDir, ".bin/typelex"); 172 + const setup = spawn(localCli, ["init", "--setup"], { 173 + cwd: projectRoot, 174 + stdio: "inherit", 175 + }); 176 + 177 + setup.on("close", (code) => { 178 + if (code === 0) { 179 + resolvePromise(); 180 + } else { 181 + process.exit(code ?? 1); 182 + } 183 + }); 184 + 185 + setup.on("error", (err) => { 186 + console.error(pc.red("โœ— Failed to run setup:"), err); 187 + reject(err); 188 + }); 189 + }); 190 + } 191 + 192 + export async function initSetup(): Promise<void> { 193 + const cwd = process.cwd(); 194 + const typelexDir = resolve(cwd, "typelex"); 195 + const mainTspPath = resolve(typelexDir, "main.tsp"); 196 + const externalsTspPath = resolve(typelexDir, "externals.tsp"); 197 + 198 + // Prompt for namespace 199 + let namespace = await promptNamespace(); 200 + 201 + // Validate namespace format 202 + while (!namespace.endsWith(".*")) { 203 + console.error(pc.red(`Error: namespace must end with ${pc.bold(".*")}`)); 204 + console.error(pc.red(`Got: ${pc.bold(namespace)}\n`)); 205 + namespace = await promptNamespace(); 206 + } 207 + 208 + // Remove the .* suffix for use in template 209 + const namespacePrefix = namespace.slice(0, -2); 210 + 211 + // Detect lexicons directory: check cwd first, then walk up parents 212 + let lexiconsDir: string | null = null; 213 + let hasLocalLexicons = false; 214 + 215 + // Check current directory for lexicons/ (will use default, no --out flag needed) 216 + try { 217 + const localPath = resolve(cwd, "lexicons"); 218 + if ((await stat(localPath)).isDirectory()) { 219 + hasLocalLexicons = true; 220 + } 221 + } catch { 222 + // Not found in current directory, check parent directories 223 + let dir = resolve(cwd, ".."); 224 + while (dir !== resolve(dir, "..")) { 225 + try { 226 + const lexPath = resolve(dir, "lexicons"); 227 + if ((await stat(lexPath)).isDirectory()) { 228 + lexiconsDir = relative(cwd, lexPath); 229 + break; 230 + } 231 + } catch { 232 + // Not found, continue up 233 + } 234 + dir = resolve(dir, ".."); 235 + } 236 + } 237 + 238 + // Determine the actual lexicons path for display 239 + const displayLexiconsPath = hasLocalLexicons 240 + ? "./lexicons" 241 + : lexiconsDir || "./lexicons"; 242 + 243 + // Inform about external lexicons 244 + console.log( 245 + `\nLexicons for ${pc.cyan(namespace)} will now be managed by typelex.`, 246 + ); 247 + console.log(`You can begin writing them in ${pc.cyan("typelex/main.tsp")}.`); 248 + console.log( 249 + `Any external lexicons should remain in ${pc.cyan(displayLexiconsPath)}.\n`, 250 + ); 251 + 252 + // Create typelex directory 253 + await mkdir(typelexDir, { recursive: true }); 254 + 255 + // Check if main.tsp exists and is non-empty 256 + let shouldCreateMain = true; 257 + try { 258 + await access(mainTspPath); 259 + const content = await readFile(mainTspPath, "utf-8"); 260 + if (content.trim().length > 0) { 261 + console.log( 262 + `${pc.green("โœ“")} ${pc.cyan("typelex/main.tsp")} already exists, skipping`, 263 + ); 264 + shouldCreateMain = false; 265 + } 266 + } catch { 267 + // File doesn't exist, we'll create it 268 + } 269 + 270 + if (shouldCreateMain) { 271 + await writeFile(mainTspPath, createMainTemplate(namespacePrefix), "utf-8"); 272 + console.log(`${pc.green("โœ“")} Created ${pc.cyan("typelex/main.tsp")}`); 273 + } 274 + 275 + // Generate externals.tsp with any existing external lexicons 276 + const outDir = lexiconsDir || "./lexicons"; 277 + await generateExternalsFile(namespace, cwd, outDir); 278 + console.log(`${pc.green("โœ“")} Created ${pc.cyan("typelex/externals.tsp")}`); 279 + 280 + // Add build script to package.json 281 + const packageJsonPath = resolve(cwd, "package.json"); 282 + try { 283 + const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8")); 284 + if (!packageJson.scripts) { 285 + packageJson.scripts = {}; 286 + } 287 + if (!packageJson.scripts["build:typelex"]) { 288 + const outFlag = lexiconsDir ? ` --out ${lexiconsDir}` : ""; 289 + packageJson.scripts["build:typelex"] = 290 + `typelex compile ${namespace}${outFlag}`; 291 + await writeFile( 292 + packageJsonPath, 293 + JSON.stringify(packageJson, null, 2) + "\n", 294 + "utf-8", 295 + ); 296 + console.log( 297 + `${pc.green("โœ“")} Added ${pc.cyan("build:typelex")} script to ${pc.cyan("package.json")}`, 298 + ); 299 + if (hasLocalLexicons) { 300 + console.log( 301 + pc.dim( 302 + ` Using existing lexicons directory: ${pc.cyan("./lexicons")}`, 303 + ), 304 + ); 305 + } else if (lexiconsDir) { 306 + console.log( 307 + pc.dim( 308 + ` Using existing lexicons directory: ${pc.cyan(lexiconsDir)}`, 309 + ), 310 + ); 311 + } 312 + } else { 313 + console.log( 314 + `${pc.green("โœ“")} ${pc.cyan("build:typelex")} script already exists in ${pc.cyan("package.json")}`, 315 + ); 316 + } 317 + } catch (err) { 318 + console.warn( 319 + pc.yellow(`โš  Could not update ${pc.cyan("package.json")}:`), 320 + (err as Error).message, 321 + ); 322 + } 323 + 324 + console.log(`\n${pc.green("โœ“")} ${pc.bold("All set!")}`); 325 + console.log(`\n${pc.bold("Next steps:")}`); 326 + console.log( 327 + ` ${pc.dim("1.")} Edit ${pc.cyan("typelex/main.tsp")} to define the ${pc.cyan(namespace)} lexicons`, 328 + ); 329 + console.log( 330 + ` ${pc.dim("2.")} Keep putting external lexicons into ${pc.cyan(displayLexiconsPath)}`, 331 + ); 332 + console.log( 333 + ` ${pc.dim("3.")} Run ${pc.cyan("npm run build:typelex")} to compile to JSON`, 334 + ); 335 + }
+1
packages/cli/src/index.ts
··· 1 + export { compileCommand } from "./commands/compile.js";
+38
packages/cli/src/utils/ensure-imports.ts
··· 1 + import { readFile } from "fs/promises"; 2 + import { resolve } from "path"; 3 + 4 + const REQUIRED_FIRST_LINE = 'import "@typelex/emitter";'; 5 + const REQUIRED_SECOND_LINE = 'import "./externals.tsp";'; 6 + 7 + /** 8 + * Validates that main.tsp starts with the required imports. 9 + * Fails the build if the first two lines are not exactly as expected. 10 + * 11 + * @param cwd - Current working directory 12 + */ 13 + export async function ensureMainImports(cwd: string): Promise<void> { 14 + const mainPath = resolve(cwd, "typelex/main.tsp"); 15 + 16 + try { 17 + const content = await readFile(mainPath, "utf-8"); 18 + const lines = content.split("\n"); 19 + 20 + if (lines[0]?.trim() !== REQUIRED_FIRST_LINE) { 21 + console.error(`Error: main.tsp must start with: ${REQUIRED_FIRST_LINE}`); 22 + console.error(`Found: ${lines[0] || "(empty line)"}`); 23 + process.exit(1); 24 + } 25 + 26 + if (lines[1]?.trim() !== REQUIRED_SECOND_LINE) { 27 + console.error(`Error: Line 2 of main.tsp must be: ${REQUIRED_SECOND_LINE}`); 28 + console.error(`Found: ${lines[1] || "(empty line)"}`); 29 + process.exit(1); 30 + } 31 + } catch (err) { 32 + if ((err as NodeJS.ErrnoException).code === "ENOENT") { 33 + console.error("Error: typelex/main.tsp not found"); 34 + process.exit(1); 35 + } 36 + throw err; 37 + } 38 + }
+28
packages/cli/src/utils/escape-keywords.ts
··· 1 + /** 2 + * Complete list of TypeSpec reserved keywords (67 total) 3 + * Source: @typespec/compiler/src/core/scanner.ts 4 + */ 5 + const TYPESPEC_KEYWORDS = new Set([ 6 + // Active keywords 7 + "import", "model", "scalar", "namespace", "using", "op", "enum", "alias", 8 + "is", "interface", "union", "projection", "else", "if", "dec", "fn", 9 + "const", "init", "extern", "extends", "true", "false", "return", "void", 10 + "never", "unknown", "valueof", "typeof", 11 + // Reserved keywords 12 + "statemachine", "macro", "package", "metadata", "env", "arg", "declare", 13 + "array", "struct", "record", "module", "mod", "sym", "context", "prop", 14 + "property", "scenario", "pub", "sub", "typeref", "trait", "this", "self", 15 + "super", "keyof", "with", "implements", "impl", "satisfies", "flag", "auto", 16 + "partial", "private", "public", "protected", "internal", "sealed", "local", 17 + "async" 18 + ]); 19 + 20 + /** 21 + * Escape TypeSpec reserved keywords in a namespace identifier 22 + * Example: "pub.leaflet.example" -> "`pub`.leaflet.example" 23 + */ 24 + export function escapeTypeSpecKeywords(nsid: string): string { 25 + return nsid.split('.').map(part => 26 + TYPESPEC_KEYWORDS.has(part) ? `\`${part}\`` : part 27 + ).join('.'); 28 + }
+106
packages/cli/src/utils/externals-generator.ts
··· 1 + import { resolve } from "path"; 2 + import { writeFile, mkdir } from "fs/promises"; 3 + import { findExternalLexicons, LexiconDoc, isTokenDef, isModelDef } from "./lexicon.js"; 4 + import { escapeTypeSpecKeywords } from "./escape-keywords.js"; 5 + 6 + /** 7 + * Convert camelCase to PascalCase 8 + */ 9 + function toPascalCase(str: string): string { 10 + return str.charAt(0).toUpperCase() + str.slice(1); 11 + } 12 + 13 + /** 14 + * Extract namespace prefix from pattern (e.g., "app.bsky.*" -> "app.bsky") 15 + */ 16 + function getNamespacePrefix(pattern: string): string { 17 + if (!pattern.endsWith(".*")) { 18 + throw new Error(`Namespace pattern must end with .*: ${pattern}`); 19 + } 20 + return pattern.slice(0, -2); 21 + } 22 + 23 + /** 24 + * Generate TypeSpec external definitions from lexicon documents 25 + */ 26 + function generateExternalsCode(lexicons: Map<string, LexiconDoc>, outDir: string, excludedPrefix: string): string { 27 + const lines: string[] = []; 28 + 29 + lines.push('import "@typelex/emitter";'); 30 + lines.push(""); 31 + lines.push(`// Generated by typelex from ${outDir} (excluding ${excludedPrefix}.*)`); 32 + lines.push("// This file is auto-generated. Do not edit manually."); 33 + lines.push(""); 34 + 35 + // Sort namespaces for consistent output 36 + const sortedNamespaces = Array.from(lexicons.entries()).sort(([a], [b]) => 37 + a.localeCompare(b) 38 + ); 39 + 40 + for (const [nsid, lexicon] of sortedNamespaces) { 41 + lines.push("@external"); 42 + // Escape reserved keywords in namespace 43 + const escapedNsid = escapeTypeSpecKeywords(nsid); 44 + lines.push(`namespace ${escapedNsid} {`); 45 + 46 + // Sort definitions for consistent output 47 + const sortedDefs = Object.entries(lexicon.defs).sort(([a], [b]) => 48 + a.localeCompare(b) 49 + ); 50 + 51 + for (const [defName, def] of sortedDefs) { 52 + if (!isModelDef(def)) { 53 + continue; 54 + } 55 + 56 + const modelName = toPascalCase(defName); 57 + const isToken = isTokenDef(def); 58 + 59 + if (isToken) { 60 + lines.push(` @token model ${modelName} { }`); 61 + } else { 62 + lines.push(` model ${modelName} { }`); 63 + } 64 + } 65 + 66 + lines.push("}"); 67 + lines.push(""); 68 + } 69 + 70 + return lines.join("\n"); 71 + } 72 + 73 + /** 74 + * Generate externals.tsp file for the given namespace pattern 75 + */ 76 + export async function generateExternalsFile( 77 + namespacePattern: string, 78 + cwd: string, 79 + outDir: string = "./lexicons" 80 + ): Promise<void> { 81 + try { 82 + const prefix = getNamespacePrefix(namespacePattern); 83 + const lexiconsDir = resolve(cwd, outDir); 84 + const outputFile = resolve(cwd, "typelex/externals.tsp"); 85 + 86 + const externals = await findExternalLexicons(lexiconsDir, prefix); 87 + 88 + if (externals.size === 0) { 89 + // No externals, create empty file 90 + await mkdir(resolve(cwd, "typelex"), { recursive: true }); 91 + await writeFile( 92 + outputFile, 93 + `import "@typelex/emitter";\n\n// Generated by typelex from ${outDir} (excluding ${prefix}.*)\n// No external lexicons found\n`, 94 + "utf-8" 95 + ); 96 + return; 97 + } 98 + 99 + const code = generateExternalsCode(externals, outDir, prefix); 100 + await mkdir(resolve(cwd, "typelex"), { recursive: true }); 101 + await writeFile(outputFile, code, "utf-8"); 102 + } catch (error) { 103 + // Re-throw with better context 104 + throw new Error(`Failed to generate externals: ${error instanceof Error ? error.message : String(error)}`); 105 + } 106 + }
+78
packages/cli/src/utils/lexicon.ts
··· 1 + import { readFile } from "fs/promises"; 2 + import { resolve } from "path"; 3 + import { globby } from "globby"; 4 + 5 + export interface LexiconDef { 6 + type: string; 7 + [key: string]: unknown; 8 + } 9 + 10 + export interface LexiconDoc { 11 + lexicon: number; 12 + id: string; 13 + defs: Record<string, LexiconDef>; 14 + } 15 + 16 + /** 17 + * Read and parse a lexicon JSON file 18 + */ 19 + export async function readLexicon(path: string): Promise<LexiconDoc> { 20 + const content = await readFile(path, "utf-8"); 21 + return JSON.parse(content); 22 + } 23 + 24 + /** 25 + * Find all lexicon files in a directory 26 + */ 27 + export async function findLexicons(dir: string): Promise<string[]> { 28 + try { 29 + const pattern = resolve(dir, "**/*.json"); 30 + return await globby(pattern); 31 + } catch { 32 + // If directory doesn't exist, return empty array 33 + return []; 34 + } 35 + } 36 + 37 + /** 38 + * Extract external lexicons that don't match the given namespace 39 + */ 40 + export async function findExternalLexicons( 41 + lexiconsDir: string, 42 + primaryNamespace: string 43 + ): Promise<Map<string, LexiconDoc>> { 44 + const files = await findLexicons(lexiconsDir); 45 + const externals = new Map<string, LexiconDoc>(); 46 + 47 + for (const file of files) { 48 + const lexicon = await readLexicon(file); 49 + if (!lexicon.id.startsWith(primaryNamespace)) { 50 + externals.set(lexicon.id, lexicon); 51 + } 52 + } 53 + 54 + return externals; 55 + } 56 + 57 + /** 58 + * Check if a definition is a token type 59 + */ 60 + export function isTokenDef(def: LexiconDef): boolean { 61 + return def.type === "token"; 62 + } 63 + 64 + /** 65 + * Check if a definition should become a model in TypeSpec 66 + */ 67 + export function isModelDef(def: LexiconDef): boolean { 68 + const type = def.type; 69 + return ( 70 + type === "object" || 71 + type === "token" || 72 + type === "record" || 73 + type === "union" || 74 + type === "string" || 75 + type === "bytes" || 76 + type === "cid-link" 77 + ); 78 + }
+291
packages/cli/test/helpers/test-project.ts
··· 1 + import { mkdtemp, rm, mkdir, writeFile, readFile, readdir, stat } from "fs/promises"; 2 + import { join, resolve, dirname } from "path"; 3 + import { tmpdir } from "os"; 4 + import { spawn } from "child_process"; 5 + import { fileURLToPath } from "url"; 6 + 7 + const __filename = fileURLToPath(import.meta.url); 8 + const __dirname = dirname(__filename); 9 + 10 + export interface TestProjectOptions { 11 + packageManager?: "npm" | "pnpm"; 12 + } 13 + 14 + export class TestProject { 15 + public readonly path: string; 16 + public scenarioPath?: string; 17 + private cleanupHandlers: Array<() => Promise<void>> = []; 18 + 19 + constructor(path: string) { 20 + this.path = path; 21 + } 22 + 23 + static async create(options: TestProjectOptions = {}): Promise<TestProject> { 24 + const tmpDir = await mkdtemp(join(tmpdir(), "typelex-test-")); 25 + const project = new TestProject(tmpDir); 26 + 27 + // Create lock file based on package manager (scenarios provide their own package.json and lexicons) 28 + if (options.packageManager === "pnpm") { 29 + await writeFile(join(tmpDir, "pnpm-lock.yaml"), "lockfileVersion: '6.0'\n"); 30 + } else if (options.packageManager === "npm") { 31 + // npm is default, no lock file needed for detection 32 + } 33 + 34 + return project; 35 + } 36 + 37 + async cleanup(): Promise<void> { 38 + for (const handler of this.cleanupHandlers) { 39 + await handler(); 40 + } 41 + await rm(this.path, { recursive: true, force: true }); 42 + } 43 + 44 + async writeFile(relativePath: string, content: string): Promise<void> { 45 + const fullPath = join(this.path, relativePath); 46 + await mkdir(join(fullPath, ".."), { recursive: true }); 47 + await writeFile(fullPath, content); 48 + } 49 + 50 + async readFile(relativePath: string): Promise<string> { 51 + return readFile(join(this.path, relativePath), "utf-8"); 52 + } 53 + 54 + async fileExists(relativePath: string): Promise<boolean> { 55 + try { 56 + await stat(join(this.path, relativePath)); 57 + return true; 58 + } catch { 59 + return false; 60 + } 61 + } 62 + 63 + async readJson(relativePath: string): Promise<unknown> { 64 + const content = await this.readFile(relativePath); 65 + return JSON.parse(content); 66 + } 67 + 68 + async getDirectoryContents(relativePath: string = ""): Promise<string[]> { 69 + const fullPath = join(this.path, relativePath); 70 + try { 71 + return await readdir(fullPath); 72 + } catch { 73 + return []; 74 + } 75 + } 76 + 77 + async runCommand( 78 + command: string, 79 + args: string[], 80 + options: { input?: string; env?: Record<string, string>; cwd?: string } = {} 81 + ): Promise<{ stdout: string; stderr: string; exitCode: number; output: string }> { 82 + return new Promise((promiseResolve, promiseReject) => { 83 + // Add monorepo node_modules/.bin to PATH for tsp and other tools 84 + const monorepoRoot = resolve(__dirname, "../../../.."); 85 + const tspBinPath = join(monorepoRoot, "node_modules/.bin"); 86 + const envPath = options.env?.PATH || process.env.PATH || ""; 87 + const newPath = `${tspBinPath}:${envPath}`; 88 + 89 + const child = spawn(command, args, { 90 + cwd: options.cwd || this.path, 91 + env: { ...process.env, ...options.env, PATH: newPath }, 92 + }); 93 + 94 + let stdout = ""; 95 + let stderr = ""; 96 + 97 + child.stdout?.on("data", (data) => { 98 + stdout += data.toString(); 99 + }); 100 + 101 + child.stderr?.on("data", (data) => { 102 + stderr += data.toString(); 103 + }); 104 + 105 + if (options.input) { 106 + child.stdin?.write(options.input); 107 + child.stdin?.end(); 108 + } 109 + 110 + child.on("close", (exitCode) => { 111 + promiseResolve({ 112 + stdout, 113 + stderr, 114 + exitCode: exitCode ?? 0, 115 + output: stdout + stderr // Combined output for easier testing 116 + }); 117 + }); 118 + 119 + child.on("error", promiseReject); 120 + }); 121 + } 122 + 123 + async runTypelex(args: string[], options?: { input?: string; cwd?: string }): Promise<{ 124 + stdout: string; 125 + stderr: string; 126 + exitCode: number; 127 + output: string; // Combined stdout + stderr 128 + }> { 129 + // Use the local CLI from the monorepo 130 + const cliPath = resolve(__dirname, "../../dist/cli.js"); 131 + const result = await this.runCommand("node", [cliPath, ...args], options); 132 + return { 133 + ...result, 134 + output: result.stdout + result.stderr, 135 + }; 136 + } 137 + 138 + async compile(namespace: string, outDir: string = "./lexicons", options?: { cwd?: string }): Promise<void> { 139 + const result = await this.runTypelex(["compile", namespace, "--out", outDir], options); 140 + if (result.exitCode !== 0) { 141 + throw new Error(`Compilation failed: ${result.output}`); 142 + } 143 + } 144 + 145 + async init(namespace: string, options?: { cwd?: string }): Promise<void> { 146 + const result = await this.runTypelex(["init", "--setup"], { 147 + input: `${namespace}\n`, 148 + ...options, 149 + }); 150 + if (result.exitCode !== 0) { 151 + throw new Error(`Init failed: ${result.output}`); 152 + } 153 + } 154 + 155 + async runBuildScript(options?: { cwd?: string }): Promise<{stdout: string; stderr: string}> { 156 + const result = await this.runCommand("npm", ["run", "build:typelex"], options); 157 + if (result.exitCode !== 0) { 158 + throw new Error(`Build failed with exit code ${result.exitCode}:\n${result.output}`); 159 + } 160 + return { stdout: result.stdout, stderr: result.stderr }; 161 + } 162 + 163 + async expectBuildToFail(options?: { cwd?: string }): Promise<{stdout: string; stderr: string; output: string}> { 164 + const result = await this.runCommand("npm", ["run", "build:typelex"], options); 165 + if (result.exitCode === 0) { 166 + throw new Error(`Expected build to fail but it succeeded`); 167 + } 168 + return { stdout: result.stdout, stderr: result.stderr, output: result.output }; 169 + } 170 + 171 + /** 172 + * Compare files in the project against an expected directory 173 + * Only checks files that exist in expectedDir 174 + */ 175 + async compareTo(expectedSubdir: string = "expected"): Promise<void> { 176 + const { readdir } = await import("fs/promises"); 177 + 178 + if (!this.scenarioPath) { 179 + throw new Error("scenarioPath not set on TestProject"); 180 + } 181 + 182 + const expectedDir = join(this.scenarioPath, expectedSubdir); 183 + 184 + // Helper to recursively list all files in a directory 185 + async function listAllFiles(dir: string, prefix: string = ""): Promise<string[]> { 186 + const files: string[] = []; 187 + try { 188 + const entries = await readdir(dir, { withFileTypes: true }); 189 + for (const entry of entries) { 190 + const fullPath = join(dir, entry.name); 191 + const relPath = prefix ? join(prefix, entry.name) : entry.name; 192 + if (entry.isDirectory()) { 193 + files.push(...await listAllFiles(fullPath, relPath)); 194 + } else { 195 + files.push(relPath); 196 + } 197 + } 198 + } catch { 199 + // Directory doesn't exist 200 + } 201 + return files.sort(); 202 + } 203 + 204 + async function compareRecursive(relPath: string = "") { 205 + const expectedPath = join(expectedDir, relPath); 206 + const actualPath = join(this.path, relPath); 207 + 208 + const entries = await readdir(expectedPath, { withFileTypes: true }); 209 + 210 + for (const entry of entries) { 211 + const entryRelPath = join(relPath, entry.name); 212 + 213 + if (entry.isDirectory()) { 214 + await compareRecursive.call(this, entryRelPath); 215 + } else { 216 + const expected = await readFile(join(expectedDir, entryRelPath), "utf-8"); 217 + 218 + let actual: string; 219 + try { 220 + actual = await readFile(join(this.path, entryRelPath), "utf-8"); 221 + } catch (err) { 222 + if ((err as NodeJS.ErrnoException).code === "ENOENT") { 223 + // File is missing - show what files actually exist 224 + const actualFiles = await listAllFiles(this.path); 225 + throw new Error( 226 + `Expected file not found: ${entryRelPath}\n\n` + 227 + `Actual files in project:\n${actualFiles.map(f => ` ${f}`).join("\n") || " (none)"}` 228 + ); 229 + } 230 + throw err; 231 + } 232 + 233 + if (expected !== actual) { 234 + throw new Error( 235 + `File mismatch: ${entryRelPath}\n\nExpected:\n${expected}\n\nActual:\n${actual}` 236 + ); 237 + } 238 + } 239 + } 240 + } 241 + 242 + await compareRecursive.call(this); 243 + } 244 + 245 + /** 246 + * Mock npm/pnpm install by creating node_modules structure 247 + * Links to the real packages from the monorepo 248 + */ 249 + async mockInstall(): Promise<void> { 250 + const nodeModulesPath = join(this.path, "node_modules"); 251 + await mkdir(nodeModulesPath, { recursive: true }); 252 + await mkdir(join(nodeModulesPath, ".bin"), { recursive: true }); 253 + await mkdir(join(nodeModulesPath, "@typelex"), { recursive: true }); 254 + await mkdir(join(nodeModulesPath, "@typespec"), { recursive: true }); 255 + 256 + // Get paths to real packages in monorepo 257 + const monorepoRoot = resolve(__dirname, "../../../.."); 258 + const cliPackagePath = resolve(monorepoRoot, "packages/cli"); 259 + const emitterPackagePath = resolve(monorepoRoot, "packages/emitter"); 260 + const typespecCompilerPath = resolve(monorepoRoot, "node_modules/@typespec/compiler"); 261 + 262 + // Create symlinks to real packages 263 + const { symlink } = await import("fs/promises"); 264 + 265 + try { 266 + await symlink(cliPackagePath, join(nodeModulesPath, "@typelex/cli"), "dir"); 267 + } catch (err) { 268 + if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err; 269 + } 270 + 271 + try { 272 + await symlink(emitterPackagePath, join(nodeModulesPath, "@typelex/emitter"), "dir"); 273 + } catch (err) { 274 + if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err; 275 + } 276 + 277 + try { 278 + await symlink(typespecCompilerPath, join(nodeModulesPath, "@typespec/compiler"), "dir"); 279 + } catch (err) { 280 + if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err; 281 + } 282 + 283 + // Create bin symlink for typelex CLI 284 + const cliPath = resolve(cliPackagePath, "dist/cli.js"); 285 + try { 286 + await symlink(cliPath, join(nodeModulesPath, ".bin/typelex"), "file"); 287 + } catch (err) { 288 + if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err; 289 + } 290 + } 291 + }
+123
packages/cli/test/scenarios/README.md
··· 1 + # Test Scenarios 2 + 3 + This directory contains declarative test scenarios for the typelex CLI. 4 + 5 + ## Philosophy 6 + 7 + **These tests focus on CLI workflows, NOT language features.** 8 + 9 + The CLI's job is to: 10 + 1. Find/create lexicons directories (`./lexicons`, `../lexicons`) 11 + 2. Read existing JSON lexicons from disk 12 + 3. Generate `externals.tsp` from those JSON files 13 + 4. Run compilation while preserving external lexicons 14 + 5. Manage paths and directory structures correctly 15 + 16 + Language features (syntax, types, decorators) are tested in the emitter package. 17 + 18 + ## Test Coverage 19 + 20 + All non-trivial branches in the CLI code are tested. Each test was verified by: 21 + 1. Breaking the code (commenting out the condition) 22 + 2. Verifying the test fails 23 + 3. Fixing the code and verifying the test passes 24 + 25 + ### Current Scenarios (8 total) 26 + 27 + **External Lexicon Workflows** (The Core CLI Functionality): 28 + - `compile-with-external-atproto` - Real JSONโ†’TSPโ†’JSON cycle, externals preserved 29 + - `compile-to-parent-lexicons` - Compile with `../lexicons` directory 30 + - `compile-idempotent` - Deterministic output across runs 31 + 32 + **Init Workflows** (Directory Detection & File Management): 33 + - `init-finds-current-lexicons` - Detects `./lexicons`, no `--out` flag 34 + - `init-finds-parent-lexicons` - Detects `../lexicons`, adds `--out ../lexicons` 35 + - `init-overwrites-empty-main` - Empty `main.tsp` gets overwritten 36 + - `init-preserves-build-script` - Existing `build:typelex` not overwritten 37 + 38 + **Validation** (Error Handling): 39 + - `validation-errors` - Namespace format, path validation, file structure 40 + 41 + ### Branch Coverage Matrix 42 + 43 + | File | Line | Branch | Tested By | 44 + |------|------|--------|-----------| 45 + | compile.ts | 21 | Path validation | validation-errors | 46 + | ensure-imports.ts | 20 | First line check | validation-errors | 47 + | ensure-imports.ts | 26 | Second line check | validation-errors | 48 + | ensure-imports.ts | 32 | File not found | validation-errors | 49 + | externals-generator.ts | 87 | No externals case | All compile scenarios | 50 + | init.ts | 194 | Local lexicons dir | init-finds-current-lexicons | 51 + | init.ts | 203 | Parent lexicons dir | init-finds-parent-lexicons | 52 + | init.ts | 231 | Empty main.tsp | init-overwrites-empty-main | 53 + | init.ts | 252 | No scripts object | All init scenarios (crashes without) | 54 + | init.ts | 255 | Script exists | init-preserves-build-script | 55 + 56 + ## Structure 57 + 58 + Each scenario directory contains: 59 + 60 + ``` 61 + scenario-name/ 62 + project/ # Realistic project structure 63 + package.json 64 + typelex/ 65 + main.tsp # Input TypeSpec 66 + externals.tsp # Boilerplate or generated 67 + lexicons/ # REAL JSON FILES (not mocked!) 68 + com/atproto/... # Checked-in external lexicons 69 + expected/ # Expected outputs (optional) 70 + lexicons/ 71 + com/myapp/... 72 + test.ts # Test logic with run() function 73 + ``` 74 + 75 + ## Writing Tests 76 + 77 + The `test.ts` exports a `run()` function that performs assertions: 78 + 79 + ```typescript 80 + import { expect } from "vitest"; 81 + 82 + export const namespace = "com.myapp.*"; 83 + 84 + export async function run(project, scenarioPath) { 85 + // Compile 86 + await project.compile(namespace); 87 + 88 + // Assert on behavior 89 + const externals = await project.readFile("typelex/externals.tsp"); 90 + expect(externals).toContain("namespace com.atproto.label.defs"); 91 + 92 + // Verify files match expected 93 + await verifyExpectedFiles(join(scenarioPath, "expected"), project); 94 + } 95 + ``` 96 + 97 + Available exports: 98 + - `namespace` - Default namespace 99 + - `packageManager` - "npm" or "pnpm" 100 + - `lexiconsDirLocation` - "current", "parent" 101 + - `run(project, scenarioPath)` - Test logic 102 + 103 + Available helpers: 104 + - `project.compile(namespace, outDir?)` - Compile (throws on error) 105 + - `project.init(namespace)` - Run init (throws on error) 106 + - `project.runTypelex(args, options?)` - Run any command 107 + - `project.writeFile/readFile/readJson/fileExists` 108 + - `verifyExpectedFiles(expectedDir, project)` - Match expected outputs 109 + 110 + ## Key Insight 111 + 112 + Most tests should have **real lexicons/ folders with JSON files**. This tests the actual CLI behavior: reading JSON from disk, generating externals.tsp, and emitting new JSON that correctly references external lexicons. 113 + 114 + Don't test language features here - test file I/O, directory management, and the JSONโ†”TSPโ†”JSON workflow. 115 + 116 + ## Adding New Tests 117 + 118 + When adding a new scenario, verify it catches bugs: 119 + 1. Write the test 120 + 2. Break the corresponding code 121 + 3. Run tests - should FAIL 122 + 4. Fix the code 123 + 5. Run tests - should PASS
+27
packages/cli/test/scenarios/basic/expected/lexicons/com/atproto/label/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.label.defs", 4 + "defs": { 5 + "selfLabels": { 6 + "type": "object", 7 + "properties": { 8 + "values": { 9 + "type": "array", 10 + "items": { "type": "ref", "ref": "#selfLabel" }, 11 + "maxLength": 10 12 + } 13 + }, 14 + "required": ["values"] 15 + }, 16 + "selfLabel": { 17 + "type": "object", 18 + "properties": { 19 + "val": { 20 + "type": "string", 21 + "maxLength": 128 22 + } 23 + }, 24 + "required": ["val"] 25 + } 26 + } 27 + }
+21
packages/cli/test/scenarios/basic/expected/lexicons/com/test/example/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.test.example.profile", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "literal:self", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "description": { 12 + "type": "string", 13 + "maxGraphemes": 256, 14 + "description": "Free-form profile description." 15 + } 16 + } 17 + }, 18 + "description": "My profile." 19 + } 20 + } 21 + }
+8
packages/cli/test/scenarios/basic/expected/package.json
··· 1 + { 2 + "name": "test-idempotent", 3 + "version": "1.0.0", 4 + "type": "module", 5 + "scripts": { 6 + "build:typelex": "typelex compile com.test.*" 7 + } 8 + }
+10
packages/cli/test/scenarios/basic/expected/typelex/externals.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Generated by typelex from ./lexicons (excluding com.test.*) 4 + // This file is auto-generated. Do not edit manually. 5 + 6 + @external 7 + namespace com.atproto.label.defs { 8 + model SelfLabel { } 9 + model SelfLabels { } 10 + }
+12
packages/cli/test/scenarios/basic/expected/typelex/main.tsp
··· 1 + import "@typelex/emitter"; 2 + import "./externals.tsp"; 3 + 4 + namespace com.test.example.profile { 5 + /** My profile. */ 6 + @rec("literal:self") 7 + model Main { 8 + /** Free-form profile description.*/ 9 + @maxGraphemes(256) 10 + description?: string; 11 + } 12 + }
+27
packages/cli/test/scenarios/basic/project/lexicons/com/atproto/label/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.label.defs", 4 + "defs": { 5 + "selfLabels": { 6 + "type": "object", 7 + "properties": { 8 + "values": { 9 + "type": "array", 10 + "items": { "type": "ref", "ref": "#selfLabel" }, 11 + "maxLength": 10 12 + } 13 + }, 14 + "required": ["values"] 15 + }, 16 + "selfLabel": { 17 + "type": "object", 18 + "properties": { 19 + "val": { 20 + "type": "string", 21 + "maxLength": 128 22 + } 23 + }, 24 + "required": ["val"] 25 + } 26 + } 27 + }
+1
packages/cli/test/scenarios/basic/project/package.json
··· 1 + {"name":"test-idempotent","version":"1.0.0","type":"module"}
+10
packages/cli/test/scenarios/basic/test.ts
··· 1 + export async function run(project) { 2 + await project.init("com.test.*"); 3 + 4 + await project.runBuildScript(); 5 + await project.compareTo("expected"); 6 + 7 + // Second build - verify idempotency 8 + await project.runBuildScript(); 9 + await project.compareTo("expected"); 10 + }
+25
packages/cli/test/scenarios/init-preserves-main/expected/lexicons/com/example/custom.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.example.custom", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "foo": { 12 + "type": "string" 13 + }, 14 + "bar": { 15 + "type": "integer" 16 + } 17 + }, 18 + "required": [ 19 + "foo", 20 + "bar" 21 + ] 22 + } 23 + } 24 + } 25 + }
+8
packages/cli/test/scenarios/init-preserves-main/expected/package.json
··· 1 + { 2 + "name": "test-init-preserves-main", 3 + "version": "1.0.0", 4 + "type": "module", 5 + "scripts": { 6 + "build:typelex": "typelex compile com.example.*" 7 + } 8 + }
+4
packages/cli/test/scenarios/init-preserves-main/expected/typelex/externals.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Generated by typelex from ./lexicons (excluding com.example.*) 4 + // No external lexicons found
+10
packages/cli/test/scenarios/init-preserves-main/expected/typelex/main.tsp
··· 1 + import "@typelex/emitter"; 2 + import "./externals.tsp"; 3 + 4 + namespace com.example.custom { 5 + @rec("tid") 6 + model Main { 7 + @required foo: string; 8 + @required bar: integer; 9 + } 10 + }
+1
packages/cli/test/scenarios/init-preserves-main/project/package.json
··· 1 + {"name":"test-init-preserves-main","version":"1.0.0","type":"module"}
+10
packages/cli/test/scenarios/init-preserves-main/project/typelex/main.tsp
··· 1 + import "@typelex/emitter"; 2 + import "./externals.tsp"; 3 + 4 + namespace com.example.custom { 5 + @rec("tid") 6 + model Main { 7 + @required foo: string; 8 + @required bar: integer; 9 + } 10 + }
+9
packages/cli/test/scenarios/init-preserves-main/test.ts
··· 1 + export async function run(project) { 2 + await project.init("com.example.*"); 3 + await project.runBuildScript(); 4 + await project.compareTo("expected"); 5 + 6 + // Second build - verify idempotency 7 + await project.runBuildScript(); 8 + await project.compareTo("expected"); 9 + }
+21
packages/cli/test/scenarios/missing-dependency/expected/lexicons/com/external/media/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.external.media.defs", 4 + "defs": { 5 + "video": { 6 + "type": "object", 7 + "properties": { 8 + "url": { 9 + "type": "string", 10 + "format": "uri" 11 + }, 12 + "mimeType": { 13 + "type": "string" 14 + } 15 + }, 16 + "required": [ 17 + "url" 18 + ] 19 + } 20 + } 21 + }
+25
packages/cli/test/scenarios/missing-dependency/expected/lexicons/com/myapp/post.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.myapp.post", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "text": { 12 + "type": "string" 13 + }, 14 + "video": { 15 + "type": "ref", 16 + "ref": "com.external.media.defs#video" 17 + } 18 + }, 19 + "required": [ 20 + "text" 21 + ] 22 + } 23 + } 24 + } 25 + }
+8
packages/cli/test/scenarios/missing-dependency/expected/package.json
··· 1 + { 2 + "name": "test-missing-dependency", 3 + "version": "1.0.0", 4 + "type": "module", 5 + "scripts": { 6 + "build:typelex": "typelex compile com.myapp.*" 7 + } 8 + }
+9
packages/cli/test/scenarios/missing-dependency/expected/typelex/externals.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Generated by typelex from ./lexicons (excluding com.myapp.*) 4 + // This file is auto-generated. Do not edit manually. 5 + 6 + @external 7 + namespace com.external.media.defs { 8 + model Video { } 9 + }
+10
packages/cli/test/scenarios/missing-dependency/expected/typelex/main.tsp
··· 1 + import "@typelex/emitter"; 2 + import "./externals.tsp"; 3 + 4 + namespace com.myapp.post { 5 + @rec("tid") 6 + model Main { 7 + @required text: string; 8 + video?: com.external.media.defs.Video; 9 + } 10 + }
+1
packages/cli/test/scenarios/missing-dependency/project/package.json
··· 1 + {"name":"test-missing-dependency","version":"1.0.0","type":"module"}
+51
packages/cli/test/scenarios/missing-dependency/test.ts
··· 1 + export async function run(project) { 2 + await project.init("com.myapp.*"); 3 + 4 + // Edit main.tsp to reference a missing external lexicon 5 + await project.writeFile("typelex/main.tsp", `import "@typelex/emitter"; 6 + import "./externals.tsp"; 7 + 8 + namespace com.myapp.post { 9 + @rec("tid") 10 + model Main { 11 + @required text: string; 12 + video?: com.external.media.defs.Video; 13 + } 14 + } 15 + `); 16 + 17 + // Build should fail because com.external.media.defs doesn't exist 18 + const failure = await project.expectBuildToFail(); 19 + if (!failure.output.includes("com.external.media.defs")) { 20 + throw new Error(`Expected error about missing com.external.media.defs, got: ${failure.output}`); 21 + } 22 + 23 + // Add the missing external lexicon 24 + await project.writeFile("lexicons/com/external/media/defs.json", JSON.stringify({ 25 + "lexicon": 1, 26 + "id": "com.external.media.defs", 27 + "defs": { 28 + "video": { 29 + "type": "object", 30 + "properties": { 31 + "url": { 32 + "type": "string", 33 + "format": "uri" 34 + }, 35 + "mimeType": { 36 + "type": "string" 37 + } 38 + }, 39 + "required": ["url"] 40 + } 41 + } 42 + }, null, 2) + "\n"); 43 + 44 + // Now build should succeed 45 + await project.runBuildScript(); 46 + await project.compareTo("expected"); 47 + 48 + // Verify idempotency 49 + await project.runBuildScript(); 50 + await project.compareTo("expected"); 51 + }
+21
packages/cli/test/scenarios/nested-init/expected/lexicons/com/myservice/example/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.myservice.example.profile", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "literal:self", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "description": { 12 + "type": "string", 13 + "maxGraphemes": 256, 14 + "description": "Free-form profile description." 15 + } 16 + } 17 + }, 18 + "description": "My profile." 19 + } 20 + } 21 + }
+8
packages/cli/test/scenarios/nested-init/expected/package.json
··· 1 + { 2 + "name": "test-nested-init", 3 + "version": "1.0.0", 4 + "type": "module", 5 + "scripts": { 6 + "build:typelex": "typelex compile com.myservice.*" 7 + } 8 + }
+4
packages/cli/test/scenarios/nested-init/expected/typelex/externals.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Generated by typelex from ./lexicons (excluding com.myservice.*) 4 + // No external lexicons found
+12
packages/cli/test/scenarios/nested-init/expected/typelex/main.tsp
··· 1 + import "@typelex/emitter"; 2 + import "./externals.tsp"; 3 + 4 + namespace com.myservice.example.profile { 5 + /** My profile. */ 6 + @rec("literal:self") 7 + model Main { 8 + /** Free-form profile description.*/ 9 + @maxGraphemes(256) 10 + description?: string; 11 + } 12 + }
+1
packages/cli/test/scenarios/nested-init/project/package.json
··· 1 + {"name":"test-nested-init","version":"1.0.0","type":"module"}
+16
packages/cli/test/scenarios/nested-init/test.ts
··· 1 + import { join } from "path"; 2 + 3 + export async function run(project) { 4 + const apiDir = join(project.path, "src/api"); 5 + 6 + // Init at root (where package.json is) 7 + await project.init("com.myservice.*"); 8 + 9 + // Build from nested directory should work (this is what we're testing) 10 + await project.runBuildScript({ cwd: apiDir }); 11 + await project.compareTo("expected"); 12 + 13 + // Verify idempotency 14 + await project.runBuildScript({ cwd: apiDir }); 15 + await project.compareTo("expected"); 16 + }
+8
packages/cli/test/scenarios/parent-lexicons/expected1/app/package.json
··· 1 + { 2 + "name": "test-parent-lexicons", 3 + "version": "1.0.0", 4 + "type": "module", 5 + "scripts": { 6 + "build:typelex": "typelex compile com.myapp.* --out ../lexicons" 7 + } 8 + }
+10
packages/cli/test/scenarios/parent-lexicons/expected1/app/typelex/externals.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Generated by typelex from ../lexicons (excluding com.myapp.*) 4 + // This file is auto-generated. Do not edit manually. 5 + 6 + @external 7 + namespace com.atproto.label.defs { 8 + model SelfLabel { } 9 + model SelfLabels { } 10 + }
+12
packages/cli/test/scenarios/parent-lexicons/expected1/app/typelex/main.tsp
··· 1 + import "@typelex/emitter"; 2 + import "./externals.tsp"; 3 + 4 + namespace com.myapp.example.profile { 5 + /** My profile. */ 6 + @rec("literal:self") 7 + model Main { 8 + /** Free-form profile description.*/ 9 + @maxGraphemes(256) 10 + description?: string; 11 + } 12 + }
+27
packages/cli/test/scenarios/parent-lexicons/expected1/lexicons/com/atproto/label/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.label.defs", 4 + "defs": { 5 + "selfLabels": { 6 + "type": "object", 7 + "properties": { 8 + "values": { 9 + "type": "array", 10 + "items": { "type": "ref", "ref": "#selfLabel" }, 11 + "maxLength": 10 12 + } 13 + }, 14 + "required": ["values"] 15 + }, 16 + "selfLabel": { 17 + "type": "object", 18 + "properties": { 19 + "val": { 20 + "type": "string", 21 + "maxLength": 128 22 + } 23 + }, 24 + "required": ["val"] 25 + } 26 + } 27 + }
+21
packages/cli/test/scenarios/parent-lexicons/expected1/lexicons/com/myapp/example/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.myapp.example.profile", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "literal:self", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "description": { 12 + "type": "string", 13 + "maxGraphemes": 256, 14 + "description": "Free-form profile description." 15 + } 16 + } 17 + }, 18 + "description": "My profile." 19 + } 20 + } 21 + }
+8
packages/cli/test/scenarios/parent-lexicons/expected2/app/package.json
··· 1 + { 2 + "name": "test-parent-lexicons", 3 + "version": "1.0.0", 4 + "type": "module", 5 + "scripts": { 6 + "build:typelex": "typelex compile com.myapp.* --out ../lexicons" 7 + } 8 + }
+10
packages/cli/test/scenarios/parent-lexicons/expected2/app/typelex/externals.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Generated by typelex from ../lexicons (excluding com.myapp.*) 4 + // This file is auto-generated. Do not edit manually. 5 + 6 + @external 7 + namespace com.atproto.label.defs { 8 + model SelfLabel { } 9 + model SelfLabels { } 10 + }
+13
packages/cli/test/scenarios/parent-lexicons/expected2/app/typelex/main.tsp
··· 1 + import "@typelex/emitter"; 2 + import "./externals.tsp"; 3 + 4 + namespace com.myapp.example.profile { 5 + /** My profile. */ 6 + @rec("literal:self") 7 + model Main { 8 + /** Free-form profile description.*/ 9 + @maxGraphemes(256) 10 + description?: string; 11 + labels?: com.atproto.label.defs.SelfLabels; 12 + } 13 + }
+27
packages/cli/test/scenarios/parent-lexicons/expected2/lexicons/com/atproto/label/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.label.defs", 4 + "defs": { 5 + "selfLabels": { 6 + "type": "object", 7 + "properties": { 8 + "values": { 9 + "type": "array", 10 + "items": { "type": "ref", "ref": "#selfLabel" }, 11 + "maxLength": 10 12 + } 13 + }, 14 + "required": ["values"] 15 + }, 16 + "selfLabel": { 17 + "type": "object", 18 + "properties": { 19 + "val": { 20 + "type": "string", 21 + "maxLength": 128 22 + } 23 + }, 24 + "required": ["val"] 25 + } 26 + } 27 + }
+25
packages/cli/test/scenarios/parent-lexicons/expected2/lexicons/com/myapp/example/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.myapp.example.profile", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "literal:self", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "description": { 12 + "type": "string", 13 + "maxGraphemes": 256, 14 + "description": "Free-form profile description." 15 + }, 16 + "labels": { 17 + "type": "ref", 18 + "ref": "com.atproto.label.defs#selfLabels" 19 + } 20 + } 21 + }, 22 + "description": "My profile." 23 + } 24 + } 25 + }
+1
packages/cli/test/scenarios/parent-lexicons/project/app/package.json
··· 1 + {"name":"test-parent-lexicons","version":"1.0.0","type":"module"}
+27
packages/cli/test/scenarios/parent-lexicons/project/lexicons/com/atproto/label/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.label.defs", 4 + "defs": { 5 + "selfLabels": { 6 + "type": "object", 7 + "properties": { 8 + "values": { 9 + "type": "array", 10 + "items": { "type": "ref", "ref": "#selfLabel" }, 11 + "maxLength": 10 12 + } 13 + }, 14 + "required": ["values"] 15 + }, 16 + "selfLabel": { 17 + "type": "object", 18 + "properties": { 19 + "val": { 20 + "type": "string", 21 + "maxLength": 128 22 + } 23 + }, 24 + "required": ["val"] 25 + } 26 + } 27 + }
+45
packages/cli/test/scenarios/parent-lexicons/test.ts
··· 1 + import { join } from "path"; 2 + 3 + export async function run(project) { 4 + const appDir = join(project.path, "app"); 5 + 6 + await project.init("com.myapp.*", { cwd: appDir }); 7 + 8 + // Verify init generated externals.tsp with existing external lexicons (before build) 9 + const externals = await project.readFile("app/typelex/externals.tsp"); 10 + if (!externals.includes("com.atproto.label.defs")) { 11 + throw new Error( 12 + "externals.tsp should contain external lexicons after init", 13 + ); 14 + } 15 + 16 + // Verify init created a working project with default main.tsp 17 + await project.runBuildScript({ cwd: appDir }); 18 + await project.compareTo("expected1"); 19 + 20 + // Edit main.tsp to add labels (simulates user editing the file) 21 + await project.writeFile( 22 + "app/typelex/main.tsp", 23 + `import "@typelex/emitter"; 24 + import "./externals.tsp"; 25 + 26 + namespace com.myapp.example.profile { 27 + /** My profile. */ 28 + @rec("literal:self") 29 + model Main { 30 + /** Free-form profile description.*/ 31 + @maxGraphemes(256) 32 + description?: string; 33 + labels?: com.atproto.label.defs.SelfLabels; 34 + } 35 + } 36 + `, 37 + ); 38 + 39 + await project.runBuildScript({ cwd: appDir }); 40 + await project.compareTo("expected2"); 41 + 42 + // Third build - verify idempotency 43 + await project.runBuildScript({ cwd: appDir }); 44 + await project.compareTo("expected2"); 45 + }
+14
packages/cli/test/scenarios/reserved-keywords/expected/lexicons/app/bsky/feed/post/record.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.feed.post.record", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": { 8 + "text": { 9 + "type": "string" 10 + } 11 + } 12 + } 13 + } 14 + }
+14
packages/cli/test/scenarios/reserved-keywords/expected/lexicons/com/atproto/server/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.server.defs", 4 + "defs": { 5 + "inviteCode": { 6 + "type": "object", 7 + "properties": { 8 + "code": { 9 + "type": "string" 10 + } 11 + } 12 + } 13 + } 14 + }
+21
packages/cli/test/scenarios/reserved-keywords/expected/lexicons/pub/leaflet/example/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "pub.leaflet.example.profile", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "literal:self", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "description": { 12 + "type": "string", 13 + "maxGraphemes": 256, 14 + "description": "Free-form profile description." 15 + } 16 + } 17 + }, 18 + "description": "My profile." 19 + } 20 + } 21 + }
+7
packages/cli/test/scenarios/reserved-keywords/expected/package.json
··· 1 + { 2 + "name": "reserved-keywords-test", 3 + "type": "module", 4 + "scripts": { 5 + "build:typelex": "typelex compile pub.leaflet.*" 6 + } 7 + }
+14
packages/cli/test/scenarios/reserved-keywords/expected/typelex/externals.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Generated by typelex from ./lexicons (excluding pub.leaflet.*) 4 + // This file is auto-generated. Do not edit manually. 5 + 6 + @external 7 + namespace app.bsky.feed.post.`record` { 8 + model Main { } 9 + } 10 + 11 + @external 12 + namespace com.atproto.server.defs { 13 + model InviteCode { } 14 + }
+12
packages/cli/test/scenarios/reserved-keywords/expected/typelex/main.tsp
··· 1 + import "@typelex/emitter"; 2 + import "./externals.tsp"; 3 + 4 + namespace `pub`.leaflet.example.profile { 5 + /** My profile. */ 6 + @rec("literal:self") 7 + model Main { 8 + /** Free-form profile description.*/ 9 + @maxGraphemes(256) 10 + description?: string; 11 + } 12 + }
+14
packages/cli/test/scenarios/reserved-keywords/project/lexicons/app/bsky/feed/post/record.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.feed.post.record", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": { 8 + "text": { 9 + "type": "string" 10 + } 11 + } 12 + } 13 + } 14 + }
+14
packages/cli/test/scenarios/reserved-keywords/project/lexicons/com/atproto/server/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.server.defs", 4 + "defs": { 5 + "inviteCode": { 6 + "type": "object", 7 + "properties": { 8 + "code": { 9 + "type": "string" 10 + } 11 + } 12 + } 13 + } 14 + }
+4
packages/cli/test/scenarios/reserved-keywords/project/package.json
··· 1 + { 2 + "name": "reserved-keywords-test", 3 + "type": "module" 4 + }
+10
packages/cli/test/scenarios/reserved-keywords/test.ts
··· 1 + export async function run(project) { 2 + await project.init("pub.leaflet.*"); 3 + 4 + await project.runBuildScript(); 5 + await project.compareTo("expected"); 6 + 7 + // Second build - verify idempotency 8 + await project.runBuildScript(); 9 + await project.compareTo("expected"); 10 + }
+1
packages/cli/test/scenarios/validation-errors/project/package.json
··· 1 + {"name":"test-validation","version":"1.0.0","type":"module"}
+35
packages/cli/test/scenarios/validation-errors/test.ts
··· 1 + import { expect } from "vitest"; 2 + 3 + export async function run(project) { 4 + // Test: Namespace must end with .* 5 + let result = await project.runTypelex(["compile", "com.example"]); 6 + expect(result.exitCode).not.toBe(0); 7 + expect(result.output).toContain("namespace must end with .*"); 8 + 9 + // Test: Output path must end with 'lexicons' 10 + await project.writeFile("typelex/main.tsp", `import "@typelex/emitter";\nimport "./externals.tsp";\n`); 11 + await project.writeFile("typelex/externals.tsp", `import "@typelex/emitter";\n`); 12 + 13 + result = await project.runTypelex(["compile", "com.test.*", "--out", "./output"]); 14 + expect(result.exitCode).not.toBe(0); 15 + expect(result.output).toContain("Output directory must end with 'lexicons'"); 16 + 17 + // Test: main.tsp must exist 18 + await project.runCommand("rm", ["-rf", "typelex"]); 19 + result = await project.runTypelex(["compile", "com.test.*"]); 20 + expect(result.exitCode).not.toBe(0); 21 + expect(result.output).toContain("main.tsp not found"); 22 + 23 + // Test: main.tsp first line must be import "@typelex/emitter" 24 + await project.writeFile("typelex/main.tsp", `// wrong first line\nimport "./externals.tsp";\n`); 25 + await project.writeFile("typelex/externals.tsp", `import "@typelex/emitter";\n`); 26 + result = await project.runTypelex(["compile", "com.test.*"]); 27 + expect(result.exitCode).not.toBe(0); 28 + expect(result.output).toContain('main.tsp must start with: import "@typelex/emitter"'); 29 + 30 + // Test: main.tsp second line must be import "./externals.tsp" 31 + await project.writeFile("typelex/main.tsp", `import "@typelex/emitter";\n// wrong second line\n`); 32 + result = await project.runTypelex(["compile", "com.test.*"]); 33 + expect(result.exitCode).not.toBe(0); 34 + expect(result.output).toContain('Line 2 of main.tsp must be: import "./externals.tsp"'); 35 + }
+27
packages/cli/test/scenarios/with-external-lexicons/expected1/lexicons/com/atproto/label/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.label.defs", 4 + "defs": { 5 + "selfLabels": { 6 + "type": "object", 7 + "properties": { 8 + "values": { 9 + "type": "array", 10 + "items": { "type": "ref", "ref": "#selfLabel" }, 11 + "maxLength": 10 12 + } 13 + }, 14 + "required": ["values"] 15 + }, 16 + "selfLabel": { 17 + "type": "object", 18 + "properties": { 19 + "val": { 20 + "type": "string", 21 + "maxLength": 128 22 + } 23 + }, 24 + "required": ["val"] 25 + } 26 + } 27 + }
+21
packages/cli/test/scenarios/with-external-lexicons/expected1/lexicons/com/myapp/example/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.myapp.example.profile", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "literal:self", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "description": { 12 + "type": "string", 13 + "maxGraphemes": 256, 14 + "description": "Free-form profile description." 15 + } 16 + } 17 + }, 18 + "description": "My profile." 19 + } 20 + } 21 + }
+8
packages/cli/test/scenarios/with-external-lexicons/expected1/package.json
··· 1 + { 2 + "name": "test-external-lexicons", 3 + "version": "1.0.0", 4 + "type": "module", 5 + "scripts": { 6 + "build:typelex": "typelex compile com.myapp.*" 7 + } 8 + }
+10
packages/cli/test/scenarios/with-external-lexicons/expected1/typelex/externals.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Generated by typelex from ./lexicons (excluding com.myapp.*) 4 + // This file is auto-generated. Do not edit manually. 5 + 6 + @external 7 + namespace com.atproto.label.defs { 8 + model SelfLabel { } 9 + model SelfLabels { } 10 + }
+12
packages/cli/test/scenarios/with-external-lexicons/expected1/typelex/main.tsp
··· 1 + import "@typelex/emitter"; 2 + import "./externals.tsp"; 3 + 4 + namespace com.myapp.example.profile { 5 + /** My profile. */ 6 + @rec("literal:self") 7 + model Main { 8 + /** Free-form profile description.*/ 9 + @maxGraphemes(256) 10 + description?: string; 11 + } 12 + }
+27
packages/cli/test/scenarios/with-external-lexicons/expected2/lexicons/com/atproto/label/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.label.defs", 4 + "defs": { 5 + "selfLabels": { 6 + "type": "object", 7 + "properties": { 8 + "values": { 9 + "type": "array", 10 + "items": { "type": "ref", "ref": "#selfLabel" }, 11 + "maxLength": 10 12 + } 13 + }, 14 + "required": ["values"] 15 + }, 16 + "selfLabel": { 17 + "type": "object", 18 + "properties": { 19 + "val": { 20 + "type": "string", 21 + "maxLength": 128 22 + } 23 + }, 24 + "required": ["val"] 25 + } 26 + } 27 + }
+30
packages/cli/test/scenarios/with-external-lexicons/expected2/lexicons/com/myapp/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.myapp.profile", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": { 8 + "did": { 9 + "type": "string", 10 + "format": "did" 11 + }, 12 + "handle": { 13 + "type": "string", 14 + "format": "handle" 15 + }, 16 + "displayName": { 17 + "type": "string" 18 + }, 19 + "labels": { 20 + "type": "ref", 21 + "ref": "com.atproto.label.defs#selfLabels" 22 + } 23 + }, 24 + "required": [ 25 + "did", 26 + "handle" 27 + ] 28 + } 29 + } 30 + }
+8
packages/cli/test/scenarios/with-external-lexicons/expected2/package.json
··· 1 + { 2 + "name": "test-external-lexicons", 3 + "version": "1.0.0", 4 + "type": "module", 5 + "scripts": { 6 + "build:typelex": "typelex compile com.myapp.*" 7 + } 8 + }
+10
packages/cli/test/scenarios/with-external-lexicons/expected2/typelex/externals.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Generated by typelex from ./lexicons (excluding com.myapp.*) 4 + // This file is auto-generated. Do not edit manually. 5 + 6 + @external 7 + namespace com.atproto.label.defs { 8 + model SelfLabel { } 9 + model SelfLabels { } 10 + }
+13
packages/cli/test/scenarios/with-external-lexicons/expected2/typelex/main.tsp
··· 1 + import "@typelex/emitter"; 2 + import "./externals.tsp"; 3 + 4 + namespace com.myapp.profile { 5 + model Main { 6 + @required did: did; 7 + @required handle: handle; 8 + displayName?: string; 9 + 10 + // Reference to external lexicon 11 + labels?: com.atproto.label.defs.SelfLabels; 12 + } 13 + }
+27
packages/cli/test/scenarios/with-external-lexicons/project/lexicons/com/atproto/label/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.label.defs", 4 + "defs": { 5 + "selfLabels": { 6 + "type": "object", 7 + "properties": { 8 + "values": { 9 + "type": "array", 10 + "items": { "type": "ref", "ref": "#selfLabel" }, 11 + "maxLength": 10 12 + } 13 + }, 14 + "required": ["values"] 15 + }, 16 + "selfLabel": { 17 + "type": "object", 18 + "properties": { 19 + "val": { 20 + "type": "string", 21 + "maxLength": 128 22 + } 23 + }, 24 + "required": ["val"] 25 + } 26 + } 27 + }
+1
packages/cli/test/scenarios/with-external-lexicons/project/package.json
··· 1 + {"name":"test-external-lexicons","version":"1.0.0","type":"module"}
+36
packages/cli/test/scenarios/with-external-lexicons/test.ts
··· 1 + export async function run(project) { 2 + await project.init("com.myapp.*"); 3 + 4 + // Verify init generated externals.tsp with existing external lexicons (before build) 5 + const externals = await project.readFile("typelex/externals.tsp"); 6 + if (!externals.includes("com.atproto.label.defs")) { 7 + throw new Error("externals.tsp should contain external lexicons after init"); 8 + } 9 + 10 + // Verify init created a working project with default main.tsp 11 + await project.runBuildScript(); 12 + await project.compareTo("expected1"); 13 + 14 + // Edit main.tsp to add a profile schema (simulates user editing the file) 15 + await project.writeFile("typelex/main.tsp", `import "@typelex/emitter"; 16 + import "./externals.tsp"; 17 + 18 + namespace com.myapp.profile { 19 + model Main { 20 + @required did: did; 21 + @required handle: handle; 22 + displayName?: string; 23 + 24 + // Reference to external lexicon 25 + labels?: com.atproto.label.defs.SelfLabels; 26 + } 27 + } 28 + `); 29 + 30 + await project.runBuildScript(); 31 + await project.compareTo("expected2"); 32 + 33 + // Third build - verify idempotency 34 + await project.runBuildScript(); 35 + await project.compareTo("expected2"); 36 + }
+77
packages/cli/test/scenarios.test.ts
··· 1 + import { describe, it, afterEach } from "vitest"; 2 + import { readdirSync, statSync, existsSync } from "fs"; 3 + import { readFile, readdir } from "fs/promises"; 4 + import { join, dirname, relative } from "path"; 5 + import { fileURLToPath } from "url"; 6 + import { TestProject } from "./helpers/test-project.js"; 7 + 8 + const __filename = fileURLToPath(import.meta.url); 9 + const __dirname = dirname(__filename); 10 + 11 + const SCENARIOS_DIR = join(__dirname, "scenarios"); 12 + 13 + async function copyDirRecursive(src: string, dest: string, project: TestProject) { 14 + const { mkdir } = await import("fs/promises"); 15 + const entries = await readdir(src, { withFileTypes: true }); 16 + 17 + for (const entry of entries) { 18 + const srcPath = join(src, entry.name); 19 + const destPath = join(dest, entry.name); 20 + 21 + if (entry.isDirectory()) { 22 + // Create the directory in destination (even if empty) 23 + const relativePath = relative(project.path, destPath); 24 + await mkdir(join(project.path, relativePath), { recursive: true }); 25 + await copyDirRecursive(srcPath, destPath, project); 26 + } else { 27 + const content = await readFile(srcPath, "utf-8"); 28 + const relativePath = relative(project.path, destPath); 29 + await project.writeFile(relativePath, content); 30 + } 31 + } 32 + } 33 + 34 + describe("CLI scenarios", () => { 35 + let project: TestProject; 36 + 37 + afterEach(async () => { 38 + if (project) { 39 + await project.cleanup(); 40 + } 41 + }); 42 + 43 + // Auto-discover scenario directories 44 + const scenarios = readdirSync(SCENARIOS_DIR) 45 + .map((name) => join(SCENARIOS_DIR, name)) 46 + .filter((path) => statSync(path).isDirectory()) 47 + .filter((path) => existsSync(join(path, "test.ts"))); 48 + 49 + for (const scenarioPath of scenarios) { 50 + const scenarioName = scenarioPath.split("/").pop()!; 51 + 52 + it(scenarioName, async () => { 53 + // Load test module to get config 54 + const testModule = await import(join(scenarioPath, "test.ts")); 55 + if (typeof testModule.run !== "function") { 56 + throw new Error(`${scenarioName}/test.ts must export a run() function`); 57 + } 58 + 59 + // Create project 60 + project = await TestProject.create({ 61 + packageManager: testModule.packageManager || "npm", 62 + }); 63 + project.scenarioPath = scenarioPath; 64 + 65 + // Copy project files 66 + const projectDir = join(scenarioPath, "project"); 67 + if (existsSync(projectDir)) { 68 + await copyDirRecursive(projectDir, project.path, project); 69 + } 70 + 71 + await project.mockInstall(); 72 + 73 + // Run the scenario 74 + await testModule.run(project); 75 + }); 76 + } 77 + });
+20
packages/cli/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "Node16", 5 + "moduleResolution": "Node16", 6 + "lib": ["ES2022"], 7 + "outDir": "dist", 8 + "rootDir": "src", 9 + "declaration": true, 10 + "declarationMap": true, 11 + "sourceMap": true, 12 + "strict": true, 13 + "esModuleInterop": true, 14 + "skipLibCheck": true, 15 + "forceConsistentCasingInFileNames": true, 16 + "resolveJsonModule": true 17 + }, 18 + "include": ["src/**/*"], 19 + "exclude": ["node_modules", "dist"] 20 + }
+4
packages/cli/typelex/externals.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Generated by typelex 4 + // No external lexicons found
+10
packages/cli/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config'; 2 + 3 + export default defineConfig({ 4 + test: { 5 + globals: true, 6 + environment: 'node', 7 + testTimeout: 60000, // CLI operations can take time 8 + hookTimeout: 60000, 9 + }, 10 + });
+65
packages/emitter/lib/decorators.tsp
··· 163 163 extern dec errors(target: unknown, ...errors: unknown[]); 164 164 165 165 /** 166 + * Forces a model, scalar, or union to be inlined instead of creating a standalone def. 167 + * By default, named types create separate definitions with references. 168 + * Use @inline to expand the type inline at each usage site. 169 + * 170 + * @example Inline model 171 + * ```typespec 172 + * @inline 173 + * model Caption { 174 + * text?: string; 175 + * } 176 + * 177 + * model Main { 178 + * captions?: Caption[]; // Expands inline, no separate "caption" def 179 + * } 180 + * ``` 181 + * 182 + * @example Inline scalar 183 + * ```typespec 184 + * @inline 185 + * @maxLength(50) 186 + * scalar Handle extends string; 187 + * 188 + * model Main { 189 + * handle?: Handle; // Expands to { type: "string", maxLength: 50 } 190 + * } 191 + * ``` 192 + * 193 + * @example Inline union 194 + * ```typespec 195 + * @inline 196 + * union Status { "active", "inactive", string } 197 + * 198 + * model Main { 199 + * status?: Status; // Expands inline with knownValues 200 + * } 201 + * ``` 202 + */ 203 + extern dec inline(target: unknown); 204 + 205 + /** 206 + * Specifies a default value for a scalar or union definition. 207 + * Only valid on standalone scalar or union defs (not @inline). 208 + * The value must match the underlying type (string, integer, or boolean). 209 + * For unions with token refs, you can pass a model reference directly. 210 + * 211 + * @param value - The default value (literal or model reference for tokens) 212 + * 213 + * @example Scalar with default 214 + * ```typespec 215 + * @default("standard") 216 + * scalar Mode extends string; 217 + * ``` 218 + * 219 + * @example Union with token default 220 + * ```typespec 221 + * @default(Inperson) 222 + * union EventMode { Hybrid, Inperson, Virtual, string } 223 + * 224 + * @token 225 + * model Inperson {} 226 + * ``` 227 + */ 228 + extern dec `default`(target: unknown, value: unknown); 229 + 230 + /** 166 231 * Marks a namespace as external, preventing it from emitting JSON output. 167 232 * This decorator can only be applied to namespaces. 168 233 * Useful for importing definitions from other lexicons without re-emitting them.
+1 -1
packages/emitter/package.json
··· 1 1 { 2 2 "name": "@typelex/emitter", 3 - "version": "0.1.6", 3 + "version": "0.3.1", 4 4 "description": "TypeSpec emitter for ATProto Lexicon definitions", 5 5 "main": "dist/index.js", 6 6 "type": "module",
+18
packages/emitter/src/decorators.ts
··· 25 25 const maxBytesKey = Symbol("maxBytes"); 26 26 const minBytesKey = Symbol("minBytes"); 27 27 const externalKey = Symbol("external"); 28 + const defaultKey = Symbol("default"); 28 29 29 30 /** 30 31 * @maxBytes decorator for maximum length of bytes type ··· 297 298 } 298 299 299 300 /** 301 + * @default decorator for setting default values on scalars and unions 302 + * The value can be a literal (string, number, boolean) or a model reference for tokens 303 + */ 304 + export function $default(context: DecoratorContext, target: Type, value: any) { 305 + // Just store the raw value - let the emitter handle unwrapping and validation 306 + context.program.stateMap(defaultKey).set(target, value); 307 + } 308 + 309 + export function getDefault( 310 + program: Program, 311 + target: Type, 312 + ): any | undefined { 313 + return program.stateMap(defaultKey).get(target); 314 + } 315 + 316 + /** 300 317 * @external decorator for marking a namespace as external 301 318 * External namespaces are skipped during emission and don't produce JSON files 302 319 */ ··· 310 327 }); 311 328 return; 312 329 } 330 + 313 331 context.program.stateSet(externalKey).add(target); 314 332 } 315 333
+307 -24
packages/emitter/src/emitter.ts
··· 48 48 LexCidLink, 49 49 LexRefVariant, 50 50 LexToken, 51 + LexBoolean, 52 + LexInteger, 53 + LexString, 51 54 } from "./types.js"; 52 55 53 56 import { ··· 68 71 getMaxBytes, 69 72 getMinBytes, 70 73 isExternal, 74 + getDefault, 71 75 } from "./decorators.js"; 72 76 73 77 export interface EmitterOptions { ··· 98 102 private options: EmitterOptions, 99 103 ) {} 100 104 105 + /** 106 + * Process the raw default value from the decorator, unwrapping TypeSpec value objects 107 + * and returning either a primitive (string, number, boolean) or a Type (for model references) 108 + */ 109 + private processDefaultValue(rawValue: any): string | number | boolean | Type | undefined { 110 + if (rawValue === undefined) return undefined; 111 + 112 + // TypeSpec may wrap values - check if this is a value object first 113 + if (rawValue && typeof rawValue === 'object' && rawValue.valueKind) { 114 + if (rawValue.valueKind === "StringValue") { 115 + return rawValue.value; 116 + } else if (rawValue.valueKind === "NumericValue" || rawValue.valueKind === "NumberValue") { 117 + return rawValue.value; 118 + } else if (rawValue.valueKind === "BooleanValue") { 119 + return rawValue.value; 120 + } 121 + return undefined; // Unsupported valueKind 122 + } 123 + 124 + // Check if it's a Type object (Model, String, Number, Boolean literals) 125 + if (rawValue && typeof rawValue === 'object' && rawValue.kind) { 126 + if (rawValue.kind === "String") { 127 + return (rawValue as StringLiteral).value; 128 + } else if (rawValue.kind === "Number") { 129 + return (rawValue as NumericLiteral).value; 130 + } else if (rawValue.kind === "Boolean") { 131 + return (rawValue as BooleanLiteral).value; 132 + } else if (rawValue.kind === "Model") { 133 + // Return the model itself for token references 134 + return rawValue as Model; 135 + } 136 + return undefined; // Unsupported kind 137 + } 138 + 139 + // Direct primitive value 140 + if (typeof rawValue === 'string' || typeof rawValue === 'number' || typeof rawValue === 'boolean') { 141 + return rawValue; 142 + } 143 + 144 + return undefined; 145 + } 146 + 101 147 async emit() { 102 148 const globalNs = this.program.getGlobalNamespaceType(); 103 149 ··· 124 170 125 171 // Skip external namespaces - they don't emit JSON files 126 172 if (isExternal(this.program, ns)) { 173 + // Validate that all models in external namespaces are empty (stub-only) 174 + for (const [_, model] of ns.models) { 175 + if (model.properties && model.properties.size > 0) { 176 + this.program.reportDiagnostic({ 177 + code: "external-model-not-empty", 178 + severity: "error", 179 + message: `Models in @external namespaces must be empty stubs. Model '${model.name}' in namespace '${fullName}' has properties.`, 180 + target: model, 181 + }); 182 + } 183 + } 127 184 return; 128 185 } 129 186 ··· 345 402 } 346 403 347 404 private addScalarToDefs(lexicon: LexiconDoc, scalar: Scalar) { 405 + // Only skip if the scalar itself is in TypeSpec namespace (built-in scalars) 348 406 if (scalar.namespace?.name === "TypeSpec") return; 349 - if (scalar.baseScalar?.namespace?.name === "TypeSpec") return; 350 407 351 408 // Skip @inline scalars - they should be inlined, not defined separately 352 409 if (isInline(this.program, scalar)) { ··· 357 414 const scalarDef = this.scalarToLexiconPrimitive(scalar, undefined); 358 415 if (scalarDef) { 359 416 const description = getDoc(this.program, scalar); 360 - lexicon.defs[defName] = { ...scalarDef, description } as LexUserType; 417 + 418 + // Apply @default decorator if present 419 + const rawDefault = getDefault(this.program, scalar); 420 + const defaultValue = this.processDefaultValue(rawDefault); 421 + let defWithDefault: LexObjectProperty = { ...scalarDef }; 422 + 423 + if (defaultValue !== undefined) { 424 + // Check if it's a Type (model reference for tokens) 425 + if (typeof defaultValue === 'object' && 'kind' in defaultValue) { 426 + // For model references, we need to resolve to NSID 427 + // This shouldn't happen for scalars, only unions support token refs 428 + this.program.reportDiagnostic({ 429 + code: "invalid-default-on-scalar", 430 + severity: "error", 431 + message: "@default on scalars must be a literal value (string, number, or boolean), not a model reference", 432 + target: scalar, 433 + }); 434 + } else { 435 + // Validate that the default value matches the type 436 + this.assertValidValueForType(scalarDef.type, defaultValue, scalar); 437 + // Type-safe narrowing based on both the type discriminator and value type 438 + if (scalarDef.type === "boolean" && typeof defaultValue === "boolean") { 439 + (defWithDefault as LexBoolean).default = defaultValue; 440 + } else if (scalarDef.type === "integer" && typeof defaultValue === "number") { 441 + (defWithDefault as LexInteger).default = defaultValue; 442 + } else if (scalarDef.type === "string" && typeof defaultValue === "string") { 443 + (defWithDefault as LexString).default = defaultValue; 444 + } 445 + } 446 + } 447 + 448 + // Apply integer constraints for standalone scalar defs 449 + if (scalarDef.type === "integer") { 450 + const minValue = getMinValue(this.program, scalar); 451 + if (minValue !== undefined) { 452 + (defWithDefault as LexInteger).minimum = minValue; 453 + } 454 + const maxValue = getMaxValue(this.program, scalar); 455 + if (maxValue !== undefined) { 456 + (defWithDefault as LexInteger).maximum = maxValue; 457 + } 458 + } 459 + 460 + lexicon.defs[defName] = { ...defWithDefault, description } as LexUserType; 361 461 } 362 462 } 363 463 ··· 380 480 if (unionDef.type === "string" && (unionDef.knownValues || unionDef.enum)) { 381 481 const defName = name.charAt(0).toLowerCase() + name.slice(1); 382 482 const description = getDoc(this.program, union); 383 - lexicon.defs[defName] = { ...unionDef, description }; 483 + 484 + // Apply @default decorator if present 485 + const rawDefault = getDefault(this.program, union); 486 + const defaultValue = this.processDefaultValue(rawDefault); 487 + let defWithDefault: LexString = { ...unionDef as LexString }; 488 + 489 + if (defaultValue !== undefined) { 490 + // Check if it's a Type (model reference for tokens) 491 + if (typeof defaultValue === 'object' && 'kind' in defaultValue) { 492 + // Resolve the model reference to its NSID 493 + const tokenModel = defaultValue as Model; 494 + const tokenRef = this.getModelReference(tokenModel, true); // fullyQualified=true 495 + if (tokenRef) { 496 + defWithDefault = { ...defWithDefault, default: tokenRef }; 497 + } else { 498 + this.program.reportDiagnostic({ 499 + code: "invalid-default-token", 500 + severity: "error", 501 + message: "@default value must be a valid token model reference", 502 + target: union, 503 + }); 504 + } 505 + } else { 506 + // Literal value - validate it matches the union type 507 + if (typeof defaultValue !== "string") { 508 + this.program.reportDiagnostic({ 509 + code: "invalid-default-value-type", 510 + severity: "error", 511 + message: `Default value type mismatch: expected string, got ${typeof defaultValue}`, 512 + target: union, 513 + }); 514 + } else { 515 + defWithDefault = { ...defWithDefault, default: defaultValue }; 516 + } 517 + } 518 + } 519 + 520 + lexicon.defs[defName] = { ...defWithDefault, description }; 384 521 } else if (unionDef.type === "union") { 385 522 this.program.reportDiagnostic({ 386 523 code: "union-refs-not-allowed-as-def", ··· 390 527 `Use @inline to inline them at usage sites, use @token models for known values, or use string literals.`, 391 528 target: union, 392 529 }); 530 + } else if (unionDef.type === "integer" && (unionDef as LexInteger).enum) { 531 + // Integer enums can also be defs 532 + const defName = name.charAt(0).toLowerCase() + name.slice(1); 533 + const description = getDoc(this.program, union); 534 + 535 + // Apply @default decorator if present 536 + const rawDefault = getDefault(this.program, union); 537 + const defaultValue = this.processDefaultValue(rawDefault); 538 + let defWithDefault: LexInteger = { ...unionDef as LexInteger }; 539 + 540 + if (defaultValue !== undefined) { 541 + if (typeof defaultValue === "number") { 542 + defWithDefault = { ...defWithDefault, default: defaultValue }; 543 + } else { 544 + this.program.reportDiagnostic({ 545 + code: "invalid-default-value-type", 546 + severity: "error", 547 + message: `Default value type mismatch: expected integer, got ${typeof defaultValue}`, 548 + target: union, 549 + }); 550 + } 551 + } 552 + 553 + lexicon.defs[defName] = { ...defWithDefault, description }; 393 554 } 394 555 } 395 556 ··· 490 651 isClosed(this.program, unionType) 491 652 ) { 492 653 const propDesc = prop ? getDoc(this.program, prop) : undefined; 493 - const defaultValue = prop?.defaultValue 494 - ? serializeValueAsJson(this.program, prop.defaultValue, prop) 495 - : undefined; 654 + 655 + // Check for default value: property default takes precedence, then union's @default 656 + let defaultValue: string | number | boolean | undefined; 657 + if (prop?.defaultValue !== undefined) { 658 + defaultValue = serializeValueAsJson(this.program, prop.defaultValue, prop) as string | number | boolean; 659 + } else { 660 + // If no property default, check union's @default decorator 661 + const rawUnionDefault = getDefault(this.program, unionType); 662 + const unionDefault = this.processDefaultValue(rawUnionDefault); 663 + if (unionDefault !== undefined && typeof unionDefault === 'number') { 664 + defaultValue = unionDefault; 665 + } 666 + } 667 + 496 668 return { 497 669 type: "integer", 498 670 enum: variants.numericLiterals, ··· 515 687 ) { 516 688 const isClosedUnion = isClosed(this.program, unionType); 517 689 const propDesc = prop ? getDoc(this.program, prop) : undefined; 518 - const defaultValue = prop?.defaultValue 519 - ? serializeValueAsJson(this.program, prop.defaultValue, prop) 520 - : undefined; 690 + 691 + // Check for default value: property default takes precedence, then union's @default 692 + let defaultValue: string | number | boolean | undefined; 693 + if (prop?.defaultValue !== undefined) { 694 + defaultValue = serializeValueAsJson(this.program, prop.defaultValue, prop) as string | number | boolean; 695 + } else { 696 + // If no property default, check union's @default decorator 697 + const rawUnionDefault = getDefault(this.program, unionType); 698 + const unionDefault = this.processDefaultValue(rawUnionDefault); 699 + 700 + if (unionDefault !== undefined) { 701 + // Check if it's a Type (model reference for tokens) 702 + if (typeof unionDefault === 'object' && 'kind' in unionDefault && unionDefault.kind === 'Model') { 703 + // Resolve the model reference to its NSID 704 + const tokenModel = unionDefault as Model; 705 + const tokenRef = this.getModelReference(tokenModel, true); // fullyQualified=true 706 + if (tokenRef) { 707 + defaultValue = tokenRef; 708 + } 709 + } else if (typeof unionDefault === 'string') { 710 + defaultValue = unionDefault; 711 + } 712 + } 713 + } 714 + 521 715 const maxLength = getMaxLength(this.program, unionType); 522 716 const minLength = getMinLength(this.program, unionType); 523 717 const maxGraphemes = getMaxGraphemes(this.program, unionType); ··· 544 738 545 739 // Model reference union (including empty union with unknown) 546 740 if (variants.unionRefs.length > 0 || variants.hasUnknown) { 547 - if (variants.stringLiterals.length > 0 || variants.knownValueRefs.length > 0) { 741 + if ( 742 + variants.stringLiterals.length > 0 || 743 + variants.knownValueRefs.length > 0 744 + ) { 548 745 this.program.reportDiagnostic({ 549 746 code: "union-mixed-refs-literals", 550 747 severity: "error", ··· 1144 1341 prop?: ModelProperty, 1145 1342 propDesc?: string, 1146 1343 ): LexObjectProperty | null { 1344 + // Check if this scalar should be referenced instead of inlined 1345 + const scalarRef = this.getScalarReference(scalar); 1346 + if (scalarRef) { 1347 + // Check if property has a default value that would conflict with the scalar's @default 1348 + if (prop?.defaultValue !== undefined) { 1349 + const scalarDefaultRaw = getDefault(this.program, scalar); 1350 + const scalarDefault = this.processDefaultValue(scalarDefaultRaw); 1351 + const propDefault = serializeValueAsJson(this.program, prop.defaultValue, prop); 1352 + 1353 + // If the scalar has a different default, or if the property has a default but the scalar doesn't, error 1354 + if (scalarDefault !== propDefault) { 1355 + this.program.reportDiagnostic({ 1356 + code: "conflicting-defaults", 1357 + severity: "error", 1358 + message: scalarDefault !== undefined 1359 + ? `Property default value conflicts with scalar's @default decorator. The scalar "${scalar.name}" has @default(${JSON.stringify(scalarDefault)}) but property has default value ${JSON.stringify(propDefault)}. Either remove the property default, mark the scalar @inline, or make the defaults match.` 1360 + : `Property has a default value but the referenced scalar "${scalar.name}" does not. Either add @default to the scalar, mark it @inline to allow property-level defaults, or remove the property default.`, 1361 + target: prop, 1362 + }); 1363 + } 1364 + } 1365 + 1366 + return { type: "ref" as const, ref: scalarRef, description: propDesc }; 1367 + } 1368 + 1369 + // Inline the scalar 1147 1370 const primitive = this.scalarToLexiconPrimitive(scalar, prop); 1148 1371 if (!primitive) return null; 1149 1372 ··· 1232 1455 if (!isDefining) { 1233 1456 const unionRef = this.getUnionReference(unionType); 1234 1457 if (unionRef) { 1458 + // Check if property has a default value that would conflict with the union's @default 1459 + if (prop?.defaultValue !== undefined) { 1460 + const unionDefaultRaw = getDefault(this.program, unionType); 1461 + const unionDefault = this.processDefaultValue(unionDefaultRaw); 1462 + const propDefault = serializeValueAsJson(this.program, prop.defaultValue, prop); 1463 + 1464 + // For union defaults that are model references, we need to resolve them for comparison 1465 + let resolvedUnionDefault: string | number | boolean | undefined; 1466 + if (unionDefault && typeof unionDefault === 'object' && 'kind' in unionDefault && unionDefault.kind === 'Model') { 1467 + const ref = this.getModelReference(unionDefault as Model, true); 1468 + resolvedUnionDefault = ref || undefined; 1469 + } else { 1470 + resolvedUnionDefault = unionDefault as string | number | boolean; 1471 + } 1472 + 1473 + // If the union has a different default, or if the property has a default but the union doesn't, error 1474 + if (resolvedUnionDefault !== propDefault) { 1475 + this.program.reportDiagnostic({ 1476 + code: "conflicting-defaults", 1477 + severity: "error", 1478 + message: unionDefault !== undefined 1479 + ? `Property default value conflicts with union's @default decorator. The union "${unionType.name}" has @default(${JSON.stringify(resolvedUnionDefault)}) but property has default value ${JSON.stringify(propDefault)}. Either remove the property default, mark the union @inline, or make the defaults match.` 1480 + : `Property has a default value but the referenced union "${unionType.name}" does not. Either add @default to the union, mark it @inline to allow property-level defaults, or remove the property default.`, 1481 + target: prop, 1482 + }); 1483 + } 1484 + } 1485 + 1235 1486 return { type: "ref" as const, ref: unionRef, description: propDesc }; 1236 1487 } 1237 1488 } ··· 1257 1508 // Check if this scalar (or its base) is bytes type 1258 1509 if (this.isScalarOfType(scalar, "bytes")) { 1259 1510 const byteDef: LexBytes = { type: "bytes" }; 1260 - const target = prop || scalar; 1261 1511 1262 - const minLength = getMinBytes(this.program, target); 1512 + // Check scalar first for its own constraints, then property overrides 1513 + const minLength = getMinBytes(this.program, scalar) ?? (prop ? getMinBytes(this.program, prop) : undefined); 1263 1514 if (minLength !== undefined) { 1264 1515 byteDef.minLength = minLength; 1265 1516 } 1266 1517 1267 - const maxLength = getMaxBytes(this.program, target); 1518 + const maxLength = getMaxBytes(this.program, scalar) ?? (prop ? getMaxBytes(this.program, prop) : undefined); 1268 1519 if (maxLength !== undefined) { 1269 1520 byteDef.maxLength = maxLength; 1270 1521 } ··· 1296 1547 1297 1548 // Apply string constraints 1298 1549 if (primitive.type === "string") { 1299 - const target = prop || scalar; 1300 - const maxLength = getMaxLength(this.program, target); 1550 + // Check scalar first for its own constraints, then property overrides 1551 + const maxLength = getMaxLength(this.program, scalar) ?? (prop ? getMaxLength(this.program, prop) : undefined); 1301 1552 if (maxLength !== undefined) { 1302 1553 primitive.maxLength = maxLength; 1303 1554 } 1304 - const minLength = getMinLength(this.program, target); 1555 + const minLength = getMinLength(this.program, scalar) ?? (prop ? getMinLength(this.program, prop) : undefined); 1305 1556 if (minLength !== undefined) { 1306 1557 primitive.minLength = minLength; 1307 1558 } 1308 - const maxGraphemes = getMaxGraphemes(this.program, target); 1559 + const maxGraphemes = getMaxGraphemes(this.program, scalar) ?? (prop ? getMaxGraphemes(this.program, prop) : undefined); 1309 1560 if (maxGraphemes !== undefined) { 1310 1561 primitive.maxGraphemes = maxGraphemes; 1311 1562 } 1312 - const minGraphemes = getMinGraphemes(this.program, target); 1563 + const minGraphemes = getMinGraphemes(this.program, scalar) ?? (prop ? getMinGraphemes(this.program, prop) : undefined); 1313 1564 if (minGraphemes !== undefined) { 1314 1565 primitive.minGraphemes = minGraphemes; 1315 1566 } 1316 1567 } 1317 1568 1318 1569 // Apply numeric constraints 1319 - if (prop && primitive.type === "integer") { 1320 - const minValue = getMinValue(this.program, prop); 1570 + if (primitive.type === "integer") { 1571 + // Check scalar first for its own constraints, then property overrides 1572 + const minValue = getMinValue(this.program, scalar) ?? (prop ? getMinValue(this.program, prop) : undefined); 1321 1573 if (minValue !== undefined) { 1322 1574 primitive.minimum = minValue; 1323 1575 } 1324 - const maxValue = getMaxValue(this.program, prop); 1576 + const maxValue = getMaxValue(this.program, scalar) ?? (prop ? getMaxValue(this.program, prop) : undefined); 1325 1577 if (maxValue !== undefined) { 1326 1578 primitive.maximum = maxValue; 1327 1579 } ··· 1417 1669 private assertValidValueForType( 1418 1670 primitiveType: string, 1419 1671 value: unknown, 1420 - prop: ModelProperty, 1672 + target: ModelProperty | Scalar | Union, 1421 1673 ): void { 1422 1674 const valid = 1423 1675 (primitiveType === "boolean" && typeof value === "boolean") || ··· 1428 1680 code: "invalid-default-value-type", 1429 1681 severity: "error", 1430 1682 message: `Default value type mismatch: expected ${primitiveType}, got ${typeof value}`, 1431 - target: prop, 1683 + target: target, 1432 1684 }); 1433 1685 } 1434 1686 } ··· 1483 1735 model: Model, 1484 1736 fullyQualified = false, 1485 1737 ): string | null { 1486 - return this.getReference(model, model.name, model.namespace, fullyQualified); 1738 + return this.getReference( 1739 + model, 1740 + model.name, 1741 + model.namespace, 1742 + fullyQualified, 1743 + ); 1487 1744 } 1488 1745 1489 1746 private getUnionReference(union: Union): string | null { 1490 1747 return this.getReference(union, union.name, union.namespace); 1748 + } 1749 + 1750 + private getScalarReference(scalar: Scalar): string | null { 1751 + // Built-in TypeSpec scalars (string, integer, boolean themselves) should not be referenced 1752 + if (scalar.namespace?.name === "TypeSpec") return null; 1753 + 1754 + // @inline scalars should be inlined, not referenced 1755 + if (isInline(this.program, scalar)) return null; 1756 + 1757 + // Scalars without names or namespace can't be referenced 1758 + if (!scalar.name || !scalar.namespace) return null; 1759 + 1760 + const defName = scalar.name.charAt(0).toLowerCase() + scalar.name.slice(1); 1761 + const namespaceName = getNamespaceFullName(scalar.namespace); 1762 + if (!namespaceName) return null; 1763 + 1764 + // Local reference (same namespace) - use short ref 1765 + if ( 1766 + this.currentLexiconId === namespaceName || 1767 + this.currentLexiconId === `${namespaceName}.defs` 1768 + ) { 1769 + return `#${defName}`; 1770 + } 1771 + 1772 + // Cross-namespace reference 1773 + return `${namespaceName}#${defName}`; 1491 1774 } 1492 1775 1493 1776 private modelToLexiconArray(
+2
packages/emitter/src/tsp-index.ts
··· 15 15 $maxBytes, 16 16 $minBytes, 17 17 $external, 18 + $default, 18 19 } from "./decorators.js"; 19 20 20 21 /** @internal */ ··· 36 37 maxBytes: $maxBytes, 37 38 minBytes: $minBytes, 38 39 external: $external, 40 + default: $default, 39 41 }, 40 42 };
+48
packages/emitter/test/integration/atproto/input/app/bsky/actor/defs.tsp
··· 232 232 prioritizeFollowedUsers?: boolean; 233 233 } 234 234 235 + @inline 235 236 @maxLength(640) 236 237 @maxGraphemes(64) 237 238 scalar InterestTag extends string; ··· 292 293 @required did: did; 293 294 } 294 295 296 + @inline 295 297 @maxLength(100) 296 298 scalar NudgeToken extends string; 297 299 ··· 372 374 isActive?: boolean; 373 375 } 374 376 } 377 + 378 + // --- Externals --- 379 + 380 + @external 381 + namespace com.atproto.label.defs { 382 + model Label { } 383 + } 384 + 385 + @external 386 + namespace app.bsky.graph.defs { 387 + model StarterPackViewBasic { } 388 + model ListViewBasic { } 389 + } 390 + 391 + @external 392 + namespace com.atproto.repo.strongRef { 393 + model Main { } 394 + } 395 + 396 + @external 397 + namespace app.bsky.notification.defs { 398 + model ActivitySubscription { } 399 + } 400 + 401 + @external 402 + namespace app.bsky.feed.threadgate { 403 + model MentionRule { } 404 + model FollowerRule { } 405 + model FollowingRule { } 406 + model ListRule { } 407 + } 408 + 409 + @external 410 + namespace app.bsky.feed.postgate { 411 + model DisableRule { } 412 + } 413 + 414 + @external 415 + namespace app.bsky.actor.status { 416 + @token model Live { } 417 + } 418 + 419 + @external 420 + namespace app.bsky.embed.external { 421 + model View { } 422 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/getPreferences.tsp
··· 7 7 @required preferences: app.bsky.actor.defs.Preferences; 8 8 }; 9 9 } 10 + 11 + // --- Externals --- 12 + 13 + @external 14 + namespace app.bsky.actor.defs { 15 + model Preferences { } 16 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/getProfile.tsp
··· 8 8 @required actor: atIdentifier 9 9 ): app.bsky.actor.defs.ProfileViewDetailed; 10 10 } 11 + 12 + // --- Externals --- 13 + 14 + @external 15 + namespace app.bsky.actor.defs { 16 + model ProfileViewDetailed { } 17 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/getProfiles.tsp
··· 10 10 @required profiles: app.bsky.actor.defs.ProfileViewDetailed[]; 11 11 }; 12 12 } 13 + 14 + // --- Externals --- 15 + 16 + @external 17 + namespace app.bsky.actor.defs { 18 + model ProfileViewDetailed { } 19 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/getSuggestions.tsp
··· 19 19 recId?: int32; 20 20 }; 21 21 } 22 + 23 + // --- Externals --- 24 + 25 + @external 26 + namespace app.bsky.actor.defs { 27 + model ProfileView { } 28 + }
+12
packages/emitter/test/integration/atproto/input/app/bsky/actor/profile.tsp
··· 34 34 createdAt?: datetime; 35 35 } 36 36 } 37 + 38 + // --- Externals --- 39 + 40 + @external 41 + namespace com.atproto.label.defs { 42 + model SelfLabels { } 43 + } 44 + 45 + @external 46 + namespace com.atproto.repo.strongRef { 47 + model Main { } 48 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/putPreferences.tsp
··· 7 7 @required preferences: app.bsky.actor.defs.Preferences; 8 8 }): void; 9 9 } 10 + 11 + // --- Externals --- 12 + 13 + @external 14 + namespace app.bsky.actor.defs { 15 + model Preferences { } 16 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/searchActors.tsp
··· 20 20 @required actors: app.bsky.actor.defs.ProfileView[]; 21 21 }; 22 22 } 23 + 24 + // --- Externals --- 25 + 26 + @external 27 + namespace app.bsky.actor.defs { 28 + model ProfileView { } 29 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/searchActorsTypeahead.tsp
··· 17 17 @required actors: app.bsky.actor.defs.ProfileViewBasic[]; 18 18 }; 19 19 } 20 + 21 + // --- Externals --- 22 + 23 + @external 24 + namespace app.bsky.actor.defs { 25 + model ProfileViewBasic { } 26 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/status.tsp
··· 22 22 @token 23 23 model Live {} 24 24 } 25 + 26 + // --- Externals --- 27 + 28 + @external 29 + namespace app.bsky.embed.external { 30 + model Main { } 31 + }
+14
packages/emitter/test/integration/atproto/input/app/bsky/bookmark/defs.tsp
··· 24 24 ); 25 25 } 26 26 } 27 + 28 + // --- Externals --- 29 + 30 + @external 31 + namespace com.atproto.repo.strongRef { 32 + model Main { } 33 + } 34 + 35 + @external 36 + namespace app.bsky.feed.defs { 37 + model BlockedPost { } 38 + model NotFoundPost { } 39 + model PostView { } 40 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/bookmark/getBookmarks.tsp
··· 14 14 @required bookmarks: app.bsky.bookmark.defs.BookmarkView[]; 15 15 }; 16 16 } 17 + 18 + // --- Externals --- 19 + 20 + @external 21 + namespace app.bsky.bookmark.defs { 22 + model BookmarkView { } 23 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/embed/images.tsp
··· 40 40 aspectRatio?: app.bsky.embed.defs.AspectRatio; 41 41 } 42 42 } 43 + 44 + // --- Externals --- 45 + 46 + @external 47 + namespace app.bsky.embed.defs { 48 + model AspectRatio { } 49 + }
+54
packages/emitter/test/integration/atproto/input/app/bsky/embed/record.tsp
··· 74 74 detached: boolean = true; 75 75 } 76 76 } 77 + 78 + // --- Externals --- 79 + 80 + @external 81 + namespace com.atproto.repo.strongRef { 82 + model Main { } 83 + } 84 + 85 + @external 86 + namespace app.bsky.feed.defs { 87 + model GeneratorView { } 88 + model BlockedAuthor { } 89 + } 90 + 91 + @external 92 + namespace app.bsky.graph.defs { 93 + model ListView { } 94 + model StarterPackViewBasic { } 95 + } 96 + 97 + @external 98 + namespace app.bsky.labeler.defs { 99 + model LabelerView { } 100 + } 101 + 102 + @external 103 + namespace app.bsky.actor.defs { 104 + model ProfileViewBasic { } 105 + } 106 + 107 + @external 108 + namespace com.atproto.label.defs { 109 + model Label { } 110 + } 111 + 112 + @external 113 + namespace app.bsky.embed.images { 114 + model View { } 115 + } 116 + 117 + @external 118 + namespace app.bsky.embed.video { 119 + model View { } 120 + } 121 + 122 + @external 123 + namespace app.bsky.embed.external { 124 + model View { } 125 + } 126 + 127 + @external 128 + namespace app.bsky.embed.recordWithMedia { 129 + model View { } 130 + }
+26
packages/emitter/test/integration/atproto/input/app/bsky/embed/recordWithMedia.tsp
··· 26 26 ); 27 27 } 28 28 } 29 + 30 + // --- Externals --- 31 + 32 + @external 33 + namespace app.bsky.embed.`record` { 34 + model Main { } 35 + model View { } 36 + } 37 + 38 + @external 39 + namespace app.bsky.embed.images { 40 + model Main { } 41 + model View { } 42 + } 43 + 44 + @external 45 + namespace app.bsky.embed.video { 46 + model Main { } 47 + model View { } 48 + } 49 + 50 + @external 51 + namespace app.bsky.embed.external { 52 + model Main { } 53 + model View { } 54 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/embed/video.tsp
··· 36 36 aspectRatio?: app.bsky.embed.defs.AspectRatio; 37 37 } 38 38 } 39 + 40 + // --- Externals --- 41 + 42 + @external 43 + namespace app.bsky.embed.defs { 44 + model AspectRatio { } 45 + }
+49
packages/emitter/test/integration/atproto/input/app/bsky/feed/defs.tsp
··· 246 246 @token 247 247 model InteractionShare {} 248 248 } 249 + 250 + // --- Externals --- 251 + 252 + @external 253 + namespace app.bsky.actor.defs { 254 + model ProfileViewBasic { } 255 + model ViewerState { } 256 + model ProfileView { } 257 + } 258 + 259 + @external 260 + namespace app.bsky.embed.images { 261 + model View { } 262 + } 263 + 264 + @external 265 + namespace app.bsky.embed.video { 266 + model View { } 267 + } 268 + 269 + @external 270 + namespace app.bsky.embed.external { 271 + model View { } 272 + } 273 + 274 + @external 275 + namespace app.bsky.embed.`record` { 276 + model View { } 277 + } 278 + 279 + @external 280 + namespace app.bsky.embed.recordWithMedia { 281 + model View { } 282 + } 283 + 284 + @external 285 + namespace com.atproto.label.defs { 286 + model Label { } 287 + } 288 + 289 + @external 290 + namespace app.bsky.richtext.facet { 291 + model Main { } 292 + } 293 + 294 + @external 295 + namespace app.bsky.graph.defs { 296 + model ListViewBasic { } 297 + }
+18
packages/emitter/test/integration/atproto/input/app/bsky/feed/generator.tsp
··· 30 30 @required createdAt: datetime; 31 31 } 32 32 } 33 + 34 + // --- Externals --- 35 + 36 + @external 37 + namespace app.bsky.richtext.facet { 38 + model Main { } 39 + } 40 + 41 + @external 42 + namespace com.atproto.label.defs { 43 + model SelfLabels { } 44 + } 45 + 46 + @external 47 + namespace app.bsky.feed.defs { 48 + @token model ContentModeUnspecified { } 49 + @token model ContentModeVideo { } 50 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getActorFeeds.tsp
··· 16 16 @required feeds: app.bsky.feed.defs.GeneratorView[]; 17 17 }; 18 18 } 19 + 20 + // --- Externals --- 21 + 22 + @external 23 + namespace app.bsky.feed.defs { 24 + model GeneratorView { } 25 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getActorLikes.tsp
··· 20 20 @required feed: app.bsky.feed.defs.FeedViewPost[]; 21 21 }; 22 22 } 23 + 24 + // --- Externals --- 25 + 26 + @external 27 + namespace app.bsky.feed.defs { 28 + model FeedViewPost { } 29 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getAuthorFeed.tsp
··· 25 25 @required feed: app.bsky.feed.defs.FeedViewPost[]; 26 26 }; 27 27 } 28 + 29 + // --- Externals --- 30 + 31 + @external 32 + namespace app.bsky.feed.defs { 33 + model FeedViewPost { } 34 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeed.tsp
··· 19 19 @required feed: app.bsky.feed.defs.FeedViewPost[]; 20 20 }; 21 21 } 22 + 23 + // --- Externals --- 24 + 25 + @external 26 + namespace app.bsky.feed.defs { 27 + model FeedViewPost { } 28 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeedGenerator.tsp
··· 18 18 isValid: boolean; 19 19 }; 20 20 } 21 + 22 + // --- Externals --- 23 + 24 + @external 25 + namespace app.bsky.feed.defs { 26 + model GeneratorView { } 27 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeedGenerators.tsp
··· 9 9 @required feeds: app.bsky.feed.defs.GeneratorView[]; 10 10 }; 11 11 } 12 + 13 + // --- Externals --- 14 + 15 + @external 16 + namespace app.bsky.feed.defs { 17 + model GeneratorView { } 18 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeedSkeleton.tsp
··· 26 26 reqId?: string; 27 27 }; 28 28 } 29 + 30 + // --- Externals --- 31 + 32 + @external 33 + namespace app.bsky.feed.defs { 34 + model SkeletonFeedPost { } 35 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getLikes.tsp
··· 28 28 @required actor: app.bsky.actor.defs.ProfileView; 29 29 } 30 30 } 31 + 32 + // --- Externals --- 33 + 34 + @external 35 + namespace app.bsky.actor.defs { 36 + model ProfileView { } 37 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getListFeed.tsp
··· 20 20 @required feed: app.bsky.feed.defs.FeedViewPost[]; 21 21 }; 22 22 } 23 + 24 + // --- Externals --- 25 + 26 + @external 27 + namespace app.bsky.feed.defs { 28 + model FeedViewPost { } 29 + }
+10
packages/emitter/test/integration/atproto/input/app/bsky/feed/getPostThread.tsp
··· 26 26 threadgate?: app.bsky.feed.defs.ThreadgateView; 27 27 }; 28 28 } 29 + 30 + // --- Externals --- 31 + 32 + @external 33 + namespace app.bsky.feed.defs { 34 + model ThreadViewPost { } 35 + model NotFoundPost { } 36 + model BlockedPost { } 37 + model ThreadgateView { } 38 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getPosts.tsp
··· 11 11 @required posts: app.bsky.feed.defs.PostView[]; 12 12 }; 13 13 } 14 + 15 + // --- Externals --- 16 + 17 + @external 18 + namespace app.bsky.feed.defs { 19 + model PostView { } 20 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getQuotes.tsp
··· 22 22 @required posts: app.bsky.feed.defs.PostView[]; 23 23 }; 24 24 } 25 + 26 + // --- Externals --- 27 + 28 + @external 29 + namespace app.bsky.feed.defs { 30 + model PostView { } 31 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getRepostedBy.tsp
··· 22 22 @required repostedBy: app.bsky.actor.defs.ProfileView[]; 23 23 }; 24 24 } 25 + 26 + // --- Externals --- 27 + 28 + @external 29 + namespace app.bsky.actor.defs { 30 + model ProfileView { } 31 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getSuggestedFeeds.tsp
··· 14 14 @required feeds: app.bsky.feed.defs.GeneratorView[]; 15 15 }; 16 16 } 17 + 18 + // --- Externals --- 19 + 20 + @external 21 + namespace app.bsky.feed.defs { 22 + model GeneratorView { } 23 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getTimeline.tsp
··· 17 17 @required feed: app.bsky.feed.defs.FeedViewPost[]; 18 18 }; 19 19 } 20 + 21 + // --- Externals --- 22 + 23 + @external 24 + namespace app.bsky.feed.defs { 25 + model FeedViewPost { } 26 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/like.tsp
··· 13 13 via?: com.atproto.repo.strongRef.Main; 14 14 } 15 15 } 16 + 17 + // --- Externals --- 18 + 19 + @external 20 + namespace com.atproto.repo.strongRef { 21 + model Main { } 22 + }
+42
packages/emitter/test/integration/atproto/input/app/bsky/feed/post.tsp
··· 74 74 @maxGraphemes(64) 75 75 @maxLength(640) 76 76 scalar PostTag extends string; 77 + 78 + // --- Externals --- 79 + 80 + @external 81 + namespace app.bsky.richtext.facet { 82 + model Main { } 83 + } 84 + 85 + @external 86 + namespace app.bsky.embed.images { 87 + model Main { } 88 + } 89 + 90 + @external 91 + namespace app.bsky.embed.video { 92 + model Main { } 93 + } 94 + 95 + @external 96 + namespace app.bsky.embed.external { 97 + model Main { } 98 + } 99 + 100 + @external 101 + namespace app.bsky.embed.`record` { 102 + model Main { } 103 + } 104 + 105 + @external 106 + namespace app.bsky.embed.recordWithMedia { 107 + model Main { } 108 + } 109 + 110 + @external 111 + namespace com.atproto.label.defs { 112 + model SelfLabels { } 113 + } 114 + 115 + @external 116 + namespace com.atproto.repo.strongRef { 117 + model Main { } 118 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/repost.tsp
··· 9 9 via?: com.atproto.repo.strongRef.Main; 10 10 } 11 11 } 12 + 13 + // --- Externals --- 14 + 15 + @external 16 + namespace com.atproto.repo.strongRef { 17 + model Main { } 18 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/searchPosts.tsp
··· 56 56 @required posts: app.bsky.feed.defs.PostView[]; 57 57 }; 58 58 } 59 + 60 + // --- Externals --- 61 + 62 + @external 63 + namespace app.bsky.feed.defs { 64 + model PostView { } 65 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/sendInteractions.tsp
··· 7 7 @required interactions: app.bsky.feed.defs.Interaction[]; 8 8 }): {}; 9 9 } 10 + 11 + // --- Externals --- 12 + 13 + @external 14 + namespace app.bsky.feed.defs { 15 + model Interaction { } 16 + }
+23
packages/emitter/test/integration/atproto/input/app/bsky/graph/defs.tsp
··· 139 139 followedBy?: atUri; 140 140 } 141 141 } 142 + 143 + // --- Externals --- 144 + 145 + @external 146 + namespace com.atproto.label.defs { 147 + model Label { } 148 + } 149 + 150 + @external 151 + namespace app.bsky.actor.defs { 152 + model ProfileView { } 153 + model ProfileViewBasic { } 154 + } 155 + 156 + @external 157 + namespace app.bsky.richtext.facet { 158 + model Main { } 159 + } 160 + 161 + @external 162 + namespace app.bsky.feed.defs { 163 + model GeneratorView { } 164 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getActorStarterPacks.tsp
··· 16 16 @required starterPacks: app.bsky.graph.defs.StarterPackViewBasic[]; 17 17 }; 18 18 } 19 + 20 + // --- Externals --- 21 + 22 + @external 23 + namespace app.bsky.graph.defs { 24 + model StarterPackViewBasic { } 25 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getBlocks.tsp
··· 14 14 @required blocks: app.bsky.actor.defs.ProfileView[]; 15 15 }; 16 16 } 17 + 18 + // --- Externals --- 19 + 20 + @external 21 + namespace app.bsky.actor.defs { 22 + model ProfileView { } 23 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getFollowers.tsp
··· 17 17 @required followers: app.bsky.actor.defs.ProfileView[]; 18 18 }; 19 19 } 20 + 21 + // --- Externals --- 22 + 23 + @external 24 + namespace app.bsky.actor.defs { 25 + model ProfileView { } 26 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getFollows.tsp
··· 17 17 @required follows: app.bsky.actor.defs.ProfileView[]; 18 18 }; 19 19 } 20 + 21 + // --- Externals --- 22 + 23 + @external 24 + namespace app.bsky.actor.defs { 25 + model ProfileView { } 26 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getKnownFollowers.tsp
··· 17 17 @required followers: app.bsky.actor.defs.ProfileView[]; 18 18 }; 19 19 } 20 + 21 + // --- Externals --- 22 + 23 + @external 24 + namespace app.bsky.actor.defs { 25 + model ProfileView { } 26 + }
+8
packages/emitter/test/integration/atproto/input/app/bsky/graph/getList.tsp
··· 18 18 @required items: app.bsky.graph.defs.ListItemView[]; 19 19 }; 20 20 } 21 + 22 + // --- Externals --- 23 + 24 + @external 25 + namespace app.bsky.graph.defs { 26 + model ListView { } 27 + model ListItemView { } 28 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getListBlocks.tsp
··· 14 14 @required lists: app.bsky.graph.defs.ListView[]; 15 15 }; 16 16 } 17 + 18 + // --- Externals --- 19 + 20 + @external 21 + namespace app.bsky.graph.defs { 22 + model ListView { } 23 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getListMutes.tsp
··· 14 14 @required lists: app.bsky.graph.defs.ListView[]; 15 15 }; 16 16 } 17 + 18 + // --- Externals --- 19 + 20 + @external 21 + namespace app.bsky.graph.defs { 22 + model ListView { } 23 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getLists.tsp
··· 20 20 @required lists: app.bsky.graph.defs.ListView[]; 21 21 }; 22 22 } 23 + 24 + // --- Externals --- 25 + 26 + @external 27 + namespace app.bsky.graph.defs { 28 + model ListView { } 29 + }
+8
packages/emitter/test/integration/atproto/input/app/bsky/graph/getListsWithMembership.tsp
··· 27 27 listItem?: app.bsky.graph.defs.ListItemView; 28 28 } 29 29 } 30 + 31 + // --- Externals --- 32 + 33 + @external 34 + namespace app.bsky.graph.defs { 35 + model ListView { } 36 + model ListItemView { } 37 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getMutes.tsp
··· 14 14 @required mutes: app.bsky.actor.defs.ProfileView[]; 15 15 }; 16 16 } 17 + 18 + // --- Externals --- 19 + 20 + @external 21 + namespace app.bsky.actor.defs { 22 + model ProfileView { } 23 + }
+8
packages/emitter/test/integration/atproto/input/app/bsky/graph/getRelationships.tsp
··· 26 26 /** the primary actor at-identifier could not be resolved */ 27 27 model ActorNotFound {} 28 28 } 29 + 30 + // --- Externals --- 31 + 32 + @external 33 + namespace app.bsky.graph.defs { 34 + model Relationship { } 35 + model NotFoundActor { } 36 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getStarterPack.tsp
··· 11 11 @required starterPack: app.bsky.graph.defs.StarterPackView; 12 12 }; 13 13 } 14 + 15 + // --- Externals --- 16 + 17 + @external 18 + namespace app.bsky.graph.defs { 19 + model StarterPackView { } 20 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getStarterPacks.tsp
··· 11 11 @required starterPacks: app.bsky.graph.defs.StarterPackViewBasic[]; 12 12 }; 13 13 } 14 + 15 + // --- Externals --- 16 + 17 + @external 18 + namespace app.bsky.graph.defs { 19 + model StarterPackViewBasic { } 20 + }
+8
packages/emitter/test/integration/atproto/input/app/bsky/graph/getStarterPacksWithMembership.tsp
··· 24 24 listItem?: app.bsky.graph.defs.ListItemView; 25 25 } 26 26 } 27 + 28 + // --- Externals --- 29 + 30 + @external 31 + namespace app.bsky.graph.defs { 32 + model StarterPackView { } 33 + model ListItemView { } 34 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getSuggestedFollowsByActor.tsp
··· 16 16 recId?: int32; 17 17 }; 18 18 } 19 + 20 + // --- Externals --- 21 + 22 + @external 23 + namespace app.bsky.actor.defs { 24 + model ProfileView { } 25 + }
+17
packages/emitter/test/integration/atproto/input/app/bsky/graph/list.tsp
··· 27 27 @required createdAt: datetime; 28 28 } 29 29 } 30 + 31 + // --- Externals --- 32 + 33 + @external 34 + namespace app.bsky.graph.defs { 35 + model ListPurpose { } 36 + } 37 + 38 + @external 39 + namespace app.bsky.richtext.facet { 40 + model Main { } 41 + } 42 + 43 + @external 44 + namespace com.atproto.label.defs { 45 + model SelfLabels { } 46 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/searchStarterPacks.tsp
··· 18 18 @required starterPacks: app.bsky.graph.defs.StarterPackViewBasic[]; 19 19 }; 20 20 } 21 + 22 + // --- Externals --- 23 + 24 + @external 25 + namespace app.bsky.graph.defs { 26 + model StarterPackViewBasic { } 27 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/starterpack.tsp
··· 33 33 uri: atUri; 34 34 } 35 35 } 36 + 37 + // --- Externals --- 38 + 39 + @external 40 + namespace app.bsky.richtext.facet { 41 + model Main { } 42 + }
+20
packages/emitter/test/integration/atproto/input/app/bsky/labeler/defs.tsp
··· 54 54 labelValueDefinitions?: com.atproto.label.defs.LabelValueDefinition[]; 55 55 } 56 56 } 57 + 58 + // --- Externals --- 59 + 60 + @external 61 + namespace app.bsky.actor.defs { 62 + model ProfileView { } 63 + } 64 + 65 + @external 66 + namespace com.atproto.label.defs { 67 + model Label { } 68 + model LabelValue { } 69 + model LabelValueDefinition { } 70 + } 71 + 72 + @external 73 + namespace com.atproto.moderation.defs { 74 + model ReasonType { } 75 + model SubjectType { } 76 + }
+8
packages/emitter/test/integration/atproto/input/app/bsky/labeler/getServices.tsp
··· 15 15 )[]; 16 16 }; 17 17 } 18 + 19 + // --- Externals --- 20 + 21 + @external 22 + namespace app.bsky.labeler.defs { 23 + model LabelerView { } 24 + model LabelerViewDetailed { } 25 + }
+18
packages/emitter/test/integration/atproto/input/app/bsky/labeler/service.tsp
··· 20 20 subjectCollections?: nsid[]; 21 21 } 22 22 } 23 + 24 + // --- Externals --- 25 + 26 + @external 27 + namespace app.bsky.labeler.defs { 28 + model LabelerPolicies { } 29 + } 30 + 31 + @external 32 + namespace com.atproto.label.defs { 33 + model SelfLabels { } 34 + } 35 + 36 + @external 37 + namespace com.atproto.moderation.defs { 38 + model ReasonType { } 39 + model SubjectType { } 40 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/notification/getPreferences.tsp
··· 7 7 @required preferences: app.bsky.notification.defs.Preferences; 8 8 }; 9 9 } 10 + 11 + // --- Externals --- 12 + 13 + @external 14 + namespace app.bsky.notification.defs { 15 + model Preferences { } 16 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/notification/listActivitySubscriptions.tsp
··· 14 14 @required subscriptions: app.bsky.actor.defs.ProfileView[]; 15 15 }; 16 16 } 17 + 18 + // --- Externals --- 19 + 20 + @external 21 + namespace app.bsky.actor.defs { 22 + model ProfileView { } 23 + }
+12
packages/emitter/test/integration/atproto/input/app/bsky/notification/listNotifications.tsp
··· 40 40 labels?: com.atproto.label.defs.Label[]; 41 41 }; 42 42 } 43 + 44 + // --- Externals --- 45 + 46 + @external 47 + namespace app.bsky.actor.defs { 48 + model ProfileView { } 49 + } 50 + 51 + @external 52 + namespace com.atproto.label.defs { 53 + model Label { } 54 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/notification/putActivitySubscription.tsp
··· 13 13 activitySubscription?: app.bsky.notification.defs.ActivitySubscription; 14 14 }; 15 15 } 16 + 17 + // --- Externals --- 18 + 19 + @external 20 + namespace app.bsky.notification.defs { 21 + model ActivitySubscription { } 22 + }
+10
packages/emitter/test/integration/atproto/input/app/bsky/notification/putPreferencesV2.tsp
··· 21 21 @required preferences: app.bsky.notification.defs.Preferences; 22 22 }; 23 23 } 24 + 25 + // --- Externals --- 26 + 27 + @external 28 + namespace app.bsky.notification.defs { 29 + model ChatPreference { } 30 + model FilterablePreference { } 31 + model Preference { } 32 + model Preferences { } 33 + }
+13
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/defs.tsp
··· 114 114 completeUa?: string; 115 115 } 116 116 } 117 + 118 + // --- Externals --- 119 + 120 + @external 121 + namespace app.bsky.actor.defs { 122 + model ProfileViewBasic { } 123 + } 124 + 125 + @external 126 + namespace app.bsky.feed.defs { 127 + model PostView { } 128 + model BlockedAuthor { } 129 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getAgeAssuranceState.tsp
··· 5 5 @query 6 6 op main(): app.bsky.unspecced.defs.AgeAssuranceState; 7 7 } 8 + 9 + // --- Externals --- 10 + 11 + @external 12 + namespace app.bsky.unspecced.defs { 13 + model AgeAssuranceState { } 14 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.tsp
··· 11 11 @required starterPacks: app.bsky.graph.defs.StarterPackView[]; 12 12 }; 13 13 } 14 + 15 + // --- Externals --- 16 + 17 + @external 18 + namespace app.bsky.graph.defs { 19 + model StarterPackView { } 20 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getPopularFeedGenerators.tsp
··· 15 15 @required feeds: app.bsky.feed.defs.GeneratorView[]; 16 16 }; 17 17 } 18 + 19 + // --- Externals --- 20 + 21 + @external 22 + namespace app.bsky.feed.defs { 23 + model GeneratorView { } 24 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getPostThreadOtherV2.tsp
··· 26 26 @required value: (app.bsky.unspecced.defs.ThreadItemPost | unknown); 27 27 } 28 28 } 29 + 30 + // --- Externals --- 31 + 32 + @external 33 + namespace app.bsky.unspecced.defs { 34 + model ThreadItemPost { } 35 + }
+15
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getPostThreadV2.tsp
··· 55 55 ); 56 56 } 57 57 } 58 + 59 + // --- Externals --- 60 + 61 + @external 62 + namespace app.bsky.feed.defs { 63 + model ThreadgateView { } 64 + } 65 + 66 + @external 67 + namespace app.bsky.unspecced.defs { 68 + model ThreadItemPost { } 69 + model ThreadItemNoUnauthenticated { } 70 + model ThreadItemNotFound { } 71 + model ThreadItemBlocked { } 72 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getSuggestedFeeds.tsp
··· 11 11 @required feeds: app.bsky.feed.defs.GeneratorView[]; 12 12 }; 13 13 } 14 + 15 + // --- Externals --- 16 + 17 + @external 18 + namespace app.bsky.feed.defs { 19 + model GeneratorView { } 20 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getSuggestedStarterPacks.tsp
··· 11 11 @required starterPacks: app.bsky.graph.defs.StarterPackView[]; 12 12 }; 13 13 } 14 + 15 + // --- Externals --- 16 + 17 + @external 18 + namespace app.bsky.graph.defs { 19 + model StarterPackView { } 20 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getSuggestedUsers.tsp
··· 14 14 @required actors: app.bsky.actor.defs.ProfileView[]; 15 15 }; 16 16 } 17 + 18 + // --- Externals --- 19 + 20 + @external 21 + namespace app.bsky.actor.defs { 22 + model ProfileView { } 23 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getSuggestionsSkeleton.tsp
··· 26 26 recId?: integer; 27 27 }; 28 28 } 29 + 30 + // --- Externals --- 31 + 32 + @external 33 + namespace app.bsky.unspecced.defs { 34 + model SkeletonSearchActor { } 35 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getTrendingTopics.tsp
··· 15 15 @required suggested: app.bsky.unspecced.defs.TrendingTopic[]; 16 16 }; 17 17 } 18 + 19 + // --- Externals --- 20 + 21 + @external 22 + namespace app.bsky.unspecced.defs { 23 + model TrendingTopic { } 24 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getTrends.tsp
··· 11 11 @required trends: app.bsky.unspecced.defs.TrendView[]; 12 12 }; 13 13 } 14 + 15 + // --- Externals --- 16 + 17 + @external 18 + namespace app.bsky.unspecced.defs { 19 + model TrendView { } 20 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getTrendsSkeleton.tsp
··· 14 14 @required trends: app.bsky.unspecced.defs.SkeletonTrend[]; 15 15 }; 16 16 } 17 + 18 + // --- Externals --- 19 + 20 + @external 21 + namespace app.bsky.unspecced.defs { 22 + model SkeletonTrend { } 23 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/initAgeAssurance.tsp
··· 22 22 countryCode: string; 23 23 }): app.bsky.unspecced.defs.AgeAssuranceState; 24 24 } 25 + 26 + // --- Externals --- 27 + 28 + @external 29 + namespace app.bsky.unspecced.defs { 30 + model AgeAssuranceState { } 31 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/searchActorsSkeleton.tsp
··· 31 31 @required actors: app.bsky.unspecced.defs.SkeletonSearchActor[]; 32 32 }; 33 33 } 34 + 35 + // --- Externals --- 36 + 37 + @external 38 + namespace app.bsky.unspecced.defs { 39 + model SkeletonSearchActor { } 40 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/searchPostsSkeleton.tsp
··· 59 59 @required posts: app.bsky.unspecced.defs.SkeletonSearchPost[]; 60 60 }; 61 61 } 62 + 63 + // --- Externals --- 64 + 65 + @external 66 + namespace app.bsky.unspecced.defs { 67 + model SkeletonSearchPost { } 68 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/searchStarterPacksSkeleton.tsp
··· 28 28 @required starterPacks: app.bsky.unspecced.defs.SkeletonSearchStarterPack[]; 29 29 }; 30 30 } 31 + 32 + // --- Externals --- 33 + 34 + @external 35 + namespace app.bsky.unspecced.defs { 36 + model SkeletonSearchStarterPack { } 37 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/video/getJobStatus.tsp
··· 9 9 @required jobStatus: app.bsky.video.defs.JobStatus; 10 10 }; 11 11 } 12 + 13 + // --- Externals --- 14 + 15 + @external 16 + namespace app.bsky.video.defs { 17 + model JobStatus { } 18 + }
+7
packages/emitter/test/integration/atproto/input/app/bsky/video/uploadVideo.tsp
··· 10 10 @required jobStatus: app.bsky.video.defs.JobStatus; 11 11 }; 12 12 } 13 + 14 + // --- Externals --- 15 + 16 + @external 17 + namespace app.bsky.video.defs { 18 + model JobStatus { } 19 + }
+14
packages/emitter/test/integration/atproto/input/chat/bsky/actor/defs.tsp
··· 20 20 verification?: app.bsky.actor.defs.VerificationState; 21 21 } 22 22 } 23 + 24 + // --- Externals --- 25 + 26 + @external 27 + namespace app.bsky.actor.defs { 28 + model ProfileAssociated { } 29 + model ViewerState { } 30 + model VerificationState { } 31 + } 32 + 33 + @external 34 + namespace com.atproto.label.defs { 35 + model Label { } 36 + }
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/addReaction.tsp
··· 27 27 @required message: chat.bsky.convo.defs.MessageView; 28 28 }; 29 29 } 30 + 31 + // --- Externals --- 32 + 33 + @external 34 + namespace chat.bsky.convo.defs { 35 + model MessageView { } 36 + }
+18
packages/emitter/test/integration/atproto/input/chat/bsky/convo/defs.tsp
··· 139 139 @required reaction: ReactionView; 140 140 } 141 141 } 142 + 143 + // --- Externals --- 144 + 145 + @external 146 + namespace app.bsky.richtext.facet { 147 + model Main { } 148 + } 149 + 150 + @external 151 + namespace app.bsky.embed.`record` { 152 + model Main { } 153 + model View { } 154 + } 155 + 156 + @external 157 + namespace chat.bsky.actor.defs { 158 + model ProfileViewBasic { } 159 + }
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/deleteMessageForSelf.tsp
··· 7 7 @required messageId: string; 8 8 }): chat.bsky.convo.defs.DeletedMessageView; 9 9 } 10 + 11 + // --- Externals --- 12 + 13 + @external 14 + namespace chat.bsky.convo.defs { 15 + model DeletedMessageView { } 16 + }
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/getConvo.tsp
··· 6 6 @required convo: chat.bsky.convo.defs.ConvoView; 7 7 }; 8 8 } 9 + 10 + // --- Externals --- 11 + 12 + @external 13 + namespace chat.bsky.convo.defs { 14 + model ConvoView { } 15 + }
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/getConvoAvailability.tsp
··· 13 13 convo?: chat.bsky.convo.defs.ConvoView; 14 14 }; 15 15 } 16 + 17 + // --- Externals --- 18 + 19 + @external 20 + namespace chat.bsky.convo.defs { 21 + model ConvoView { } 22 + }
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/getConvoForMembers.tsp
··· 11 11 @required convo: chat.bsky.convo.defs.ConvoView; 12 12 }; 13 13 } 14 + 15 + // --- Externals --- 16 + 17 + @external 18 + namespace chat.bsky.convo.defs { 19 + model ConvoView { } 20 + }
+16
packages/emitter/test/integration/atproto/input/chat/bsky/convo/getLog.tsp
··· 21 21 )[]; 22 22 }; 23 23 } 24 + 25 + // --- Externals --- 26 + 27 + @external 28 + namespace chat.bsky.convo.defs { 29 + model LogBeginConvo { } 30 + model LogAcceptConvo { } 31 + model LogLeaveConvo { } 32 + model LogMuteConvo { } 33 + model LogUnmuteConvo { } 34 + model LogCreateMessage { } 35 + model LogDeleteMessage { } 36 + model LogReadMessage { } 37 + model LogAddReaction { } 38 + model LogRemoveReaction { } 39 + }
+8
packages/emitter/test/integration/atproto/input/chat/bsky/convo/getMessages.tsp
··· 21 21 )[]; 22 22 }; 23 23 } 24 + 25 + // --- Externals --- 26 + 27 + @external 28 + namespace chat.bsky.convo.defs { 29 + model MessageView { } 30 + model DeletedMessageView { } 31 + }
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/listConvos.tsp
··· 17 17 @required convos: chat.bsky.convo.defs.ConvoView[]; 18 18 }; 19 19 } 20 + 21 + // --- Externals --- 22 + 23 + @external 24 + namespace chat.bsky.convo.defs { 25 + model ConvoView { } 26 + }
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/muteConvo.tsp
··· 8 8 @required convo: chat.bsky.convo.defs.ConvoView; 9 9 }; 10 10 } 11 + 12 + // --- Externals --- 13 + 14 + @external 15 + namespace chat.bsky.convo.defs { 16 + model ConvoView { } 17 + }
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/removeReaction.tsp
··· 24 24 @required message: chat.bsky.convo.defs.MessageView; 25 25 }; 26 26 } 27 + 28 + // --- Externals --- 29 + 30 + @external 31 + namespace chat.bsky.convo.defs { 32 + model MessageView { } 33 + }
+8
packages/emitter/test/integration/atproto/input/chat/bsky/convo/sendMessage.tsp
··· 7 7 @required message: chat.bsky.convo.defs.MessageInput; 8 8 }): chat.bsky.convo.defs.MessageView; 9 9 } 10 + 11 + // --- Externals --- 12 + 13 + @external 14 + namespace chat.bsky.convo.defs { 15 + model MessageInput { } 16 + model MessageView { } 17 + }
+8
packages/emitter/test/integration/atproto/input/chat/bsky/convo/sendMessageBatch.tsp
··· 15 15 @required message: chat.bsky.convo.defs.MessageInput; 16 16 } 17 17 } 18 + 19 + // --- Externals --- 20 + 21 + @external 22 + namespace chat.bsky.convo.defs { 23 + model MessageView { } 24 + model MessageInput { } 25 + }
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/unmuteConvo.tsp
··· 8 8 @required convo: chat.bsky.convo.defs.ConvoView; 9 9 }; 10 10 } 11 + 12 + // --- Externals --- 13 + 14 + @external 15 + namespace chat.bsky.convo.defs { 16 + model ConvoView { } 17 + }
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/updateRead.tsp
··· 9 9 @required convo: chat.bsky.convo.defs.ConvoView; 10 10 }; 11 11 } 12 + 13 + // --- Externals --- 14 + 15 + @external 16 + namespace chat.bsky.convo.defs { 17 + model ConvoView { } 18 + }
+8
packages/emitter/test/integration/atproto/input/chat/bsky/moderation/getMessageContext.tsp
··· 20 20 )[]; 21 21 }; 22 22 } 23 + 24 + // --- Externals --- 25 + 26 + @external 27 + namespace chat.bsky.convo.defs { 28 + model MessageView { } 29 + model DeletedMessageView { } 30 + }
+8
packages/emitter/test/integration/atproto/input/com/atproto/admin/defs.tsp
··· 38 38 @required value: string; 39 39 } 40 40 } 41 + 42 + // --- Externals --- 43 + 44 + @external 45 + namespace com.atproto.server.defs { 46 + model InviteCode { } 47 + model InviteCodeUse { } 48 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/admin/getAccountInfo.tsp
··· 7 7 @required did: did 8 8 ): com.atproto.admin.defs.AccountView; 9 9 } 10 + 11 + // --- Externals --- 12 + 13 + @external 14 + namespace com.atproto.admin.defs { 15 + model AccountView { } 16 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/admin/getAccountInfos.tsp
··· 9 9 @required infos: com.atproto.admin.defs.AccountView[]; 10 10 }; 11 11 } 12 + 13 + // --- Externals --- 14 + 15 + @external 16 + namespace com.atproto.admin.defs { 17 + model AccountView { } 18 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/admin/getInviteCodes.tsp
··· 16 16 @required codes: com.atproto.server.defs.InviteCode[]; 17 17 }; 18 18 } 19 + 20 + // --- Externals --- 21 + 22 + @external 23 + namespace com.atproto.server.defs { 24 + model InviteCode { } 25 + }
+14
packages/emitter/test/integration/atproto/input/com/atproto/admin/getSubjectStatus.tsp
··· 20 20 deactivated?: com.atproto.admin.defs.StatusAttr; 21 21 }; 22 22 } 23 + 24 + // --- Externals --- 25 + 26 + @external 27 + namespace com.atproto.admin.defs { 28 + model RepoRef { } 29 + model RepoBlobRef { } 30 + model StatusAttr { } 31 + } 32 + 33 + @external 34 + namespace com.atproto.repo.strongRef { 35 + model Main { } 36 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/admin/searchAccounts.tsp
··· 15 15 @required accounts: com.atproto.admin.defs.AccountView[]; 16 16 }; 17 17 } 18 + 19 + // --- Externals --- 20 + 21 + @external 22 + namespace com.atproto.admin.defs { 23 + model AccountView { } 24 + }
+14
packages/emitter/test/integration/atproto/input/com/atproto/admin/updateSubjectStatus.tsp
··· 26 26 takedown?: com.atproto.admin.defs.StatusAttr; 27 27 }; 28 28 } 29 + 30 + // --- Externals --- 31 + 32 + @external 33 + namespace com.atproto.admin.defs { 34 + model RepoRef { } 35 + model RepoBlobRef { } 36 + model StatusAttr { } 37 + } 38 + 39 + @external 40 + namespace com.atproto.repo.strongRef { 41 + model Main { } 42 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/identity/refreshIdentity.tsp
··· 17 17 @required identifier: atIdentifier; 18 18 }): com.atproto.identity.defs.IdentityInfo; 19 19 } 20 + 21 + // --- Externals --- 22 + 23 + @external 24 + namespace com.atproto.identity.defs { 25 + model IdentityInfo { } 26 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/identity/resolveIdentity.tsp
··· 19 19 identifier: atIdentifier 20 20 ): com.atproto.identity.defs.IdentityInfo; 21 21 } 22 + 23 + // --- Externals --- 24 + 25 + @external 26 + namespace com.atproto.identity.defs { 27 + model IdentityInfo { } 28 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/label/queryLabels.tsp
··· 21 21 @required labels: com.atproto.label.defs.Label[]; 22 22 }; 23 23 } 24 + 25 + // --- Externals --- 26 + 27 + @external 28 + namespace com.atproto.label.defs { 29 + model Label { } 30 + }
+8
packages/emitter/test/integration/atproto/input/com/atproto/label/subscribeLabels.tsp
··· 21 21 cursor?: integer 22 22 ): (Labels | Info); 23 23 } 24 + 25 + // --- Externals --- 26 + 27 + @external 28 + namespace com.atproto.label.defs { 29 + model Label { } 30 + model Info { } 31 + }
+17
packages/emitter/test/integration/atproto/input/com/atproto/moderation/createReport.tsp
··· 50 50 meta?: unknown; 51 51 } 52 52 } 53 + 54 + // --- Externals --- 55 + 56 + @external 57 + namespace com.atproto.moderation.defs { 58 + model ReasonType { } 59 + } 60 + 61 + @external 62 + namespace com.atproto.admin.defs { 63 + model RepoRef { } 64 + } 65 + 66 + @external 67 + namespace com.atproto.repo.strongRef { 68 + model Main { } 69 + }
+51
packages/emitter/test/integration/atproto/input/com/atproto/moderation/defs.tsp
··· 93 93 string, 94 94 } 95 95 } 96 + 97 + // --- Externals --- 98 + 99 + @external 100 + namespace tools.ozone.report.defs { 101 + @token model ReasonAppeal { } 102 + @token model ReasonChildSafetyCSAM { } 103 + @token model ReasonChildSafetyEndangerment { } 104 + @token model ReasonChildSafetyGroom { } 105 + @token model ReasonChildSafetyHarassment { } 106 + @token model ReasonChildSafetyMinorPrivacy { } 107 + @token model ReasonChildSafetyOther { } 108 + @token model ReasonChildSafetyPromotion { } 109 + @token model ReasonCivicDisclosure { } 110 + @token model ReasonCivicElectoralProcess { } 111 + @token model ReasonCivicImpersonation { } 112 + @token model ReasonCivicInterference { } 113 + @token model ReasonCivicMisinformation { } 114 + @token model ReasonHarassmentDoxxing { } 115 + @token model ReasonHarassmentHateSpeech { } 116 + @token model ReasonHarassmentOther { } 117 + @token model ReasonHarassmentTargeted { } 118 + @token model ReasonHarassmentTroll { } 119 + @token model ReasonMisleadingBot { } 120 + @token model ReasonMisleadingImpersonation { } 121 + @token model ReasonMisleadingMisinformation { } 122 + @token model ReasonMisleadingOther { } 123 + @token model ReasonMisleadingScam { } 124 + @token model ReasonMisleadingSpam { } 125 + @token model ReasonMisleadingSyntheticContent { } 126 + @token model ReasonRuleBanEvasion { } 127 + @token model ReasonRuleOther { } 128 + @token model ReasonRuleProhibitedSales { } 129 + @token model ReasonRuleSiteSecurity { } 130 + @token model ReasonRuleStolenContent { } 131 + @token model ReasonSexualAbuseContent { } 132 + @token model ReasonSexualAnimal { } 133 + @token model ReasonSexualDeepfake { } 134 + @token model ReasonSexualNCII { } 135 + @token model ReasonSexualOther { } 136 + @token model ReasonSexualSextortion { } 137 + @token model ReasonSexualUnlabeled { } 138 + @token model ReasonViolenceAnimalWelfare { } 139 + @token model ReasonViolenceExtremistContent { } 140 + @token model ReasonViolenceGlorification { } 141 + @token model ReasonViolenceGraphicContent { } 142 + @token model ReasonViolenceOther { } 143 + @token model ReasonViolenceSelfHarm { } 144 + @token model ReasonViolenceThreats { } 145 + @token model ReasonViolenceTrafficking { } 146 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/repo/applyWrites.tsp
··· 79 79 80 80 model DeleteResult {} 81 81 } 82 + 83 + // --- Externals --- 84 + 85 + @external 86 + namespace com.atproto.repo.defs { 87 + model CommitMeta { } 88 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/repo/createRecord.tsp
··· 36 36 /** Indicates that 'swapCommit' didn't match current repo commit. */ 37 37 model InvalidSwap {} 38 38 } 39 + 40 + // --- Externals --- 41 + 42 + @external 43 + namespace com.atproto.repo.defs { 44 + model CommitMeta { } 45 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/repo/deleteRecord.tsp
··· 28 28 29 29 model InvalidSwap {} 30 30 } 31 + 32 + // --- Externals --- 33 + 34 + @external 35 + namespace com.atproto.repo.defs { 36 + model CommitMeta { } 37 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/repo/putRecord.tsp
··· 39 39 40 40 model InvalidSwap {} 41 41 } 42 + 43 + // --- Externals --- 44 + 45 + @external 46 + namespace com.atproto.repo.defs { 47 + model CommitMeta { } 48 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/server/getAccountInviteCodes.tsp
··· 15 15 @required codes: com.atproto.server.defs.InviteCode[]; 16 16 }; 17 17 } 18 + 19 + // --- Externals --- 20 + 21 + @external 22 + namespace com.atproto.server.defs { 23 + model InviteCode { } 24 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/sync/getHostStatus.tsp
··· 22 22 23 23 model HostNotFound {} 24 24 } 25 + 26 + // --- Externals --- 27 + 28 + @external 29 + namespace com.atproto.sync.defs { 30 + model HostStatus { } 31 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/sync/listHosts.tsp
··· 29 29 status?: com.atproto.sync.defs.HostStatus; 30 30 } 31 31 } 32 + 33 + // --- Externals --- 34 + 35 + @external 36 + namespace com.atproto.sync.defs { 37 + model HostStatus { } 38 + }
+7
packages/emitter/test/integration/atproto/input/com/atproto/temp/fetchLabels.tsp
··· 13 13 @required labels: com.atproto.label.defs.Label[]; 14 14 }; 15 15 } 16 + 17 + // --- Externals --- 18 + 19 + @external 20 + namespace com.atproto.label.defs { 21 + model Label { } 22 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/communication/createTemplate.tsp
··· 26 26 createdBy?: did; 27 27 }): tools.ozone.communication.defs.TemplateView; 28 28 } 29 + 30 + // --- Externals --- 31 + 32 + @external 33 + namespace tools.ozone.communication.defs { 34 + model TemplateView { } 35 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/communication/listTemplates.tsp
··· 7 7 @required communicationTemplates: tools.ozone.communication.defs.TemplateView[]; 8 8 }; 9 9 } 10 + 11 + // --- Externals --- 12 + 13 + @external 14 + namespace tools.ozone.communication.defs { 15 + model TemplateView { } 16 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/communication/updateTemplate.tsp
··· 29 29 disabled?: boolean; 30 30 }): tools.ozone.communication.defs.TemplateView; 31 31 } 32 + 33 + // --- Externals --- 34 + 35 + @external 36 + namespace tools.ozone.communication.defs { 37 + model TemplateView { } 38 + }
+34
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/defs.tsp
··· 601 601 @token 602 602 model TimelineEventPlcTombstone {} 603 603 } 604 + 605 + // --- Externals --- 606 + 607 + @external 608 + namespace com.atproto.admin.defs { 609 + model RepoRef { } 610 + model ThreatSignature { } 611 + } 612 + 613 + @external 614 + namespace com.atproto.repo.strongRef { 615 + model Main { } 616 + } 617 + 618 + @external 619 + namespace chat.bsky.convo.defs { 620 + model MessageRef { } 621 + } 622 + 623 + @external 624 + namespace com.atproto.moderation.defs { 625 + model SubjectType { } 626 + model ReasonType { } 627 + } 628 + 629 + @external 630 + namespace com.atproto.server.defs { 631 + model InviteCode { } 632 + } 633 + 634 + @external 635 + namespace com.atproto.label.defs { 636 + model Label { } 637 + }
+40
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/emitEvent.tsp
··· 54 54 externalId?: string; 55 55 }): tools.ozone.moderation.defs.ModEventView; 56 56 } 57 + 58 + // --- Externals --- 59 + 60 + @external 61 + namespace tools.ozone.moderation.defs { 62 + model ModEventTakedown { } 63 + model ModEventAcknowledge { } 64 + model ModEventEscalate { } 65 + model ModEventComment { } 66 + model ModEventLabel { } 67 + model ModEventReport { } 68 + model ModEventMute { } 69 + model ModEventUnmute { } 70 + model ModEventMuteReporter { } 71 + model ModEventUnmuteReporter { } 72 + model ModEventReverseTakedown { } 73 + model ModEventResolveAppeal { } 74 + model ModEventEmail { } 75 + model ModEventDivert { } 76 + model ModEventTag { } 77 + model AccountEvent { } 78 + model IdentityEvent { } 79 + model RecordEvent { } 80 + model ModEventPriorityScore { } 81 + model AgeAssuranceEvent { } 82 + model AgeAssuranceOverrideEvent { } 83 + model RevokeAccountCredentialsEvent { } 84 + model ModTool { } 85 + model ModEventView { } 86 + } 87 + 88 + @external 89 + namespace com.atproto.admin.defs { 90 + model RepoRef { } 91 + } 92 + 93 + @external 94 + namespace com.atproto.repo.strongRef { 95 + model Main { } 96 + }
+39
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getAccountTimeline.tsp
··· 55 55 @required count: integer; 56 56 } 57 57 } 58 + 59 + // --- Externals --- 60 + 61 + @external 62 + namespace tools.ozone.moderation.defs { 63 + model ModEventTakedown { } 64 + model ModEventReverseTakedown { } 65 + model ModEventComment { } 66 + model ModEventReport { } 67 + model ModEventLabel { } 68 + model ModEventAcknowledge { } 69 + model ModEventEscalate { } 70 + model ModEventMute { } 71 + model ModEventUnmute { } 72 + model ModEventMuteReporter { } 73 + model ModEventUnmuteReporter { } 74 + model ModEventEmail { } 75 + model ModEventResolveAppeal { } 76 + model ModEventDivert { } 77 + model ModEventTag { } 78 + model AccountEvent { } 79 + model IdentityEvent { } 80 + model RecordEvent { } 81 + model ModEventPriorityScore { } 82 + model RevokeAccountCredentialsEvent { } 83 + model AgeAssuranceEvent { } 84 + model AgeAssuranceOverrideEvent { } 85 + @token model TimelineEventPlcCreate { } 86 + @token model TimelineEventPlcOperation { } 87 + @token model TimelineEventPlcTombstone { } 88 + } 89 + 90 + @external 91 + namespace tools.ozone.hosting.getAccountHistory { 92 + model AccountCreated { } 93 + model EmailConfirmed { } 94 + model PasswordUpdated { } 95 + model HandleUpdated { } 96 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getEvent.tsp
··· 5 5 @query 6 6 op main(@required id: integer): tools.ozone.moderation.defs.ModEventViewDetail; 7 7 } 8 + 9 + // --- Externals --- 10 + 11 + @external 12 + namespace tools.ozone.moderation.defs { 13 + model ModEventViewDetail { } 14 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getRecord.tsp
··· 8 8 @errors(RecordNotFound) 9 9 op main(@required uri: atUri, cid?: cid): tools.ozone.moderation.defs.RecordViewDetail; 10 10 } 11 + 12 + // --- Externals --- 13 + 14 + @external 15 + namespace tools.ozone.moderation.defs { 16 + model RecordViewDetail { } 17 + }
+8
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getRecords.tsp
··· 16 16 )[]; 17 17 }; 18 18 } 19 + 20 + // --- Externals --- 21 + 22 + @external 23 + namespace tools.ozone.moderation.defs { 24 + model RecordViewDetail { } 25 + model RecordViewNotFound { } 26 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getRepo.tsp
··· 8 8 @errors(RepoNotFound) 9 9 op main(@required did: did): tools.ozone.moderation.defs.RepoViewDetail; 10 10 } 11 + 12 + // --- Externals --- 13 + 14 + @external 15 + namespace tools.ozone.moderation.defs { 16 + model RepoViewDetail { } 17 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getReporterStats.tsp
··· 11 11 @required stats: tools.ozone.moderation.defs.ReporterStats[]; 12 12 }; 13 13 } 14 + 15 + // --- Externals --- 16 + 17 + @external 18 + namespace tools.ozone.moderation.defs { 19 + model ReporterStats { } 20 + }
+8
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getRepos.tsp
··· 16 16 )[]; 17 17 }; 18 18 } 19 + 20 + // --- Externals --- 21 + 22 + @external 23 + namespace tools.ozone.moderation.defs { 24 + model RepoViewDetail { } 25 + model RepoViewNotFound { } 26 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getSubjects.tsp
··· 12 12 @required subjects: tools.ozone.moderation.defs.SubjectView[]; 13 13 }; 14 14 } 15 + 16 + // --- Externals --- 17 + 18 + @external 19 + namespace tools.ozone.moderation.defs { 20 + model SubjectView { } 21 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/queryEvents.tsp
··· 80 80 @required events: tools.ozone.moderation.defs.ModEventView[]; 81 81 }; 82 82 } 83 + 84 + // --- Externals --- 85 + 86 + @external 87 + namespace tools.ozone.moderation.defs { 88 + model ModEventView { } 89 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/queryStatuses.tsp
··· 131 131 @required subjectStatuses: tools.ozone.moderation.defs.SubjectStatusView[]; 132 132 }; 133 133 } 134 + 135 + // --- Externals --- 136 + 137 + @external 138 + namespace tools.ozone.moderation.defs { 139 + model SubjectStatusView { } 140 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/searchRepos.tsp
··· 20 20 @required repos: tools.ozone.moderation.defs.RepoView[]; 21 21 }; 22 22 } 23 + 24 + // --- Externals --- 25 + 26 + @external 27 + namespace tools.ozone.moderation.defs { 28 + model RepoView { } 29 + }
+10
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/addRule.tsp
··· 28 28 createdBy?: did; 29 29 }): tools.ozone.safelink.defs.Event; 30 30 } 31 + 32 + // --- Externals --- 33 + 34 + @external 35 + namespace tools.ozone.safelink.defs { 36 + model PatternType { } 37 + model ActionType { } 38 + model ReasonType { } 39 + model Event { } 40 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/queryEvents.tsp
··· 27 27 @required events: tools.ozone.safelink.defs.Event[]; 28 28 }; 29 29 } 30 + 31 + // --- Externals --- 32 + 33 + @external 34 + namespace tools.ozone.safelink.defs { 35 + model Event { } 36 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/queryRules.tsp
··· 36 36 @required rules: tools.ozone.safelink.defs.UrlRule[]; 37 37 }; 38 38 } 39 + 40 + // --- Externals --- 41 + 42 + @external 43 + namespace tools.ozone.safelink.defs { 44 + model UrlRule { } 45 + }
+8
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/removeRule.tsp
··· 21 21 createdBy?: did; 22 22 }): tools.ozone.safelink.defs.Event; 23 23 } 24 + 25 + // --- Externals --- 26 + 27 + @external 28 + namespace tools.ozone.safelink.defs { 29 + model PatternType { } 30 + model Event { } 31 + }
+10
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/updateRule.tsp
··· 25 25 createdBy?: did; 26 26 }): tools.ozone.safelink.defs.Event; 27 27 } 28 + 29 + // --- Externals --- 30 + 31 + @external 32 + namespace tools.ozone.safelink.defs { 33 + model PatternType { } 34 + model ActionType { } 35 + model ReasonType { } 36 + model Event { } 37 + }
+10
packages/emitter/test/integration/atproto/input/tools/ozone/server/getConfig.tsp
··· 27 27 | string; 28 28 } 29 29 } 30 + 31 + // --- Externals --- 32 + 33 + @external 34 + namespace tools.ozone.team.defs { 35 + @token model RoleAdmin { } 36 + @token model RoleModerator { } 37 + @token model RoleTriage { } 38 + @token model RoleVerifier { } 39 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/set/getValues.tsp
··· 23 23 cursor?: string; 24 24 }; 25 25 } 26 + 27 + // --- Externals --- 28 + 29 + @external 30 + namespace tools.ozone.set.defs { 31 + model SetView { } 32 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/set/querySets.tsp
··· 37 37 cursor?: string; 38 38 }; 39 39 } 40 + 41 + // --- Externals --- 42 + 43 + @external 44 + namespace tools.ozone.set.defs { 45 + model SetView { } 46 + }
+8
packages/emitter/test/integration/atproto/input/tools/ozone/set/upsertSet.tsp
··· 5 5 @procedure 6 6 op main(input: tools.ozone.set.defs.Set): tools.ozone.set.defs.SetView; 7 7 } 8 + 9 + // --- Externals --- 10 + 11 + @external 12 + namespace tools.ozone.set.defs { 13 + model Set { } 14 + model SetView { } 15 + }
+10
packages/emitter/test/integration/atproto/input/tools/ozone/setting/defs.tsp
··· 27 27 @required lastUpdatedBy: did; 28 28 } 29 29 } 30 + 31 + // --- Externals --- 32 + 33 + @external 34 + namespace tools.ozone.team.defs { 35 + @token model RoleModerator { } 36 + @token model RoleTriage { } 37 + @token model RoleAdmin { } 38 + @token model RoleVerifier { } 39 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/setting/listOptions.tsp
··· 24 24 @required options: tools.ozone.setting.defs.Option[]; 25 25 }; 26 26 } 27 + 28 + // --- Externals --- 29 + 30 + @external 31 + namespace tools.ozone.setting.defs { 32 + model Option { } 33 + }
+15
packages/emitter/test/integration/atproto/input/tools/ozone/setting/upsertOption.tsp
··· 23 23 @required option: tools.ozone.setting.defs.Option; 24 24 }; 25 25 } 26 + 27 + // --- Externals --- 28 + 29 + @external 30 + namespace tools.ozone.team.defs { 31 + @token model RoleModerator { } 32 + @token model RoleTriage { } 33 + @token model RoleVerifier { } 34 + @token model RoleAdmin { } 35 + } 36 + 37 + @external 38 + namespace tools.ozone.setting.defs { 39 + model Option { } 40 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/signature/findCorrelation.tsp
··· 7 7 @required details: tools.ozone.signature.defs.SigDetail[]; 8 8 }; 9 9 } 10 + 11 + // --- Externals --- 12 + 13 + @external 14 + namespace tools.ozone.signature.defs { 15 + model SigDetail { } 16 + }
+12
packages/emitter/test/integration/atproto/input/tools/ozone/signature/findRelatedAccounts.tsp
··· 23 23 similarities?: tools.ozone.signature.defs.SigDetail[]; 24 24 } 25 25 } 26 + 27 + // --- Externals --- 28 + 29 + @external 30 + namespace com.atproto.admin.defs { 31 + model AccountView { } 32 + } 33 + 34 + @external 35 + namespace tools.ozone.signature.defs { 36 + model SigDetail { } 37 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/signature/searchAccounts.tsp
··· 17 17 @required accounts: com.atproto.admin.defs.AccountView[]; 18 18 }; 19 19 } 20 + 21 + // --- Externals --- 22 + 23 + @external 24 + namespace com.atproto.admin.defs { 25 + model AccountView { } 26 + }
+11
packages/emitter/test/integration/atproto/input/tools/ozone/team/addMember.tsp
··· 19 19 | string; 20 20 }): tools.ozone.team.defs.Member; 21 21 } 22 + 23 + // --- Externals --- 24 + 25 + @external 26 + namespace tools.ozone.team.defs { 27 + @token model RoleAdmin { } 28 + @token model RoleModerator { } 29 + @token model RoleVerifier { } 30 + @token model RoleTriage { } 31 + model Member { } 32 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/team/defs.tsp
··· 27 27 @token 28 28 model RoleVerifier {} 29 29 } 30 + 31 + // --- Externals --- 32 + 33 + @external 34 + namespace app.bsky.actor.defs { 35 + model ProfileViewDetailed { } 36 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/team/listMembers.tsp
··· 18 18 @required members: tools.ozone.team.defs.Member[]; 19 19 }; 20 20 } 21 + 22 + // --- Externals --- 23 + 24 + @external 25 + namespace tools.ozone.team.defs { 26 + model Member { } 27 + }
+11
packages/emitter/test/integration/atproto/input/tools/ozone/team/updateMember.tsp
··· 19 19 | string; 20 20 }): tools.ozone.team.defs.Member; 21 21 } 22 + 23 + // --- Externals --- 24 + 25 + @external 26 + namespace tools.ozone.team.defs { 27 + @token model RoleAdmin { } 28 + @token model RoleModerator { } 29 + @token model RoleVerifier { } 30 + @token model RoleTriage { } 31 + model Member { } 32 + }
+8
packages/emitter/test/integration/atproto/input/tools/ozone/verification/defs.tsp
··· 52 52 ); 53 53 } 54 54 } 55 + 56 + // --- Externals --- 57 + 58 + @external 59 + namespace tools.ozone.moderation.defs { 60 + model RepoViewDetail { } 61 + model RepoViewNotFound { } 62 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/verification/grantVerifications.tsp
··· 41 41 subject: did; 42 42 } 43 43 } 44 + 45 + // --- Externals --- 46 + 47 + @external 48 + namespace tools.ozone.verification.defs { 49 + model VerificationView { } 50 + }
+7
packages/emitter/test/integration/atproto/input/tools/ozone/verification/listVerifications.tsp
··· 43 43 @required verifications: tools.ozone.verification.defs.VerificationView[]; 44 44 }; 45 45 } 46 + 47 + // --- Externals --- 48 + 49 + @external 50 + namespace tools.ozone.verification.defs { 51 + model VerificationView { } 52 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/defs.tsp
··· 38 38 @required value: string; 39 39 } 40 40 } 41 + 42 + // --- Externals --- 43 + 44 + @external 45 + namespace com.atproto.server.defs { 46 + model InviteCode { } 47 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/getAccountInfo.tsp
··· 7 7 @required did: did 8 8 ): com.atproto.admin.defs.AccountView; 9 9 } 10 + 11 + // --- Externals --- 12 + 13 + @external 14 + namespace com.atproto.admin.defs { 15 + model AccountView { } 16 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/getAccountInfos.tsp
··· 9 9 @required infos: com.atproto.admin.defs.AccountView[]; 10 10 }; 11 11 } 12 + 13 + // --- Externals --- 14 + 15 + @external 16 + namespace com.atproto.admin.defs { 17 + model AccountView { } 18 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/getInviteCodes.tsp
··· 16 16 @required codes: com.atproto.server.defs.InviteCode[]; 17 17 }; 18 18 } 19 + 20 + // --- Externals --- 21 + 22 + @external 23 + namespace com.atproto.server.defs { 24 + model InviteCode { } 25 + }
+14
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/getSubjectStatus.tsp
··· 20 20 deactivated?: com.atproto.admin.defs.StatusAttr; 21 21 }; 22 22 } 23 + 24 + // --- Externals --- 25 + 26 + @external 27 + namespace com.atproto.admin.defs { 28 + model RepoRef { } 29 + model RepoBlobRef { } 30 + model StatusAttr { } 31 + } 32 + 33 + @external 34 + namespace com.atproto.repo.strongRef { 35 + model Main { } 36 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/searchAccounts.tsp
··· 15 15 @required accounts: com.atproto.admin.defs.AccountView[]; 16 16 }; 17 17 } 18 + 19 + // --- Externals --- 20 + 21 + @external 22 + namespace com.atproto.admin.defs { 23 + model AccountView { } 24 + }
+14
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/updateSubjectStatus.tsp
··· 26 26 takedown?: com.atproto.admin.defs.StatusAttr; 27 27 }; 28 28 } 29 + 30 + // --- Externals --- 31 + 32 + @external 33 + namespace com.atproto.admin.defs { 34 + model RepoRef { } 35 + model RepoBlobRef { } 36 + model StatusAttr { } 37 + } 38 + 39 + @external 40 + namespace com.atproto.repo.strongRef { 41 + model Main { } 42 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/identity/refreshIdentity.tsp
··· 17 17 @required identifier: atIdentifier; 18 18 }): com.atproto.identity.defs.IdentityInfo; 19 19 } 20 + 21 + // --- Externals --- 22 + 23 + @external 24 + namespace com.atproto.identity.defs { 25 + model IdentityInfo { } 26 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/identity/resolveIdentity.tsp
··· 19 19 identifier: atIdentifier 20 20 ): com.atproto.identity.defs.IdentityInfo; 21 21 } 22 + 23 + // --- Externals --- 24 + 25 + @external 26 + namespace com.atproto.identity.defs { 27 + model IdentityInfo { } 28 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/label/queryLabels.tsp
··· 21 21 @required labels: com.atproto.label.defs.Label[]; 22 22 }; 23 23 } 24 + 25 + // --- Externals --- 26 + 27 + @external 28 + namespace com.atproto.label.defs { 29 + model Label { } 30 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/label/subscribeLabels.tsp
··· 21 21 cursor?: integer 22 22 ): (Labels | Info); 23 23 } 24 + 25 + // --- Externals --- 26 + 27 + @external 28 + namespace com.atproto.label.defs { 29 + model Label { } 30 + }
+17
packages/emitter/test/integration/lexicon-examples/input/com/atproto/moderation/createReport.tsp
··· 50 50 meta?: unknown; 51 51 } 52 52 } 53 + 54 + // --- Externals --- 55 + 56 + @external 57 + namespace com.atproto.moderation.defs { 58 + model ReasonType { } 59 + } 60 + 61 + @external 62 + namespace com.atproto.admin.defs { 63 + model RepoRef { } 64 + } 65 + 66 + @external 67 + namespace com.atproto.repo.strongRef { 68 + model Main { } 69 + }
+51
packages/emitter/test/integration/lexicon-examples/input/com/atproto/moderation/defs.tsp
··· 102 102 string, 103 103 } 104 104 } 105 + 106 + // --- Externals --- 107 + 108 + @external 109 + namespace tools.ozone.report.defs { 110 + @token model ReasonAppeal { } 111 + @token model ReasonViolenceAnimalWelfare { } 112 + @token model ReasonViolenceThreats { } 113 + @token model ReasonViolenceGraphicContent { } 114 + @token model ReasonViolenceSelfHarm { } 115 + @token model ReasonViolenceGlorification { } 116 + @token model ReasonViolenceExtremistContent { } 117 + @token model ReasonViolenceTrafficking { } 118 + @token model ReasonViolenceOther { } 119 + @token model ReasonSexualAbuseContent { } 120 + @token model ReasonSexualNCII { } 121 + @token model ReasonSexualSextortion { } 122 + @token model ReasonSexualDeepfake { } 123 + @token model ReasonSexualAnimal { } 124 + @token model ReasonSexualUnlabeled { } 125 + @token model ReasonSexualOther { } 126 + @token model ReasonChildSafetyCSAM { } 127 + @token model ReasonChildSafetyGroom { } 128 + @token model ReasonChildSafetyMinorPrivacy { } 129 + @token model ReasonChildSafetyEndangerment { } 130 + @token model ReasonChildSafetyHarassment { } 131 + @token model ReasonChildSafetyPromotion { } 132 + @token model ReasonChildSafetyOther { } 133 + @token model ReasonHarassmentTroll { } 134 + @token model ReasonHarassmentTargeted { } 135 + @token model ReasonHarassmentHateSpeech { } 136 + @token model ReasonHarassmentDoxxing { } 137 + @token model ReasonHarassmentOther { } 138 + @token model ReasonMisleadingBot { } 139 + @token model ReasonMisleadingImpersonation { } 140 + @token model ReasonMisleadingSpam { } 141 + @token model ReasonMisleadingScam { } 142 + @token model ReasonMisleadingSyntheticContent { } 143 + @token model ReasonMisleadingMisinformation { } 144 + @token model ReasonMisleadingOther { } 145 + @token model ReasonRuleSiteSecurity { } 146 + @token model ReasonRuleStolenContent { } 147 + @token model ReasonRuleProhibitedSales { } 148 + @token model ReasonRuleBanEvasion { } 149 + @token model ReasonRuleOther { } 150 + @token model ReasonCivicElectoralProcess { } 151 + @token model ReasonCivicDisclosure { } 152 + @token model ReasonCivicInterference { } 153 + @token model ReasonCivicMisinformation { } 154 + @token model ReasonCivicImpersonation { } 155 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/repo/applyWrites.tsp
··· 79 79 80 80 model DeleteResult {} 81 81 } 82 + 83 + // --- Externals --- 84 + 85 + @external 86 + namespace com.atproto.repo.defs { 87 + model CommitMeta { } 88 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/repo/createRecord.tsp
··· 36 36 /** Indicates that 'swapCommit' didn't match current repo commit. */ 37 37 model InvalidSwap {} 38 38 } 39 + 40 + // --- Externals --- 41 + 42 + @external 43 + namespace com.atproto.repo.defs { 44 + model CommitMeta { } 45 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/repo/deleteRecord.tsp
··· 28 28 29 29 model InvalidSwap {} 30 30 } 31 + 32 + // --- Externals --- 33 + 34 + @external 35 + namespace com.atproto.repo.defs { 36 + model CommitMeta { } 37 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/repo/putRecord.tsp
··· 39 39 40 40 model InvalidSwap {} 41 41 } 42 + 43 + // --- Externals --- 44 + 45 + @external 46 + namespace com.atproto.repo.defs { 47 + model CommitMeta { } 48 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/server/getAccountInviteCodes.tsp
··· 15 15 @required codes: com.atproto.server.defs.InviteCode[]; 16 16 }; 17 17 } 18 + 19 + // --- Externals --- 20 + 21 + @external 22 + namespace com.atproto.server.defs { 23 + model InviteCode { } 24 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/sync/getHostStatus.tsp
··· 22 22 23 23 model HostNotFound {} 24 24 } 25 + 26 + // --- Externals --- 27 + 28 + @external 29 + namespace com.atproto.sync.defs { 30 + model HostStatus { } 31 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/sync/listHosts.tsp
··· 29 29 status?: com.atproto.sync.defs.HostStatus; 30 30 } 31 31 } 32 + 33 + // --- Externals --- 34 + 35 + @external 36 + namespace com.atproto.sync.defs { 37 + model HostStatus { } 38 + }
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/temp/fetchLabels.tsp
··· 13 13 @required labels: com.atproto.label.defs.Label[]; 14 14 }; 15 15 } 16 + 17 + // --- Externals --- 18 + 19 + @external 20 + namespace com.atproto.label.defs { 21 + model Label { } 22 + }
+14
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/bookmarks/bookmark.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace community.lexicon.bookmarks.bookmark { 4 + /** Record bookmarking a link to come back to later. */ 5 + @rec("tid") 6 + model Main { 7 + @required subject: uri; 8 + 9 + @required createdAt: datetime; 10 + 11 + /** Tags for content the bookmark may be related to, for example 'news' or 'funny videos' */ 12 + tags?: string[]; 13 + } 14 + }
+27
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/bookmarks/getActorBookmarks.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace community.lexicon.bookmarks.getActorBookmarks { 4 + /** Get a list of bookmarks by actor. Optionally add a list of tags to include, default will be all bookmarks. Requires auth, actor must be the requesting account. */ 5 + @query 6 + op main( 7 + tags?: string[], 8 + 9 + @minValue(1) 10 + @maxValue(100) 11 + limit?: int32 = 50, 12 + 13 + cursor?: string 14 + ): { 15 + @required 16 + bookmarks: community.lexicon.bookmarks.bookmark.Main[]; 17 + 18 + cursor?: string; 19 + }; 20 + } 21 + 22 + // --- Externals --- 23 + 24 + @external 25 + namespace community.lexicon.bookmarks.bookmark { 26 + model Main { } 27 + }
+125
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/calendar/event.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace community.lexicon.calendar.event { 4 + /** A calendar event. */ 5 + @rec("tid") 6 + model Main { 7 + /** The name of the event. */ 8 + @required 9 + name: string; 10 + 11 + /** The description of the event. */ 12 + description?: string; 13 + 14 + /** Client-declared timestamp when the event was created. */ 15 + @required 16 + createdAt: datetime; 17 + 18 + /** Client-declared timestamp when the event starts. */ 19 + startsAt?: datetime; 20 + 21 + /** Client-declared timestamp when the event ends. */ 22 + endsAt?: datetime; 23 + 24 + /** The attendance mode of the event. */ 25 + mode?: Mode; 26 + 27 + /** The status of the event. */ 28 + status?: Status; 29 + 30 + /** The locations where the event takes place. */ 31 + locations?: ( 32 + | Uri 33 + | community.lexicon.location.address.Main 34 + | community.lexicon.location.fsq.Main 35 + | community.lexicon.location.geo.Main 36 + | community.lexicon.location.hthree.Main 37 + )[]; 38 + 39 + /** URIs associated with the event. */ 40 + uris?: Uri[]; 41 + } 42 + 43 + /** The mode of the event. */ 44 + @default(Inperson) 45 + union Mode { 46 + Hybrid, 47 + Inperson, 48 + Virtual, 49 + string, 50 + } 51 + 52 + /** A virtual event that takes place online. */ 53 + @token 54 + model Virtual {} 55 + 56 + /** An in-person event that takes place offline. */ 57 + @token 58 + model Inperson {} 59 + 60 + /** A hybrid event that takes place both online and offline. */ 61 + @token 62 + model Hybrid {} 63 + 64 + /** The status of the event. */ 65 + @default(Scheduled) 66 + union Status { 67 + Cancelled, 68 + Planned, 69 + Postponed, 70 + Rescheduled, 71 + Scheduled, 72 + string, 73 + } 74 + 75 + /** The event has been created, but not finalized. */ 76 + @token 77 + model Planned {} 78 + 79 + /** The event has been created and scheduled. */ 80 + @token 81 + model Scheduled {} 82 + 83 + /** The event has been rescheduled. */ 84 + @token 85 + model Rescheduled {} 86 + 87 + /** The event has been cancelled. */ 88 + @token 89 + model Cancelled {} 90 + 91 + /** The event has been postponed and a new start date has not been set. */ 92 + @token 93 + model Postponed {} 94 + 95 + /** A URI associated with the event. */ 96 + model Uri { 97 + @required 98 + uri: uri; 99 + 100 + /** The display name of the URI. */ 101 + name?: string; 102 + } 103 + } 104 + 105 + // --- Externals --- 106 + 107 + @external 108 + namespace community.lexicon.location.address { 109 + model Main {} 110 + } 111 + 112 + @external 113 + namespace community.lexicon.location.fsq { 114 + model Main {} 115 + } 116 + 117 + @external 118 + namespace community.lexicon.location.geo { 119 + model Main {} 120 + } 121 + 122 + @external 123 + namespace community.lexicon.location.hthree { 124 + model Main {} 125 + }
+41
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/calendar/rsvp.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace community.lexicon.calendar.rsvp { 4 + /** An RSVP for an event. */ 5 + @rec("tid") 6 + model Main { 7 + @required 8 + subject: `com`.atproto.repo.strongRef.Main; 9 + 10 + @required 11 + status: Status; 12 + } 13 + 14 + @inline 15 + @default(Going) 16 + union Status { 17 + Interested, 18 + Going, 19 + Notgoing, 20 + string, 21 + } 22 + 23 + /** Interested in the event */ 24 + @token 25 + model Interested {} 26 + 27 + /** Going to the event */ 28 + @token 29 + model Going {} 30 + 31 + /** Not going to the event */ 32 + @token 33 + model Notgoing {} 34 + } 35 + 36 + // --- Externals --- 37 + 38 + @external 39 + namespace `com`.atproto.repo.strongRef { 40 + model Main {} 41 + }
+20
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/interaction/like.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace community.lexicon.interaction.like { 4 + /** A 'like' interaction with another AT Protocol record. */ 5 + @rec("tid") 6 + model Main { 7 + @required 8 + subject: `com`.atproto.repo.strongRef.Main; 9 + 10 + @required 11 + createdAt: datetime; 12 + } 13 + } 14 + 15 + // --- Externals --- 16 + 17 + @external 18 + namespace `com`.atproto.repo.strongRef { 19 + model Main { } 20 + }
+27
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/location/address.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace community.lexicon.location.address { 4 + /** A physical location in the form of a street address. */ 5 + model Main { 6 + /** The ISO 3166 country code. Preferably the 2-letter code. */ 7 + @required 8 + @minLength(2) 9 + @maxLength(10) 10 + country: string; 11 + 12 + /** The postal code of the location. */ 13 + postalCode?: string; 14 + 15 + /** The administrative region of the country. For example, a state in the USA. */ 16 + region?: string; 17 + 18 + /** The locality of the region. For example, a city in the USA. */ 19 + locality?: string; 20 + 21 + /** The street address. */ 22 + street?: string; 23 + 24 + /** The name of the location. */ 25 + name?: string; 26 + } 27 + }
+15
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/location/fsq.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace community.lexicon.location.fsq { 4 + /** A physical location contained in the Foursquare Open Source Places dataset. */ 5 + model Main { 6 + /** The unique identifier of a Foursquare POI. */ 7 + @required fsq_place_id: string; 8 + 9 + latitude?: string; 10 + longitude?: string; 11 + 12 + /** The name of the location. */ 13 + name?: string; 14 + } 15 + }
+12
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/location/geo.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace community.lexicon.location.geo { 4 + /** A physical location in the form of a WGS84 coordinate. */ 5 + model Main { 6 + @required latitude: string; 7 + @required longitude: string; 8 + altitude?: string; 9 + /** The name of the location. */ 10 + name?: string; 11 + } 12 + }
+12
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/location/hthree.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace community.lexicon.location.hthree { 4 + /** A physical location in the form of a H3 encoded location. */ 5 + model Main { 6 + /** The h3 encoded location. */ 7 + @required value: string; 8 + 9 + /** The name of the location. */ 10 + name?: string; 11 + } 12 + }
+14
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/payments/webMonetization.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + /** Web Monetization integration: https://webmonetization.org/ */ 4 + namespace community.lexicon.payments.webMonetization { 5 + @rec("any") 6 + /** Web Monetization wallet. */ 7 + model Main { 8 + /** Wallet address. */ 9 + @required address: uri; 10 + 11 + /** Short, human-readable description of how this wallet is related to this account. */ 12 + note?: string; 13 + } 14 + }
+7
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/blocks/blockquote.tsp
··· 6 6 facets?: `pub`.leaflet.richtext.facet.Main[]; 7 7 } 8 8 } 9 + 10 + // --- Externals --- 11 + 12 + @external 13 + namespace `pub`.leaflet.richtext.facet { 14 + model Main { } 15 + }
+7
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/blocks/bskyPost.tsp
··· 5 5 @required postRef: com.atproto.repo.strongRef.Main; 6 6 } 7 7 } 8 + 9 + // --- Externals --- 10 + 11 + @external 12 + namespace com.atproto.repo.strongRef { 13 + model Main { } 14 + }
+7
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/blocks/header.tsp
··· 10 10 facets?: `pub`.leaflet.richtext.facet.Main[]; 11 11 } 12 12 } 13 + 14 + // --- Externals --- 15 + 16 + @external 17 + namespace `pub`.leaflet.richtext.facet { 18 + model Main { } 19 + }
+7
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/blocks/text.tsp
··· 6 6 facets?: `pub`.leaflet.richtext.facet.Main[]; 7 7 } 8 8 } 9 + 10 + // --- Externals --- 11 + 12 + @external 13 + namespace `pub`.leaflet.richtext.facet { 14 + model Main { } 15 + }
+17
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/blocks/unorderedList.tsp
··· 10 10 children?: ListItem[]; 11 11 } 12 12 } 13 + 14 + // --- Externals --- 15 + 16 + @external 17 + namespace `pub`.leaflet.blocks.text { 18 + model Main { } 19 + } 20 + 21 + @external 22 + namespace `pub`.leaflet.blocks.header { 23 + model Main { } 24 + } 25 + 26 + @external 27 + namespace `pub`.leaflet.blocks.image { 28 + model Main { } 29 + }
+12
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/comment.tsp
··· 26 26 @required parent: atUri; 27 27 } 28 28 } 29 + 30 + // --- Externals --- 31 + 32 + @external 33 + namespace `pub`.leaflet.richtext.facet { 34 + model Main { } 35 + } 36 + 37 + @external 38 + namespace `pub`.leaflet.pages.linearDocument { 39 + model Quote { } 40 + }
+12
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/document.tsp
··· 23 23 @required pages: (`pub`.leaflet.pages.linearDocument.Main | unknown)[]; 24 24 } 25 25 } 26 + 27 + // --- Externals --- 28 + 29 + @external 30 + namespace com.atproto.repo.strongRef { 31 + model Main { } 32 + } 33 + 34 + @external 35 + namespace `pub`.leaflet.pages.linearDocument { 36 + model Main { } 37 + }
+57
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/pages/linearDocument.tsp
··· 43 43 @required offset: integer; 44 44 } 45 45 } 46 + 47 + // --- Externals --- 48 + 49 + @external 50 + namespace `pub`.leaflet.blocks.iframe { 51 + model Main { } 52 + } 53 + 54 + @external 55 + namespace `pub`.leaflet.blocks.text { 56 + model Main { } 57 + } 58 + 59 + @external 60 + namespace `pub`.leaflet.blocks.blockquote { 61 + model Main { } 62 + } 63 + 64 + @external 65 + namespace `pub`.leaflet.blocks.header { 66 + model Main { } 67 + } 68 + 69 + @external 70 + namespace `pub`.leaflet.blocks.image { 71 + model Main { } 72 + } 73 + 74 + @external 75 + namespace `pub`.leaflet.blocks.unorderedList { 76 + model Main { } 77 + } 78 + 79 + @external 80 + namespace `pub`.leaflet.blocks.website { 81 + model Main { } 82 + } 83 + 84 + @external 85 + namespace `pub`.leaflet.blocks.math { 86 + model Main { } 87 + } 88 + 89 + @external 90 + namespace `pub`.leaflet.blocks.code { 91 + model Main { } 92 + } 93 + 94 + @external 95 + namespace `pub`.leaflet.blocks.horizontalRule { 96 + model Main { } 97 + } 98 + 99 + @external 100 + namespace `pub`.leaflet.blocks.bskyPost { 101 + model Main { } 102 + }
+13
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/publication.tsp
··· 34 34 accentText?: (`pub`.leaflet.theme.color.Rgba | `pub`.leaflet.theme.color.Rgb | unknown); 35 35 } 36 36 } 37 + 38 + // --- Externals --- 39 + 40 + @external 41 + namespace `pub`.leaflet.theme.color { 42 + model Rgba { } 43 + model Rgb { } 44 + } 45 + 46 + @external 47 + namespace `pub`.leaflet.theme.backgroundImage { 48 + model Main { } 49 + }
+12
packages/emitter/test/integration/lexicon-examples/input/sh/tangled/repo/issue/state.tsp
··· 10 10 state: "sh.tangled.repo.issue.state.open" | "sh.tangled.repo.issue.state.closed" | string = "sh.tangled.repo.issue.state.open"; 11 11 } 12 12 } 13 + 14 + // --- Externals --- 15 + 16 + @external 17 + namespace sh.tangled.repo.issue.state.open { 18 + @token model Main { } 19 + } 20 + 21 + @external 22 + namespace sh.tangled.repo.issue.state.closed { 23 + @token model Main { } 24 + }
+17
packages/emitter/test/integration/lexicon-examples/input/sh/tangled/repo/pull/status.tsp
··· 10 10 status: "sh.tangled.repo.pull.status.open" | "sh.tangled.repo.pull.status.closed" | "sh.tangled.repo.pull.status.merged" | string = "sh.tangled.repo.pull.status.open"; 11 11 } 12 12 } 13 + 14 + // --- Externals --- 15 + 16 + @external 17 + namespace sh.tangled.repo.pull.status.open { 18 + @token model Main { } 19 + } 20 + 21 + @external 22 + namespace sh.tangled.repo.pull.status.closed { 23 + @token model Main { } 24 + } 25 + 26 + @external 27 + namespace sh.tangled.repo.pull.status.merged { 28 + @token model Main { } 29 + }
+35
packages/emitter/test/integration/lexicon-examples/output/community/lexicon/bookmarks/bookmark.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "community.lexicon.bookmarks.bookmark", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "Record bookmarking a link to come back to later.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": [ 12 + "subject", 13 + "createdAt" 14 + ], 15 + "properties": { 16 + "subject": { 17 + "type": "string", 18 + "format": "uri" 19 + }, 20 + "createdAt": { 21 + "type": "string", 22 + "format": "datetime" 23 + }, 24 + "tags": { 25 + "type": "array", 26 + "description": "Tags for content the bookmark may be related to, for example 'news' or 'funny videos'", 27 + "items": { 28 + "type": "string" 29 + } 30 + } 31 + } 32 + } 33 + } 34 + } 35 + }
+51
packages/emitter/test/integration/lexicon-examples/output/community/lexicon/bookmarks/getActorBookmarks.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "community.lexicon.bookmarks.getActorBookmarks", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a list of bookmarks by actor. Optionally add a list of tags to include, default will be all bookmarks. Requires auth, actor must be the requesting account.", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "tags": { 12 + "type": "array", 13 + "items": { 14 + "type": "string" 15 + } 16 + }, 17 + "limit": { 18 + "type": "integer", 19 + "minimum": 1, 20 + "maximum": 100, 21 + "default": 50 22 + }, 23 + "cursor": { 24 + "type": "string" 25 + } 26 + } 27 + }, 28 + "output": { 29 + "encoding": "application/json", 30 + "schema": { 31 + "type": "object", 32 + "required": [ 33 + "bookmarks" 34 + ], 35 + "properties": { 36 + "cursor": { 37 + "type": "string" 38 + }, 39 + "bookmarks": { 40 + "type": "array", 41 + "items": { 42 + "type": "ref", 43 + "ref": "community.lexicon.bookmarks.bookmark" 44 + } 45 + } 46 + } 47 + } 48 + } 49 + } 50 + } 51 + }
+146
packages/emitter/test/integration/lexicon-examples/output/community/lexicon/calendar/event.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "community.lexicon.calendar.event", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A calendar event.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": [ 12 + "name", 13 + "createdAt" 14 + ], 15 + "properties": { 16 + "name": { 17 + "type": "string", 18 + "description": "The name of the event." 19 + }, 20 + "description": { 21 + "type": "string", 22 + "description": "The description of the event." 23 + }, 24 + "createdAt": { 25 + "type": "string", 26 + "format": "datetime", 27 + "description": "Client-declared timestamp when the event was created." 28 + }, 29 + "startsAt": { 30 + "type": "string", 31 + "format": "datetime", 32 + "description": "Client-declared timestamp when the event starts." 33 + }, 34 + "endsAt": { 35 + "type": "string", 36 + "format": "datetime", 37 + "description": "Client-declared timestamp when the event ends." 38 + }, 39 + "mode": { 40 + "type": "ref", 41 + "ref": "#mode", 42 + "description": "The attendance mode of the event." 43 + }, 44 + "status": { 45 + "type": "ref", 46 + "ref": "#status", 47 + "description": "The status of the event." 48 + }, 49 + "locations": { 50 + "type": "array", 51 + "description": "The locations where the event takes place.", 52 + "items": { 53 + "type": "union", 54 + "refs": [ 55 + "#uri", 56 + "community.lexicon.location.address", 57 + "community.lexicon.location.fsq", 58 + "community.lexicon.location.geo", 59 + "community.lexicon.location.hthree" 60 + ] 61 + } 62 + }, 63 + "uris": { 64 + "type": "array", 65 + "description": "URIs associated with the event.", 66 + "items": { 67 + "type": "ref", 68 + "ref": "#uri" 69 + } 70 + } 71 + } 72 + } 73 + }, 74 + "mode": { 75 + "type": "string", 76 + "description": "The mode of the event.", 77 + "default": "community.lexicon.calendar.event#inperson", 78 + "knownValues": [ 79 + "community.lexicon.calendar.event#hybrid", 80 + "community.lexicon.calendar.event#inperson", 81 + "community.lexicon.calendar.event#virtual" 82 + ] 83 + }, 84 + "virtual": { 85 + "type": "token", 86 + "description": "A virtual event that takes place online." 87 + }, 88 + "inperson": { 89 + "type": "token", 90 + "description": "An in-person event that takes place offline." 91 + }, 92 + "hybrid": { 93 + "type": "token", 94 + "description": "A hybrid event that takes place both online and offline." 95 + }, 96 + "status": { 97 + "type": "string", 98 + "description": "The status of the event.", 99 + "default": "community.lexicon.calendar.event#scheduled", 100 + "knownValues": [ 101 + "community.lexicon.calendar.event#cancelled", 102 + "community.lexicon.calendar.event#planned", 103 + "community.lexicon.calendar.event#postponed", 104 + "community.lexicon.calendar.event#rescheduled", 105 + "community.lexicon.calendar.event#scheduled" 106 + ] 107 + }, 108 + "planned": { 109 + "type": "token", 110 + "description": "The event has been created, but not finalized." 111 + }, 112 + "scheduled": { 113 + "type": "token", 114 + "description": "The event has been created and scheduled." 115 + }, 116 + "rescheduled": { 117 + "type": "token", 118 + "description": "The event has been rescheduled." 119 + }, 120 + "cancelled": { 121 + "type": "token", 122 + "description": "The event has been cancelled." 123 + }, 124 + "postponed": { 125 + "type": "token", 126 + "description": "The event has been postponed and a new start date has not been set." 127 + }, 128 + "uri": { 129 + "type": "object", 130 + "description": "A URI associated with the event.", 131 + "required": [ 132 + "uri" 133 + ], 134 + "properties": { 135 + "uri": { 136 + "type": "string", 137 + "format": "uri" 138 + }, 139 + "name": { 140 + "type": "string", 141 + "description": "The display name of the URI." 142 + } 143 + } 144 + } 145 + } 146 + }
+45
packages/emitter/test/integration/lexicon-examples/output/community/lexicon/calendar/rsvp.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "community.lexicon.calendar.rsvp", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "An RSVP for an event.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": [ 12 + "subject", 13 + "status" 14 + ], 15 + "properties": { 16 + "subject": { 17 + "type": "ref", 18 + "ref": "com.atproto.repo.strongRef" 19 + }, 20 + "status": { 21 + "type": "string", 22 + "default": "community.lexicon.calendar.rsvp#going", 23 + "knownValues": [ 24 + "community.lexicon.calendar.rsvp#interested", 25 + "community.lexicon.calendar.rsvp#going", 26 + "community.lexicon.calendar.rsvp#notgoing" 27 + ] 28 + } 29 + } 30 + } 31 + }, 32 + "interested": { 33 + "type": "token", 34 + "description": "Interested in the event" 35 + }, 36 + "going": { 37 + "type": "token", 38 + "description": "Going to the event" 39 + }, 40 + "notgoing": { 41 + "type": "token", 42 + "description": "Not going to the event" 43 + } 44 + } 45 + }
+28
packages/emitter/test/integration/lexicon-examples/output/community/lexicon/interaction/like.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "community.lexicon.interaction.like", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A 'like' interaction with another AT Protocol record.", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": [ 12 + "subject", 13 + "createdAt" 14 + ], 15 + "properties": { 16 + "subject": { 17 + "type": "ref", 18 + "ref": "com.atproto.repo.strongRef" 19 + }, 20 + "createdAt": { 21 + "type": "string", 22 + "format": "datetime" 23 + } 24 + } 25 + } 26 + } 27 + } 28 + }
+41
packages/emitter/test/integration/lexicon-examples/output/community/lexicon/location/address.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "community.lexicon.location.address", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "description": "A physical location in the form of a street address.", 8 + "required": [ 9 + "country" 10 + ], 11 + "properties": { 12 + "country": { 13 + "type": "string", 14 + "description": "The ISO 3166 country code. Preferably the 2-letter code.", 15 + "minLength": 2, 16 + "maxLength": 10 17 + }, 18 + "postalCode": { 19 + "type": "string", 20 + "description": "The postal code of the location." 21 + }, 22 + "region": { 23 + "type": "string", 24 + "description": "The administrative region of the country. For example, a state in the USA." 25 + }, 26 + "locality": { 27 + "type": "string", 28 + "description": "The locality of the region. For example, a city in the USA." 29 + }, 30 + "street": { 31 + "type": "string", 32 + "description": "The street address." 33 + }, 34 + "name": { 35 + "type": "string", 36 + "description": "The name of the location." 37 + } 38 + } 39 + } 40 + } 41 + }
+29
packages/emitter/test/integration/lexicon-examples/output/community/lexicon/location/fsq.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "community.lexicon.location.fsq", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "description": "A physical location contained in the Foursquare Open Source Places dataset.", 8 + "required": [ 9 + "fsq_place_id" 10 + ], 11 + "properties": { 12 + "fsq_place_id": { 13 + "type": "string", 14 + "description": "The unique identifier of a Foursquare POI." 15 + }, 16 + "latitude": { 17 + "type": "string" 18 + }, 19 + "longitude": { 20 + "type": "string" 21 + }, 22 + "name": { 23 + "type": "string", 24 + "description": "The name of the location." 25 + } 26 + } 27 + } 28 + } 29 + }
+29
packages/emitter/test/integration/lexicon-examples/output/community/lexicon/location/geo.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "community.lexicon.location.geo", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "description": "A physical location in the form of a WGS84 coordinate.", 8 + "required": [ 9 + "latitude", 10 + "longitude" 11 + ], 12 + "properties": { 13 + "latitude": { 14 + "type": "string" 15 + }, 16 + "longitude": { 17 + "type": "string" 18 + }, 19 + "altitude": { 20 + "type": "string" 21 + }, 22 + "name": { 23 + "type": "string", 24 + "description": "The name of the location." 25 + } 26 + } 27 + } 28 + } 29 + }
+23
packages/emitter/test/integration/lexicon-examples/output/community/lexicon/location/hthree.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "community.lexicon.location.hthree", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "description": "A physical location in the form of a H3 encoded location.", 8 + "required": [ 9 + "value" 10 + ], 11 + "properties": { 12 + "value": { 13 + "type": "string", 14 + "description": "The h3 encoded location." 15 + }, 16 + "name": { 17 + "type": "string", 18 + "description": "The name of the location." 19 + } 20 + } 21 + } 22 + } 23 + }
+27
packages/emitter/test/integration/lexicon-examples/output/community/lexicon/payments/webMonetization.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "community.lexicon.payments.webMonetization", 4 + "description": "Web Monetization integration: https://webmonetization.org/", 5 + "defs": { 6 + "main": { 7 + "type": "record", 8 + "description": "Web Monetization wallet.", 9 + "key": "any", 10 + "record": { 11 + "type": "object", 12 + "required": ["address"], 13 + "properties": { 14 + "address": { 15 + "type": "string", 16 + "format": "uri", 17 + "description": "Wallet address." 18 + }, 19 + "note": { 20 + "type": "string", 21 + "description": "Short, human-readable description of how this wallet is related to this account." 22 + } 23 + } 24 + } 25 + } 26 + } 27 + }
+4 -18
packages/emitter/test/integration.test.ts
··· 54 54 path.join(scenario, "output"), 55 55 ); 56 56 57 - // Compile all inputs together (for cross-references) 58 - const tspFiles = Object.keys(inputFiles).filter((f) => 59 - f.endsWith(".tsp"), 60 - ); 61 - let emitResult: EmitResult; 62 - 63 - if (tspFiles.length > 0) { 64 - // Create a virtual main.tsp that imports all other files 65 - const mainContent = 66 - 'import "@typelex/emitter";\n' + 67 - tspFiles.map((f) => `import "./${normalizePathToPosix(f)}";`).join("\n"); 68 - const filesWithMain = { ...inputFiles, "main.tsp": mainContent }; 69 - emitResult = await doEmit(filesWithMain, "main.tsp"); 70 - } else { 71 - emitResult = { files: {}, diagnostics: [], inputFiles: {} }; 72 - } 73 - 74 57 // Generate a test for each expected output 75 58 for (const expectedPath of Object.keys(expectedFiles)) { 76 59 if (!expectedPath.endsWith(".json")) continue; ··· 80 63 const hasInput = Object.keys(inputFiles).includes(inputPath); 81 64 82 65 if (hasInput) { 83 - it(`should emit ${expectedPath}`, function () { 66 + it(`should emit ${expectedPath}`, async function () { 67 + // Compile each file in isolation 68 + const emitResult = await doEmit({ [inputPath]: inputFiles[inputPath] }, inputPath); 69 + 84 70 // Check for compilation errors 85 71 if (emitResult.diagnostics.length > 0) { 86 72 const formattedDiagnostics = emitResult.diagnostics.map((diag) =>
+30
packages/emitter/test/spec/basic/input/com/example/scalarDefaults.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace com.example.scalarDefaults { 4 + /** Test default decorator on scalars */ 5 + model Main { 6 + /** Uses string scalar with default */ 7 + mode?: Mode; 8 + 9 + /** Uses integer scalar with default */ 10 + limit?: Limit; 11 + 12 + /** Uses boolean scalar with default */ 13 + enabled?: Enabled; 14 + } 15 + 16 + /** A string type with a default value */ 17 + @default("standard") 18 + @maxLength(50) 19 + scalar Mode extends string; 20 + 21 + /** An integer type with a default value */ 22 + @default(50) 23 + @minValue(1) 24 + @maxValue(100) 25 + scalar Limit extends integer; 26 + 27 + /** A boolean type with a default value */ 28 + @default(true) 29 + scalar Enabled extends boolean; 30 + }
+22
packages/emitter/test/spec/basic/input/com/example/scalarDefs.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace com.example.scalarDefs { 4 + /** Scalar defs should create standalone defs like models and unions */ 5 + model Main { 6 + /** Uses a custom string scalar with constraints */ 7 + tag?: Tag; 8 + 9 + /** Uses a custom integer scalar with constraints */ 10 + count?: Count; 11 + } 12 + 13 + /** A custom string type with length constraints */ 14 + @maxLength(100) 15 + @maxGraphemes(50) 16 + scalar Tag extends string; 17 + 18 + /** A custom integer type with value constraints */ 19 + @minValue(1) 20 + @maxValue(100) 21 + scalar Count extends integer; 22 + }
+22
packages/emitter/test/spec/basic/input/com/example/scalarInline.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace com.example.scalarInline { 4 + /** Test inline decorator on scalars */ 5 + model Main { 6 + /** Inline scalar - should not create a def */ 7 + tag?: Tag; 8 + 9 + /** Non-inline scalar - should create a def */ 10 + category?: Category; 11 + } 12 + 13 + /** An inline scalar should be inlined at usage sites */ 14 + @inline 15 + @maxLength(50) 16 + @maxGraphemes(25) 17 + scalar Tag extends string; 18 + 19 + /** A regular scalar should create a standalone def */ 20 + @maxLength(100) 21 + scalar Category extends string; 22 + }
+53
packages/emitter/test/spec/basic/input/com/example/unionDefaults.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace com.example.unionDefaults { 4 + /** Test default decorator on unions */ 5 + model Main { 6 + /** Union with token refs and default */ 7 + eventMode?: EventMode; 8 + 9 + /** Union with string literals and default */ 10 + sortOrder?: SortOrder; 11 + 12 + /** Union with integer literals and default */ 13 + priority?: Priority; 14 + } 15 + 16 + /** Union of tokens with default pointing to a token */ 17 + @default(Inperson) 18 + union EventMode { 19 + Hybrid, 20 + Inperson, 21 + Virtual, 22 + string, 23 + } 24 + 25 + /** A hybrid event */ 26 + @token 27 + model Hybrid {} 28 + 29 + /** An in-person event */ 30 + @token 31 + model Inperson {} 32 + 33 + /** A virtual event */ 34 + @token 35 + model Virtual {} 36 + 37 + /** Union of string literals with default */ 38 + @default("asc") 39 + union SortOrder { 40 + "asc", 41 + "desc", 42 + string, 43 + } 44 + 45 + /** Union of integer literals with default (closed enum) */ 46 + @default(1) 47 + @closed 48 + union Priority { 49 + 1, 50 + 2, 51 + 3, 52 + } 53 + }
+21
packages/emitter/test/spec/basic/output/com/example/other.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.example.other", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": {} 8 + }, 9 + "someDef": { 10 + "type": "object", 11 + "required": [ 12 + "value" 13 + ], 14 + "properties": { 15 + "value": { 16 + "type": "string" 17 + } 18 + } 19 + } 20 + } 21 + }
+45
packages/emitter/test/spec/basic/output/com/example/scalarDefaults.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.example.scalarDefaults", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": { 8 + "mode": { 9 + "type": "ref", 10 + "ref": "#mode", 11 + "description": "Uses string scalar with default" 12 + }, 13 + "limit": { 14 + "type": "ref", 15 + "ref": "#limit", 16 + "description": "Uses integer scalar with default" 17 + }, 18 + "enabled": { 19 + "type": "ref", 20 + "ref": "#enabled", 21 + "description": "Uses boolean scalar with default" 22 + } 23 + }, 24 + "description": "Test default decorator on scalars" 25 + }, 26 + "mode": { 27 + "type": "string", 28 + "maxLength": 50, 29 + "default": "standard", 30 + "description": "A string type with a default value" 31 + }, 32 + "limit": { 33 + "type": "integer", 34 + "minimum": 1, 35 + "maximum": 100, 36 + "default": 50, 37 + "description": "An integer type with a default value" 38 + }, 39 + "enabled": { 40 + "type": "boolean", 41 + "default": true, 42 + "description": "A boolean type with a default value" 43 + } 44 + } 45 + }
+34
packages/emitter/test/spec/basic/output/com/example/scalarDefs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.example.scalarDefs", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": { 8 + "tag": { 9 + "type": "ref", 10 + "ref": "#tag", 11 + "description": "Uses a custom string scalar with constraints" 12 + }, 13 + "count": { 14 + "type": "ref", 15 + "ref": "#count", 16 + "description": "Uses a custom integer scalar with constraints" 17 + } 18 + }, 19 + "description": "Scalar defs should create standalone defs like models and unions" 20 + }, 21 + "tag": { 22 + "type": "string", 23 + "maxLength": 100, 24 + "maxGraphemes": 50, 25 + "description": "A custom string type with length constraints" 26 + }, 27 + "count": { 28 + "type": "integer", 29 + "minimum": 1, 30 + "maximum": 100, 31 + "description": "A custom integer type with value constraints" 32 + } 33 + } 34 + }
+28
packages/emitter/test/spec/basic/output/com/example/scalarInline.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.example.scalarInline", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": { 8 + "tag": { 9 + "type": "string", 10 + "maxLength": 50, 11 + "maxGraphemes": 25, 12 + "description": "Inline scalar - should not create a def" 13 + }, 14 + "category": { 15 + "type": "ref", 16 + "ref": "#category", 17 + "description": "Non-inline scalar - should create a def" 18 + } 19 + }, 20 + "description": "Test inline decorator on scalars" 21 + }, 22 + "category": { 23 + "type": "string", 24 + "maxLength": 100, 25 + "description": "A regular scalar should create a standalone def" 26 + } 27 + } 28 + }
+61
packages/emitter/test/spec/basic/output/com/example/unionDefaults.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.example.unionDefaults", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": { 8 + "eventMode": { 9 + "type": "ref", 10 + "ref": "#eventMode", 11 + "description": "Union with token refs and default" 12 + }, 13 + "sortOrder": { 14 + "type": "ref", 15 + "ref": "#sortOrder", 16 + "description": "Union with string literals and default" 17 + }, 18 + "priority": { 19 + "type": "ref", 20 + "ref": "#priority", 21 + "description": "Union with integer literals and default" 22 + } 23 + }, 24 + "description": "Test default decorator on unions" 25 + }, 26 + "eventMode": { 27 + "type": "string", 28 + "knownValues": [ 29 + "com.example.unionDefaults#hybrid", 30 + "com.example.unionDefaults#inperson", 31 + "com.example.unionDefaults#virtual" 32 + ], 33 + "default": "com.example.unionDefaults#inperson", 34 + "description": "Union of tokens with default pointing to a token" 35 + }, 36 + "hybrid": { 37 + "type": "token", 38 + "description": "A hybrid event" 39 + }, 40 + "inperson": { 41 + "type": "token", 42 + "description": "An in-person event" 43 + }, 44 + "virtual": { 45 + "type": "token", 46 + "description": "A virtual event" 47 + }, 48 + "sortOrder": { 49 + "type": "string", 50 + "knownValues": ["asc", "desc"], 51 + "default": "asc", 52 + "description": "Union of string literals with default" 53 + }, 54 + "priority": { 55 + "type": "integer", 56 + "enum": [1, 2, 3], 57 + "default": 1, 58 + "description": "Union of integer literals with default (closed enum)" 59 + } 60 + } 61 + }
+8
packages/emitter/test/spec/external/input/test/external.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + @external 4 + namespace test.external { 5 + model Main { } 6 + 7 + model AlsoNotEmitted { } 8 + }
+7
packages/emitter/test/spec/external/input/test/normal.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + namespace test.normal { 4 + model Main { 5 + name?: string; 6 + } 7 + }
+14
packages/emitter/test/spec/external/output/test/normal.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "test.normal", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "properties": { 8 + "name": { 9 + "type": "string" 10 + } 11 + } 12 + } 13 + } 14 + }
+23 -1
packages/emitter/test/spec.test.ts
··· 106 106 assert.deepStrictEqual(actual, expected); 107 107 }); 108 108 } else { 109 - it.skip(`TODO: ${expectedPath} (add ${inputPath})`, function () {}); 109 + it(`should emit ${expectedPath}`, function () { 110 + assert.fail( 111 + `Expected output file ${expectedPath} has no corresponding input file ${inputPath}. ` + 112 + `Either add the input file or remove the expected output.` 113 + ); 114 + }); 110 115 } 111 116 } 117 + 118 + // Check for unexpected emitted files 119 + it("should not emit unexpected files", function () { 120 + const emittedFiles = Object.keys(emitResult.files).filter(f => f.endsWith(".json")); 121 + const expectedPaths = Object.keys(expectedFiles) 122 + .filter(f => f.endsWith(".json")) 123 + .map(normalizePathToPosix); 124 + 125 + const unexpected = emittedFiles.filter(f => !expectedPaths.includes(f)); 126 + 127 + if (unexpected.length > 0) { 128 + assert.fail( 129 + `Unexpected files were emitted: ${unexpected.join(", ")}. ` + 130 + `Either add expected output files or ensure these should not be emitted.` 131 + ); 132 + } 133 + }); 112 134 }); 113 135 } 114 136 });
-7
packages/example/.gitignore
··· 1 1 # Dependencies 2 2 node_modules/ 3 3 4 - # Generated lexicons 5 - lexicon/ 6 - 7 - # TypeSpec output (if using default config) 8 - tsp-output/ 9 - generated/ 10 - 11 4 # OS 12 5 .DS_Store 13 6
+31
packages/example/lexicons/app/bsky/actor/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.actor.defs", 4 + "defs": { 5 + "profileView": { 6 + "type": "object", 7 + "required": ["did", "handle"], 8 + "properties": { 9 + "did": { "type": "string", "format": "did" }, 10 + "handle": { "type": "string", "format": "handle" }, 11 + "displayName": { 12 + "type": "string", 13 + "maxGraphemes": 64, 14 + "maxLength": 640 15 + }, 16 + "description": { 17 + "type": "string", 18 + "maxGraphemes": 256, 19 + "maxLength": 2560 20 + }, 21 + "avatar": { "type": "string", "format": "uri" }, 22 + "indexedAt": { "type": "string", "format": "datetime" }, 23 + "createdAt": { "type": "string", "format": "datetime" }, 24 + "labels": { 25 + "type": "array", 26 + "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } 27 + } 28 + } 29 + } 30 + } 31 + }
+53
packages/example/lexicons/app/bsky/actor/profile.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.actor.profile", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A declaration of a Bluesky account profile.", 8 + "key": "literal:self", 9 + "record": { 10 + "type": "object", 11 + "properties": { 12 + "displayName": { 13 + "type": "string", 14 + "maxGraphemes": 64, 15 + "maxLength": 640 16 + }, 17 + "description": { 18 + "type": "string", 19 + "description": "Free-form profile description text.", 20 + "maxGraphemes": 256, 21 + "maxLength": 2560 22 + }, 23 + "avatar": { 24 + "type": "blob", 25 + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'", 26 + "accept": ["image/png", "image/jpeg"], 27 + "maxSize": 1000000 28 + }, 29 + "banner": { 30 + "type": "blob", 31 + "description": "Larger horizontal image to display behind profile view.", 32 + "accept": ["image/png", "image/jpeg"], 33 + "maxSize": 1000000 34 + }, 35 + "labels": { 36 + "type": "union", 37 + "description": "Self-label values, specific to the Bluesky application, on the overall account.", 38 + "refs": ["com.atproto.label.defs#selfLabels"] 39 + }, 40 + "joinedViaStarterPack": { 41 + "type": "ref", 42 + "ref": "com.atproto.repo.strongRef" 43 + }, 44 + "pinnedPost": { 45 + "type": "ref", 46 + "ref": "com.atproto.repo.strongRef" 47 + }, 48 + "createdAt": { "type": "string", "format": "datetime" } 49 + } 50 + } 51 + } 52 + } 53 + }
+156
packages/example/lexicons/com/atproto/label/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.label.defs", 4 + "defs": { 5 + "label": { 6 + "type": "object", 7 + "description": "Metadata tag on an atproto resource (eg, repo or record).", 8 + "required": ["src", "uri", "val", "cts"], 9 + "properties": { 10 + "ver": { 11 + "type": "integer", 12 + "description": "The AT Protocol version of the label object." 13 + }, 14 + "src": { 15 + "type": "string", 16 + "format": "did", 17 + "description": "DID of the actor who created this label." 18 + }, 19 + "uri": { 20 + "type": "string", 21 + "format": "uri", 22 + "description": "AT URI of the record, repository (account), or other resource that this label applies to." 23 + }, 24 + "cid": { 25 + "type": "string", 26 + "format": "cid", 27 + "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to." 28 + }, 29 + "val": { 30 + "type": "string", 31 + "maxLength": 128, 32 + "description": "The short string name of the value or type of this label." 33 + }, 34 + "neg": { 35 + "type": "boolean", 36 + "description": "If true, this is a negation label, overwriting a previous label." 37 + }, 38 + "cts": { 39 + "type": "string", 40 + "format": "datetime", 41 + "description": "Timestamp when this label was created." 42 + }, 43 + "exp": { 44 + "type": "string", 45 + "format": "datetime", 46 + "description": "Timestamp at which this label expires (no longer applies)." 47 + }, 48 + "sig": { 49 + "type": "bytes", 50 + "description": "Signature of dag-cbor encoded label." 51 + } 52 + } 53 + }, 54 + "selfLabels": { 55 + "type": "object", 56 + "description": "Metadata tags on an atproto record, published by the author within the record.", 57 + "required": ["values"], 58 + "properties": { 59 + "values": { 60 + "type": "array", 61 + "items": { "type": "ref", "ref": "#selfLabel" }, 62 + "maxLength": 10 63 + } 64 + } 65 + }, 66 + "selfLabel": { 67 + "type": "object", 68 + "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.", 69 + "required": ["val"], 70 + "properties": { 71 + "val": { 72 + "type": "string", 73 + "maxLength": 128, 74 + "description": "The short string name of the value or type of this label." 75 + } 76 + } 77 + }, 78 + "labelValueDefinition": { 79 + "type": "object", 80 + "description": "Declares a label value and its expected interpretations and behaviors.", 81 + "required": ["identifier", "severity", "blurs", "locales"], 82 + "properties": { 83 + "identifier": { 84 + "type": "string", 85 + "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", 86 + "maxLength": 100, 87 + "maxGraphemes": 100 88 + }, 89 + "severity": { 90 + "type": "string", 91 + "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", 92 + "knownValues": ["inform", "alert", "none"] 93 + }, 94 + "blurs": { 95 + "type": "string", 96 + "description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", 97 + "knownValues": ["content", "media", "none"] 98 + }, 99 + "defaultSetting": { 100 + "type": "string", 101 + "description": "The default setting for this label.", 102 + "knownValues": ["ignore", "warn", "hide"], 103 + "default": "warn" 104 + }, 105 + "adultOnly": { 106 + "type": "boolean", 107 + "description": "Does the user need to have adult content enabled in order to configure this label?" 108 + }, 109 + "locales": { 110 + "type": "array", 111 + "items": { "type": "ref", "ref": "#labelValueDefinitionStrings" } 112 + } 113 + } 114 + }, 115 + "labelValueDefinitionStrings": { 116 + "type": "object", 117 + "description": "Strings which describe the label in the UI, localized into a specific language.", 118 + "required": ["lang", "name", "description"], 119 + "properties": { 120 + "lang": { 121 + "type": "string", 122 + "description": "The code of the language these strings are written in.", 123 + "format": "language" 124 + }, 125 + "name": { 126 + "type": "string", 127 + "description": "A short human-readable name for the label.", 128 + "maxGraphemes": 64, 129 + "maxLength": 640 130 + }, 131 + "description": { 132 + "type": "string", 133 + "description": "A longer description of what the label means and why it might be applied.", 134 + "maxGraphemes": 10000, 135 + "maxLength": 100000 136 + } 137 + } 138 + }, 139 + "labelValue": { 140 + "type": "string", 141 + "knownValues": [ 142 + "!hide", 143 + "!no-promote", 144 + "!warn", 145 + "!no-unauthenticated", 146 + "dmca-violation", 147 + "doxxing", 148 + "porn", 149 + "sexual", 150 + "nudity", 151 + "nsfl", 152 + "gore" 153 + ] 154 + } 155 + } 156 + }
+131
packages/example/lexicons/com/atproto/repo/applyWrites.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.applyWrites", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["repo", "writes"], 13 + "properties": { 14 + "repo": { 15 + "type": "string", 16 + "format": "at-identifier", 17 + "description": "The handle or DID of the repo (aka, current account)." 18 + }, 19 + "validate": { 20 + "type": "boolean", 21 + "description": "Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons." 22 + }, 23 + "writes": { 24 + "type": "array", 25 + "items": { 26 + "type": "union", 27 + "refs": ["#create", "#update", "#delete"], 28 + "closed": true 29 + } 30 + }, 31 + "swapCommit": { 32 + "type": "string", 33 + "description": "If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.", 34 + "format": "cid" 35 + } 36 + } 37 + } 38 + }, 39 + "output": { 40 + "encoding": "application/json", 41 + "schema": { 42 + "type": "object", 43 + "required": [], 44 + "properties": { 45 + "commit": { 46 + "type": "ref", 47 + "ref": "com.atproto.repo.defs#commitMeta" 48 + }, 49 + "results": { 50 + "type": "array", 51 + "items": { 52 + "type": "union", 53 + "refs": ["#createResult", "#updateResult", "#deleteResult"], 54 + "closed": true 55 + } 56 + } 57 + } 58 + } 59 + }, 60 + "errors": [ 61 + { 62 + "name": "InvalidSwap", 63 + "description": "Indicates that the 'swapCommit' parameter did not match current commit." 64 + } 65 + ] 66 + }, 67 + "create": { 68 + "type": "object", 69 + "description": "Operation which creates a new record.", 70 + "required": ["collection", "value"], 71 + "properties": { 72 + "collection": { "type": "string", "format": "nsid" }, 73 + "rkey": { 74 + "type": "string", 75 + "maxLength": 512, 76 + "format": "record-key", 77 + "description": "NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility." 78 + }, 79 + "value": { "type": "unknown" } 80 + } 81 + }, 82 + "update": { 83 + "type": "object", 84 + "description": "Operation which updates an existing record.", 85 + "required": ["collection", "rkey", "value"], 86 + "properties": { 87 + "collection": { "type": "string", "format": "nsid" }, 88 + "rkey": { "type": "string", "format": "record-key" }, 89 + "value": { "type": "unknown" } 90 + } 91 + }, 92 + "delete": { 93 + "type": "object", 94 + "description": "Operation which deletes an existing record.", 95 + "required": ["collection", "rkey"], 96 + "properties": { 97 + "collection": { "type": "string", "format": "nsid" }, 98 + "rkey": { "type": "string", "format": "record-key" } 99 + } 100 + }, 101 + "createResult": { 102 + "type": "object", 103 + "required": ["uri", "cid"], 104 + "properties": { 105 + "uri": { "type": "string", "format": "at-uri" }, 106 + "cid": { "type": "string", "format": "cid" }, 107 + "validationStatus": { 108 + "type": "string", 109 + "knownValues": ["valid", "unknown"] 110 + } 111 + } 112 + }, 113 + "updateResult": { 114 + "type": "object", 115 + "required": ["uri", "cid"], 116 + "properties": { 117 + "uri": { "type": "string", "format": "at-uri" }, 118 + "cid": { "type": "string", "format": "cid" }, 119 + "validationStatus": { 120 + "type": "string", 121 + "knownValues": ["valid", "unknown"] 122 + } 123 + } 124 + }, 125 + "deleteResult": { 126 + "type": "object", 127 + "required": [], 128 + "properties": {} 129 + } 130 + } 131 + }
+73
packages/example/lexicons/com/atproto/repo/createRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.createRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Create a single new repository record. Requires auth, implemented by PDS.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["repo", "collection", "record"], 13 + "properties": { 14 + "repo": { 15 + "type": "string", 16 + "format": "at-identifier", 17 + "description": "The handle or DID of the repo (aka, current account)." 18 + }, 19 + "collection": { 20 + "type": "string", 21 + "format": "nsid", 22 + "description": "The NSID of the record collection." 23 + }, 24 + "rkey": { 25 + "type": "string", 26 + "format": "record-key", 27 + "description": "The Record Key.", 28 + "maxLength": 512 29 + }, 30 + "validate": { 31 + "type": "boolean", 32 + "description": "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons." 33 + }, 34 + "record": { 35 + "type": "unknown", 36 + "description": "The record itself. Must contain a $type field." 37 + }, 38 + "swapCommit": { 39 + "type": "string", 40 + "format": "cid", 41 + "description": "Compare and swap with the previous commit by CID." 42 + } 43 + } 44 + } 45 + }, 46 + "output": { 47 + "encoding": "application/json", 48 + "schema": { 49 + "type": "object", 50 + "required": ["uri", "cid"], 51 + "properties": { 52 + "uri": { "type": "string", "format": "at-uri" }, 53 + "cid": { "type": "string", "format": "cid" }, 54 + "commit": { 55 + "type": "ref", 56 + "ref": "com.atproto.repo.defs#commitMeta" 57 + }, 58 + "validationStatus": { 59 + "type": "string", 60 + "knownValues": ["valid", "unknown"] 61 + } 62 + } 63 + } 64 + }, 65 + "errors": [ 66 + { 67 + "name": "InvalidSwap", 68 + "description": "Indicates that 'swapCommit' didn't match current repo commit." 69 + } 70 + ] 71 + } 72 + } 73 + }
+14
packages/example/lexicons/com/atproto/repo/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.defs", 4 + "defs": { 5 + "commitMeta": { 6 + "type": "object", 7 + "required": ["cid", "rev"], 8 + "properties": { 9 + "cid": { "type": "string", "format": "cid" }, 10 + "rev": { "type": "string", "format": "tid" } 11 + } 12 + } 13 + } 14 + }
+57
packages/example/lexicons/com/atproto/repo/deleteRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.deleteRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["repo", "collection", "rkey"], 13 + "properties": { 14 + "repo": { 15 + "type": "string", 16 + "format": "at-identifier", 17 + "description": "The handle or DID of the repo (aka, current account)." 18 + }, 19 + "collection": { 20 + "type": "string", 21 + "format": "nsid", 22 + "description": "The NSID of the record collection." 23 + }, 24 + "rkey": { 25 + "type": "string", 26 + "format": "record-key", 27 + "description": "The Record Key." 28 + }, 29 + "swapRecord": { 30 + "type": "string", 31 + "format": "cid", 32 + "description": "Compare and swap with the previous record by CID." 33 + }, 34 + "swapCommit": { 35 + "type": "string", 36 + "format": "cid", 37 + "description": "Compare and swap with the previous commit by CID." 38 + } 39 + } 40 + } 41 + }, 42 + "output": { 43 + "encoding": "application/json", 44 + "schema": { 45 + "type": "object", 46 + "properties": { 47 + "commit": { 48 + "type": "ref", 49 + "ref": "com.atproto.repo.defs#commitMeta" 50 + } 51 + } 52 + } 53 + }, 54 + "errors": [{ "name": "InvalidSwap" }] 55 + } 56 + } 57 + }
+51
packages/example/lexicons/com/atproto/repo/describeRepo.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.describeRepo", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get information about an account and repository, including the list of collections. Does not require auth.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["repo"], 11 + "properties": { 12 + "repo": { 13 + "type": "string", 14 + "format": "at-identifier", 15 + "description": "The handle or DID of the repo." 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "required": [ 24 + "handle", 25 + "did", 26 + "didDoc", 27 + "collections", 28 + "handleIsCorrect" 29 + ], 30 + "properties": { 31 + "handle": { "type": "string", "format": "handle" }, 32 + "did": { "type": "string", "format": "did" }, 33 + "didDoc": { 34 + "type": "unknown", 35 + "description": "The complete DID document for this account." 36 + }, 37 + "collections": { 38 + "type": "array", 39 + "description": "List of all the collections (NSIDs) for which this repo contains at least one record.", 40 + "items": { "type": "string", "format": "nsid" } 41 + }, 42 + "handleIsCorrect": { 43 + "type": "boolean", 44 + "description": "Indicates if handle is currently valid (resolves bi-directionally)" 45 + } 46 + } 47 + } 48 + } 49 + } 50 + } 51 + }
+49
packages/example/lexicons/com/atproto/repo/getRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.getRecord", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a single record from a repository. Does not require auth.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["repo", "collection", "rkey"], 11 + "properties": { 12 + "repo": { 13 + "type": "string", 14 + "format": "at-identifier", 15 + "description": "The handle or DID of the repo." 16 + }, 17 + "collection": { 18 + "type": "string", 19 + "format": "nsid", 20 + "description": "The NSID of the record collection." 21 + }, 22 + "rkey": { 23 + "type": "string", 24 + "description": "The Record Key.", 25 + "format": "record-key" 26 + }, 27 + "cid": { 28 + "type": "string", 29 + "format": "cid", 30 + "description": "The CID of the version of the record. If not specified, then return the most recent version." 31 + } 32 + } 33 + }, 34 + "output": { 35 + "encoding": "application/json", 36 + "schema": { 37 + "type": "object", 38 + "required": ["uri", "value"], 39 + "properties": { 40 + "uri": { "type": "string", "format": "at-uri" }, 41 + "cid": { "type": "string", "format": "cid" }, 42 + "value": { "type": "unknown" } 43 + } 44 + } 45 + }, 46 + "errors": [{ "name": "RecordNotFound" }] 47 + } 48 + } 49 + }
+13
packages/example/lexicons/com/atproto/repo/importRepo.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.importRepo", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.", 8 + "input": { 9 + "encoding": "application/vnd.ipld.car" 10 + } 11 + } 12 + } 13 + }
+44
packages/example/lexicons/com/atproto/repo/listMissingBlobs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.listMissingBlobs", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "limit": { 12 + "type": "integer", 13 + "minimum": 1, 14 + "maximum": 1000, 15 + "default": 500 16 + }, 17 + "cursor": { "type": "string" } 18 + } 19 + }, 20 + "output": { 21 + "encoding": "application/json", 22 + "schema": { 23 + "type": "object", 24 + "required": ["blobs"], 25 + "properties": { 26 + "cursor": { "type": "string" }, 27 + "blobs": { 28 + "type": "array", 29 + "items": { "type": "ref", "ref": "#recordBlob" } 30 + } 31 + } 32 + } 33 + } 34 + }, 35 + "recordBlob": { 36 + "type": "object", 37 + "required": ["cid", "recordUri"], 38 + "properties": { 39 + "cid": { "type": "string", "format": "cid" }, 40 + "recordUri": { "type": "string", "format": "at-uri" } 41 + } 42 + } 43 + } 44 + }
+69
packages/example/lexicons/com/atproto/repo/listRecords.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.listRecords", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "List a range of records in a repository, matching a specific collection. Does not require auth.", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["repo", "collection"], 11 + "properties": { 12 + "repo": { 13 + "type": "string", 14 + "format": "at-identifier", 15 + "description": "The handle or DID of the repo." 16 + }, 17 + "collection": { 18 + "type": "string", 19 + "format": "nsid", 20 + "description": "The NSID of the record type." 21 + }, 22 + "limit": { 23 + "type": "integer", 24 + "minimum": 1, 25 + "maximum": 100, 26 + "default": 50, 27 + "description": "The number of records to return." 28 + }, 29 + "cursor": { "type": "string" }, 30 + "rkeyStart": { 31 + "type": "string", 32 + "description": "DEPRECATED: The lowest sort-ordered rkey to start from (exclusive)" 33 + }, 34 + "rkeyEnd": { 35 + "type": "string", 36 + "description": "DEPRECATED: The highest sort-ordered rkey to stop at (exclusive)" 37 + }, 38 + "reverse": { 39 + "type": "boolean", 40 + "description": "Flag to reverse the order of the returned records." 41 + } 42 + } 43 + }, 44 + "output": { 45 + "encoding": "application/json", 46 + "schema": { 47 + "type": "object", 48 + "required": ["records"], 49 + "properties": { 50 + "cursor": { "type": "string" }, 51 + "records": { 52 + "type": "array", 53 + "items": { "type": "ref", "ref": "#record" } 54 + } 55 + } 56 + } 57 + } 58 + }, 59 + "record": { 60 + "type": "object", 61 + "required": ["uri", "cid", "value"], 62 + "properties": { 63 + "uri": { "type": "string", "format": "at-uri" }, 64 + "cid": { "type": "string", "format": "cid" }, 65 + "value": { "type": "unknown" } 66 + } 67 + } 68 + } 69 + }
+74
packages/example/lexicons/com/atproto/repo/putRecord.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.putRecord", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "required": ["repo", "collection", "rkey", "record"], 13 + "nullable": ["swapRecord"], 14 + "properties": { 15 + "repo": { 16 + "type": "string", 17 + "format": "at-identifier", 18 + "description": "The handle or DID of the repo (aka, current account)." 19 + }, 20 + "collection": { 21 + "type": "string", 22 + "format": "nsid", 23 + "description": "The NSID of the record collection." 24 + }, 25 + "rkey": { 26 + "type": "string", 27 + "format": "record-key", 28 + "description": "The Record Key.", 29 + "maxLength": 512 30 + }, 31 + "validate": { 32 + "type": "boolean", 33 + "description": "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons." 34 + }, 35 + "record": { 36 + "type": "unknown", 37 + "description": "The record to write." 38 + }, 39 + "swapRecord": { 40 + "type": "string", 41 + "format": "cid", 42 + "description": "Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation" 43 + }, 44 + "swapCommit": { 45 + "type": "string", 46 + "format": "cid", 47 + "description": "Compare and swap with the previous commit by CID." 48 + } 49 + } 50 + } 51 + }, 52 + "output": { 53 + "encoding": "application/json", 54 + "schema": { 55 + "type": "object", 56 + "required": ["uri", "cid"], 57 + "properties": { 58 + "uri": { "type": "string", "format": "at-uri" }, 59 + "cid": { "type": "string", "format": "cid" }, 60 + "commit": { 61 + "type": "ref", 62 + "ref": "com.atproto.repo.defs#commitMeta" 63 + }, 64 + "validationStatus": { 65 + "type": "string", 66 + "knownValues": ["valid", "unknown"] 67 + } 68 + } 69 + } 70 + }, 71 + "errors": [{ "name": "InvalidSwap" }] 72 + } 73 + } 74 + }
+15
packages/example/lexicons/com/atproto/repo/strongRef.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.strongRef", 4 + "description": "A URI with a content-hash fingerprint.", 5 + "defs": { 6 + "main": { 7 + "type": "object", 8 + "required": ["uri", "cid"], 9 + "properties": { 10 + "uri": { "type": "string", "format": "at-uri" }, 11 + "cid": { "type": "string", "format": "cid" } 12 + } 13 + } 14 + } 15 + }
+23
packages/example/lexicons/com/atproto/repo/uploadBlob.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.repo.uploadBlob", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.", 8 + "input": { 9 + "encoding": "*/*" 10 + }, 11 + "output": { 12 + "encoding": "application/json", 13 + "schema": { 14 + "type": "object", 15 + "required": ["blob"], 16 + "properties": { 17 + "blob": { "type": "blob" } 18 + } 19 + } 20 + } 21 + } 22 + } 23 + }
+52
packages/example/lexicons/xyz/statusphere/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "xyz.statusphere.defs", 4 + "defs": { 5 + "statusView": { 6 + "type": "object", 7 + "properties": { 8 + "uri": { 9 + "type": "string", 10 + "format": "at-uri" 11 + }, 12 + "status": { 13 + "type": "string", 14 + "maxLength": 32, 15 + "minLength": 1, 16 + "maxGraphemes": 1 17 + }, 18 + "createdAt": { 19 + "type": "string", 20 + "format": "datetime" 21 + }, 22 + "profile": { 23 + "type": "ref", 24 + "ref": "#profileView" 25 + } 26 + }, 27 + "required": [ 28 + "uri", 29 + "status", 30 + "createdAt", 31 + "profile" 32 + ] 33 + }, 34 + "profileView": { 35 + "type": "object", 36 + "properties": { 37 + "did": { 38 + "type": "string", 39 + "format": "did" 40 + }, 41 + "handle": { 42 + "type": "string", 43 + "format": "handle" 44 + } 45 + }, 46 + "required": [ 47 + "did", 48 + "handle" 49 + ] 50 + } 51 + } 52 + }
+39
packages/example/lexicons/xyz/statusphere/getStatuses.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "xyz.statusphere.getStatuses", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get a list of the most recent statuses on the network.", 8 + "parameters": { 9 + "type": "params", 10 + "properties": { 11 + "limit": { 12 + "type": "integer", 13 + "minimum": 1, 14 + "maximum": 100, 15 + "default": 50 16 + } 17 + } 18 + }, 19 + "output": { 20 + "encoding": "application/json", 21 + "schema": { 22 + "type": "object", 23 + "properties": { 24 + "statuses": { 25 + "type": "array", 26 + "items": { 27 + "type": "ref", 28 + "ref": "xyz.statusphere.defs#statusView" 29 + } 30 + } 31 + }, 32 + "required": [ 33 + "statuses" 34 + ] 35 + } 36 + } 37 + } 38 + } 39 + }
+29
packages/example/lexicons/xyz/statusphere/getUser.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "xyz.statusphere.getUser", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get the current user's profile and status.", 8 + "output": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "profile": { 14 + "type": "ref", 15 + "ref": "app.bsky.actor.defs#profileView" 16 + }, 17 + "status": { 18 + "type": "ref", 19 + "ref": "xyz.statusphere.defs#statusView" 20 + } 21 + }, 22 + "required": [ 23 + "profile" 24 + ] 25 + } 26 + } 27 + } 28 + } 29 + }
+42
packages/example/lexicons/xyz/statusphere/sendStatus.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "xyz.statusphere.sendStatus", 4 + "defs": { 5 + "main": { 6 + "type": "procedure", 7 + "description": "Send a status into the ATmosphere.", 8 + "input": { 9 + "encoding": "application/json", 10 + "schema": { 11 + "type": "object", 12 + "properties": { 13 + "status": { 14 + "type": "string", 15 + "maxLength": 32, 16 + "minLength": 1, 17 + "maxGraphemes": 1 18 + } 19 + }, 20 + "required": [ 21 + "status" 22 + ] 23 + } 24 + }, 25 + "output": { 26 + "encoding": "application/json", 27 + "schema": { 28 + "type": "object", 29 + "properties": { 30 + "status": { 31 + "type": "ref", 32 + "ref": "xyz.statusphere.defs#statusView" 33 + } 34 + }, 35 + "required": [ 36 + "status" 37 + ] 38 + } 39 + } 40 + } 41 + } 42 + }
+29
packages/example/lexicons/xyz/statusphere/status.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "xyz.statusphere.status", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "record": { 9 + "type": "object", 10 + "properties": { 11 + "status": { 12 + "type": "string", 13 + "maxLength": 32, 14 + "minLength": 1, 15 + "maxGraphemes": 1 16 + }, 17 + "createdAt": { 18 + "type": "string", 19 + "format": "datetime" 20 + } 21 + }, 22 + "required": [ 23 + "status", 24 + "createdAt" 25 + ] 26 + } 27 + } 28 + } 29 + }
+4 -4
packages/example/package.json
··· 4 4 "private": true, 5 5 "type": "module", 6 6 "scripts": { 7 - "build": "pnpm run build:lexicons && pnpm run build:codegen", 8 - "build:lexicons": "tsp compile typelex/main.tsp", 9 - "build:codegen": "lex gen-server --yes ./src lexicons/app/example/*.json" 7 + "build": "pnpm run build:typelex && pnpm run build:codegen", 8 + "build:typelex": "typelex compile xyz.statusphere.*", 9 + "build:codegen": "lex gen-server --yes ./src lexicons/xyz/statusphere/*.json" 10 10 }, 11 11 "dependencies": { 12 12 "@atproto/lex-cli": "^0.9.5", 13 13 "@atproto/xrpc-server": "^0.9.5", 14 - "@typespec/compiler": "^1.4.0", 14 + "@typelex/cli": "workspace:*", 15 15 "@typelex/emitter": "workspace:*" 16 16 }, 17 17 "devDependencies": {
+45 -6
packages/example/src/index.ts
··· 10 10 createServer as createXrpcServer, 11 11 } from '@atproto/xrpc-server' 12 12 import { schemas } from './lexicons.js' 13 + import * as XyzStatusphereGetStatuses from './types/xyz/statusphere/getStatuses.js' 14 + import * as XyzStatusphereGetUser from './types/xyz/statusphere/getUser.js' 15 + import * as XyzStatusphereSendStatus from './types/xyz/statusphere/sendStatus.js' 13 16 14 17 export function createServer(options?: XrpcOptions): Server { 15 18 return new Server(options) ··· 17 20 18 21 export class Server { 19 22 xrpc: XrpcServer 20 - app: AppNS 23 + xyz: XyzNS 21 24 22 25 constructor(options?: XrpcOptions) { 23 26 this.xrpc = createXrpcServer(schemas, options) 24 - this.app = new AppNS(this) 27 + this.xyz = new XyzNS(this) 25 28 } 26 29 } 27 30 28 - export class AppNS { 31 + export class XyzNS { 29 32 _server: Server 30 - example: AppExampleNS 33 + statusphere: XyzStatusphereNS 31 34 32 35 constructor(server: Server) { 33 36 this._server = server 34 - this.example = new AppExampleNS(server) 37 + this.statusphere = new XyzStatusphereNS(server) 35 38 } 36 39 } 37 40 38 - export class AppExampleNS { 41 + export class XyzStatusphereNS { 39 42 _server: Server 40 43 41 44 constructor(server: Server) { 42 45 this._server = server 46 + } 47 + 48 + getStatuses<A extends Auth = void>( 49 + cfg: MethodConfigOrHandler< 50 + A, 51 + XyzStatusphereGetStatuses.QueryParams, 52 + XyzStatusphereGetStatuses.HandlerInput, 53 + XyzStatusphereGetStatuses.HandlerOutput 54 + >, 55 + ) { 56 + const nsid = 'xyz.statusphere.getStatuses' // @ts-ignore 57 + return this._server.xrpc.method(nsid, cfg) 58 + } 59 + 60 + getUser<A extends Auth = void>( 61 + cfg: MethodConfigOrHandler< 62 + A, 63 + XyzStatusphereGetUser.QueryParams, 64 + XyzStatusphereGetUser.HandlerInput, 65 + XyzStatusphereGetUser.HandlerOutput 66 + >, 67 + ) { 68 + const nsid = 'xyz.statusphere.getUser' // @ts-ignore 69 + return this._server.xrpc.method(nsid, cfg) 70 + } 71 + 72 + sendStatus<A extends Auth = void>( 73 + cfg: MethodConfigOrHandler< 74 + A, 75 + XyzStatusphereSendStatus.QueryParams, 76 + XyzStatusphereSendStatus.HandlerInput, 77 + XyzStatusphereSendStatus.HandlerOutput 78 + >, 79 + ) { 80 + const nsid = 'xyz.statusphere.sendStatus' // @ts-ignore 81 + return this._server.xrpc.method(nsid, cfg) 43 82 } 44 83 }
+100 -153
packages/example/src/lexicons.ts
··· 10 10 import { type $Typed, is$typed, maybe$typed } from './util.js' 11 11 12 12 export const schemaDict = { 13 - AppExampleDefs: { 13 + XyzStatusphereDefs: { 14 14 lexicon: 1, 15 - id: 'app.example.defs', 15 + id: 'xyz.statusphere.defs', 16 16 defs: { 17 - postRef: { 17 + statusView: { 18 18 type: 'object', 19 19 properties: { 20 20 uri: { 21 21 type: 'string', 22 - description: 'AT URI of the post', 22 + format: 'at-uri', 23 23 }, 24 - cid: { 24 + status: { 25 25 type: 'string', 26 - description: 'CID of the post', 26 + maxLength: 32, 27 + minLength: 1, 28 + maxGraphemes: 1, 27 29 }, 28 - }, 29 - description: 'Reference to a post', 30 - required: ['uri', 'cid'], 31 - }, 32 - replyRef: { 33 - type: 'object', 34 - properties: { 35 - root: { 36 - type: 'ref', 37 - ref: 'lex:app.example.defs#postRef', 38 - description: 'Root post in the thread', 30 + createdAt: { 31 + type: 'string', 32 + format: 'datetime', 39 33 }, 40 - parent: { 34 + profile: { 41 35 type: 'ref', 42 - ref: 'lex:app.example.defs#postRef', 43 - description: 'Direct parent post being replied to', 36 + ref: 'lex:xyz.statusphere.defs#profileView', 44 37 }, 45 38 }, 46 - description: 'Reference to a parent post in a reply chain', 47 - required: ['root', 'parent'], 39 + required: ['uri', 'status', 'createdAt', 'profile'], 48 40 }, 49 - entity: { 41 + profileView: { 50 42 type: 'object', 51 43 properties: { 52 - start: { 53 - type: 'integer', 54 - description: 'Start index in text', 55 - }, 56 - end: { 57 - type: 'integer', 58 - description: 'End index in text', 59 - }, 60 - type: { 44 + did: { 61 45 type: 'string', 62 - description: 'Entity type', 46 + format: 'did', 63 47 }, 64 - value: { 48 + handle: { 65 49 type: 'string', 66 - description: 'Entity value (handle, URL, or tag)', 50 + format: 'handle', 67 51 }, 68 52 }, 69 - description: 'Text entity (mention, link, or tag)', 70 - required: ['start', 'end', 'type', 'value'], 71 - }, 72 - notificationType: { 73 - type: 'string', 74 - knownValues: ['like', 'repost', 'follow', 'mention', 'reply'], 75 - description: 'Type of notification', 53 + required: ['did', 'handle'], 76 54 }, 77 55 }, 78 56 }, 79 - AppExampleFollow: { 57 + XyzStatusphereGetStatuses: { 80 58 lexicon: 1, 81 - id: 'app.example.follow', 59 + id: 'xyz.statusphere.getStatuses', 82 60 defs: { 83 61 main: { 84 - type: 'record', 85 - key: 'tid', 86 - record: { 87 - type: 'object', 62 + type: 'query', 63 + description: 'Get a list of the most recent statuses on the network.', 64 + parameters: { 65 + type: 'params', 88 66 properties: { 89 - subject: { 90 - type: 'string', 91 - description: 'DID of the account being followed', 92 - }, 93 - createdAt: { 94 - type: 'string', 95 - format: 'datetime', 96 - description: 'When the follow was created', 67 + limit: { 68 + type: 'integer', 69 + minimum: 1, 70 + maximum: 100, 71 + default: 50, 97 72 }, 98 73 }, 99 - required: ['subject', 'createdAt'], 100 74 }, 101 - description: 'A follow relationship', 102 - }, 103 - }, 104 - }, 105 - AppExampleLike: { 106 - lexicon: 1, 107 - id: 'app.example.like', 108 - defs: { 109 - main: { 110 - type: 'record', 111 - key: 'tid', 112 - record: { 113 - type: 'object', 114 - properties: { 115 - subject: { 116 - type: 'ref', 117 - ref: 'lex:app.example.defs#postRef', 118 - description: 'Post being liked', 75 + output: { 76 + encoding: 'application/json', 77 + schema: { 78 + type: 'object', 79 + properties: { 80 + statuses: { 81 + type: 'array', 82 + items: { 83 + type: 'ref', 84 + ref: 'lex:xyz.statusphere.defs#statusView', 85 + }, 86 + }, 119 87 }, 120 - createdAt: { 121 - type: 'string', 122 - format: 'datetime', 123 - description: 'When the like was created', 124 - }, 88 + required: ['statuses'], 125 89 }, 126 - required: ['subject', 'createdAt'], 127 90 }, 128 - description: 'A like on a post', 129 91 }, 130 92 }, 131 93 }, 132 - AppExamplePost: { 94 + XyzStatusphereGetUser: { 133 95 lexicon: 1, 134 - id: 'app.example.post', 96 + id: 'xyz.statusphere.getUser', 135 97 defs: { 136 98 main: { 137 - type: 'record', 138 - key: 'tid', 139 - record: { 140 - type: 'object', 141 - properties: { 142 - text: { 143 - type: 'string', 144 - description: 'Post text content', 145 - }, 146 - createdAt: { 147 - type: 'string', 148 - format: 'datetime', 149 - description: 'Creation timestamp', 150 - }, 151 - langs: { 152 - type: 'array', 153 - items: { 154 - type: 'string', 99 + type: 'query', 100 + description: "Get the current user's profile and status.", 101 + output: { 102 + encoding: 'application/json', 103 + schema: { 104 + type: 'object', 105 + properties: { 106 + profile: { 107 + type: 'ref', 108 + ref: 'lex:app.bsky.actor.defs#profileView', 155 109 }, 156 - description: 'Languages the post is written in', 157 - }, 158 - entities: { 159 - type: 'array', 160 - items: { 110 + status: { 161 111 type: 'ref', 162 - ref: 'lex:app.example.defs#entity', 112 + ref: 'lex:xyz.statusphere.defs#statusView', 163 113 }, 164 - description: 'Referenced entities in the post', 165 114 }, 166 - reply: { 167 - type: 'ref', 168 - ref: 'lex:app.example.defs#replyRef', 169 - description: 'Post the user is replying to', 170 - }, 115 + required: ['profile'], 171 116 }, 172 - required: ['text', 'createdAt'], 173 117 }, 174 - description: 'A post in the feed', 175 118 }, 176 119 }, 177 120 }, 178 - AppExampleProfile: { 121 + XyzStatusphereSendStatus: { 179 122 lexicon: 1, 180 - id: 'app.example.profile', 123 + id: 'xyz.statusphere.sendStatus', 181 124 defs: { 182 125 main: { 183 - type: 'record', 184 - key: 'self', 185 - record: { 186 - type: 'object', 187 - properties: { 188 - displayName: { 189 - type: 'string', 190 - description: 'Display name', 191 - }, 192 - description: { 193 - type: 'string', 194 - description: 'Profile description', 126 + type: 'procedure', 127 + description: 'Send a status into the ATmosphere.', 128 + input: { 129 + encoding: 'application/json', 130 + schema: { 131 + type: 'object', 132 + properties: { 133 + status: { 134 + type: 'string', 135 + maxLength: 32, 136 + minLength: 1, 137 + maxGraphemes: 1, 138 + }, 195 139 }, 196 - avatar: { 197 - type: 'string', 198 - description: 'Profile avatar image', 140 + required: ['status'], 141 + }, 142 + }, 143 + output: { 144 + encoding: 'application/json', 145 + schema: { 146 + type: 'object', 147 + properties: { 148 + status: { 149 + type: 'ref', 150 + ref: 'lex:xyz.statusphere.defs#statusView', 151 + }, 199 152 }, 200 - banner: { 201 - type: 'string', 202 - description: 'Profile banner image', 203 - }, 153 + required: ['status'], 204 154 }, 205 155 }, 206 - description: 'User profile information', 207 156 }, 208 157 }, 209 158 }, 210 - AppExampleRepost: { 159 + XyzStatusphereStatus: { 211 160 lexicon: 1, 212 - id: 'app.example.repost', 161 + id: 'xyz.statusphere.status', 213 162 defs: { 214 163 main: { 215 164 type: 'record', ··· 217 166 record: { 218 167 type: 'object', 219 168 properties: { 220 - subject: { 221 - type: 'ref', 222 - ref: 'lex:app.example.defs#postRef', 223 - description: 'Post being reposted', 169 + status: { 170 + type: 'string', 171 + maxLength: 32, 172 + minLength: 1, 173 + maxGraphemes: 1, 224 174 }, 225 175 createdAt: { 226 176 type: 'string', 227 177 format: 'datetime', 228 - description: 'When the repost was created', 229 178 }, 230 179 }, 231 - required: ['subject', 'createdAt'], 180 + required: ['status', 'createdAt'], 232 181 }, 233 - description: 'A repost of another post', 234 182 }, 235 183 }, 236 184 }, ··· 267 215 } 268 216 269 217 export const ids = { 270 - AppExampleDefs: 'app.example.defs', 271 - AppExampleFollow: 'app.example.follow', 272 - AppExampleLike: 'app.example.like', 273 - AppExamplePost: 'app.example.post', 274 - AppExampleProfile: 'app.example.profile', 275 - AppExampleRepost: 'app.example.repost', 218 + XyzStatusphereDefs: 'xyz.statusphere.defs', 219 + XyzStatusphereGetStatuses: 'xyz.statusphere.getStatuses', 220 + XyzStatusphereGetUser: 'xyz.statusphere.getUser', 221 + XyzStatusphereSendStatus: 'xyz.statusphere.sendStatus', 222 + XyzStatusphereStatus: 'xyz.statusphere.status', 276 223 } as const
-79
packages/example/src/types/app/example/defs.ts
··· 1 - /** 2 - * GENERATED CODE - DO NOT MODIFY 3 - */ 4 - import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { CID } from 'multiformats/cid' 6 - import { validate as _validate } from '../../../lexicons' 7 - import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 - 9 - const is$typed = _is$typed, 10 - validate = _validate 11 - const id = 'app.example.defs' 12 - 13 - /** Reference to a post */ 14 - export interface PostRef { 15 - $type?: 'app.example.defs#postRef' 16 - /** AT URI of the post */ 17 - uri: string 18 - /** CID of the post */ 19 - cid: string 20 - } 21 - 22 - const hashPostRef = 'postRef' 23 - 24 - export function isPostRef<V>(v: V) { 25 - return is$typed(v, id, hashPostRef) 26 - } 27 - 28 - export function validatePostRef<V>(v: V) { 29 - return validate<PostRef & V>(v, id, hashPostRef) 30 - } 31 - 32 - /** Reference to a parent post in a reply chain */ 33 - export interface ReplyRef { 34 - $type?: 'app.example.defs#replyRef' 35 - root: PostRef 36 - parent: PostRef 37 - } 38 - 39 - const hashReplyRef = 'replyRef' 40 - 41 - export function isReplyRef<V>(v: V) { 42 - return is$typed(v, id, hashReplyRef) 43 - } 44 - 45 - export function validateReplyRef<V>(v: V) { 46 - return validate<ReplyRef & V>(v, id, hashReplyRef) 47 - } 48 - 49 - /** Text entity (mention, link, or tag) */ 50 - export interface Entity { 51 - $type?: 'app.example.defs#entity' 52 - /** Start index in text */ 53 - start: number 54 - /** End index in text */ 55 - end: number 56 - /** Entity type */ 57 - type: string 58 - /** Entity value (handle, URL, or tag) */ 59 - value: string 60 - } 61 - 62 - const hashEntity = 'entity' 63 - 64 - export function isEntity<V>(v: V) { 65 - return is$typed(v, id, hashEntity) 66 - } 67 - 68 - export function validateEntity<V>(v: V) { 69 - return validate<Entity & V>(v, id, hashEntity) 70 - } 71 - 72 - /** Type of notification */ 73 - export type NotificationType = 74 - | 'like' 75 - | 'repost' 76 - | 'follow' 77 - | 'mention' 78 - | 'reply' 79 - | (string & {})
-30
packages/example/src/types/app/example/follow.ts
··· 1 - /** 2 - * GENERATED CODE - DO NOT MODIFY 3 - */ 4 - import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { CID } from 'multiformats/cid' 6 - import { validate as _validate } from '../../../lexicons' 7 - import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 - 9 - const is$typed = _is$typed, 10 - validate = _validate 11 - const id = 'app.example.follow' 12 - 13 - export interface Record { 14 - $type: 'app.example.follow' 15 - /** DID of the account being followed */ 16 - subject: string 17 - /** When the follow was created */ 18 - createdAt: string 19 - [k: string]: unknown 20 - } 21 - 22 - const hashRecord = 'main' 23 - 24 - export function isRecord<V>(v: V) { 25 - return is$typed(v, id, hashRecord) 26 - } 27 - 28 - export function validateRecord<V>(v: V) { 29 - return validate<Record & V>(v, id, hashRecord, true) 30 - }
-30
packages/example/src/types/app/example/like.ts
··· 1 - /** 2 - * GENERATED CODE - DO NOT MODIFY 3 - */ 4 - import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { CID } from 'multiformats/cid' 6 - import { validate as _validate } from '../../../lexicons' 7 - import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 - import type * as AppExampleDefs from './defs.js' 9 - 10 - const is$typed = _is$typed, 11 - validate = _validate 12 - const id = 'app.example.like' 13 - 14 - export interface Record { 15 - $type: 'app.example.like' 16 - subject: AppExampleDefs.PostRef 17 - /** When the like was created */ 18 - createdAt: string 19 - [k: string]: unknown 20 - } 21 - 22 - const hashRecord = 'main' 23 - 24 - export function isRecord<V>(v: V) { 25 - return is$typed(v, id, hashRecord) 26 - } 27 - 28 - export function validateRecord<V>(v: V) { 29 - return validate<Record & V>(v, id, hashRecord, true) 30 - }
-36
packages/example/src/types/app/example/post.ts
··· 1 - /** 2 - * GENERATED CODE - DO NOT MODIFY 3 - */ 4 - import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { CID } from 'multiformats/cid' 6 - import { validate as _validate } from '../../../lexicons' 7 - import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 - import type * as AppExampleDefs from './defs.js' 9 - 10 - const is$typed = _is$typed, 11 - validate = _validate 12 - const id = 'app.example.post' 13 - 14 - export interface Record { 15 - $type: 'app.example.post' 16 - /** Post text content */ 17 - text: string 18 - /** Creation timestamp */ 19 - createdAt: string 20 - /** Languages the post is written in */ 21 - langs?: string[] 22 - /** Referenced entities in the post */ 23 - entities?: AppExampleDefs.Entity[] 24 - reply?: AppExampleDefs.ReplyRef 25 - [k: string]: unknown 26 - } 27 - 28 - const hashRecord = 'main' 29 - 30 - export function isRecord<V>(v: V) { 31 - return is$typed(v, id, hashRecord) 32 - } 33 - 34 - export function validateRecord<V>(v: V) { 35 - return validate<Record & V>(v, id, hashRecord, true) 36 - }
-34
packages/example/src/types/app/example/profile.ts
··· 1 - /** 2 - * GENERATED CODE - DO NOT MODIFY 3 - */ 4 - import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { CID } from 'multiformats/cid' 6 - import { validate as _validate } from '../../../lexicons' 7 - import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 - 9 - const is$typed = _is$typed, 10 - validate = _validate 11 - const id = 'app.example.profile' 12 - 13 - export interface Record { 14 - $type: 'app.example.profile' 15 - /** Display name */ 16 - displayName?: string 17 - /** Profile description */ 18 - description?: string 19 - /** Profile avatar image */ 20 - avatar?: string 21 - /** Profile banner image */ 22 - banner?: string 23 - [k: string]: unknown 24 - } 25 - 26 - const hashRecord = 'main' 27 - 28 - export function isRecord<V>(v: V) { 29 - return is$typed(v, id, hashRecord) 30 - } 31 - 32 - export function validateRecord<V>(v: V) { 33 - return validate<Record & V>(v, id, hashRecord, true) 34 - }
-30
packages/example/src/types/app/example/repost.ts
··· 1 - /** 2 - * GENERATED CODE - DO NOT MODIFY 3 - */ 4 - import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 - import { CID } from 'multiformats/cid' 6 - import { validate as _validate } from '../../../lexicons' 7 - import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 - import type * as AppExampleDefs from './defs.js' 9 - 10 - const is$typed = _is$typed, 11 - validate = _validate 12 - const id = 'app.example.repost' 13 - 14 - export interface Record { 15 - $type: 'app.example.repost' 16 - subject: AppExampleDefs.PostRef 17 - /** When the repost was created */ 18 - createdAt: string 19 - [k: string]: unknown 20 - } 21 - 22 - const hashRecord = 'main' 23 - 24 - export function isRecord<V>(v: V) { 25 - return is$typed(v, id, hashRecord) 26 - } 27 - 28 - export function validateRecord<V>(v: V) { 29 - return validate<Record & V>(v, id, hashRecord, true) 30 - }
+45
packages/example/src/types/xyz/statusphere/defs.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../lexicons' 7 + import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate 11 + const id = 'xyz.statusphere.defs' 12 + 13 + export interface StatusView { 14 + $type?: 'xyz.statusphere.defs#statusView' 15 + uri: string 16 + status: string 17 + createdAt: string 18 + profile: ProfileView 19 + } 20 + 21 + const hashStatusView = 'statusView' 22 + 23 + export function isStatusView<V>(v: V) { 24 + return is$typed(v, id, hashStatusView) 25 + } 26 + 27 + export function validateStatusView<V>(v: V) { 28 + return validate<StatusView & V>(v, id, hashStatusView) 29 + } 30 + 31 + export interface ProfileView { 32 + $type?: 'xyz.statusphere.defs#profileView' 33 + did: string 34 + handle: string 35 + } 36 + 37 + const hashProfileView = 'profileView' 38 + 39 + export function isProfileView<V>(v: V) { 40 + return is$typed(v, id, hashProfileView) 41 + } 42 + 43 + export function validateProfileView<V>(v: V) { 44 + return validate<ProfileView & V>(v, id, hashProfileView) 45 + }
+36
packages/example/src/types/xyz/statusphere/getStatuses.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../lexicons' 7 + import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 + import type * as XyzStatusphereDefs from './defs.js' 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate 12 + const id = 'xyz.statusphere.getStatuses' 13 + 14 + export type QueryParams = { 15 + limit: number 16 + } 17 + export type InputSchema = undefined 18 + 19 + export interface OutputSchema { 20 + statuses: XyzStatusphereDefs.StatusView[] 21 + } 22 + 23 + export type HandlerInput = void 24 + 25 + export interface HandlerSuccess { 26 + encoding: 'application/json' 27 + body: OutputSchema 28 + headers?: { [key: string]: string } 29 + } 30 + 31 + export interface HandlerError { 32 + status: number 33 + message?: string 34 + } 35 + 36 + export type HandlerOutput = HandlerError | HandlerSuccess
+36
packages/example/src/types/xyz/statusphere/getUser.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../lexicons' 7 + import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 + import type * as AppBskyActorDefs from '../../app/bsky/actor/defs.js' 9 + import type * as XyzStatusphereDefs from './defs.js' 10 + 11 + const is$typed = _is$typed, 12 + validate = _validate 13 + const id = 'xyz.statusphere.getUser' 14 + 15 + export type QueryParams = {} 16 + export type InputSchema = undefined 17 + 18 + export interface OutputSchema { 19 + profile: AppBskyActorDefs.ProfileView 20 + status?: XyzStatusphereDefs.StatusView 21 + } 22 + 23 + export type HandlerInput = void 24 + 25 + export interface HandlerSuccess { 26 + encoding: 'application/json' 27 + body: OutputSchema 28 + headers?: { [key: string]: string } 29 + } 30 + 31 + export interface HandlerError { 32 + status: number 33 + message?: string 34 + } 35 + 36 + export type HandlerOutput = HandlerError | HandlerSuccess
+40
packages/example/src/types/xyz/statusphere/sendStatus.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../lexicons' 7 + import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 + import type * as XyzStatusphereDefs from './defs.js' 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate 12 + const id = 'xyz.statusphere.sendStatus' 13 + 14 + export type QueryParams = {} 15 + 16 + export interface InputSchema { 17 + status: string 18 + } 19 + 20 + export interface OutputSchema { 21 + status: XyzStatusphereDefs.StatusView 22 + } 23 + 24 + export interface HandlerInput { 25 + encoding: 'application/json' 26 + body: InputSchema 27 + } 28 + 29 + export interface HandlerSuccess { 30 + encoding: 'application/json' 31 + body: OutputSchema 32 + headers?: { [key: string]: string } 33 + } 34 + 35 + export interface HandlerError { 36 + status: number 37 + message?: string 38 + } 39 + 40 + export type HandlerOutput = HandlerError | HandlerSuccess
+28
packages/example/src/types/xyz/statusphere/status.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { type ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../lexicons' 7 + import { type $Typed, is$typed as _is$typed, type OmitKey } from '../../../util' 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate 11 + const id = 'xyz.statusphere.status' 12 + 13 + export interface Record { 14 + $type: 'xyz.statusphere.status' 15 + status: string 16 + createdAt: string 17 + [k: string]: unknown 18 + } 19 + 20 + const hashRecord = 'main' 21 + 22 + export function isRecord<V>(v: V) { 23 + return is$typed(v, id, hashRecord) 24 + } 25 + 26 + export function validateRecord<V>(v: V) { 27 + return validate<Record & V>(v, id, hashRecord, true) 28 + }
-5
packages/example/tspconfig.yaml
··· 1 - emit: 2 - - "@typelex/emitter" 3 - options: 4 - "@typelex/emitter": 5 - output-dir: "./lexicons"
+82
packages/example/typelex/externals.tsp
··· 1 + import "@typelex/emitter"; 2 + 3 + // Generated by typelex from ./lexicons (excluding xyz.statusphere.*) 4 + // This file is auto-generated. Do not edit manually. 5 + 6 + @external 7 + namespace app.bsky.actor.defs { 8 + model ProfileView { } 9 + } 10 + 11 + @external 12 + namespace app.bsky.actor.profile { 13 + model Main { } 14 + } 15 + 16 + @external 17 + namespace com.atproto.label.defs { 18 + model Label { } 19 + model LabelValue { } 20 + model LabelValueDefinition { } 21 + model LabelValueDefinitionStrings { } 22 + model SelfLabel { } 23 + model SelfLabels { } 24 + } 25 + 26 + @external 27 + namespace com.atproto.repo.applyWrites { 28 + model Create { } 29 + model CreateResult { } 30 + model Delete { } 31 + model DeleteResult { } 32 + model Update { } 33 + model UpdateResult { } 34 + } 35 + 36 + @external 37 + namespace com.atproto.repo.createRecord { 38 + } 39 + 40 + @external 41 + namespace com.atproto.repo.defs { 42 + model CommitMeta { } 43 + } 44 + 45 + @external 46 + namespace com.atproto.repo.deleteRecord { 47 + } 48 + 49 + @external 50 + namespace com.atproto.repo.describeRepo { 51 + } 52 + 53 + @external 54 + namespace com.atproto.repo.getRecord { 55 + } 56 + 57 + @external 58 + namespace com.atproto.repo.importRepo { 59 + } 60 + 61 + @external 62 + namespace com.atproto.repo.listMissingBlobs { 63 + model RecordBlob { } 64 + } 65 + 66 + @external 67 + namespace com.atproto.repo.listRecords { 68 + model Record { } 69 + } 70 + 71 + @external 72 + namespace com.atproto.repo.putRecord { 73 + } 74 + 75 + @external 76 + namespace com.atproto.repo.strongRef { 77 + model Main { } 78 + } 79 + 80 + @external 81 + namespace com.atproto.repo.uploadBlob { 82 + }
+46 -122
packages/example/typelex/main.tsp
··· 1 1 import "@typelex/emitter"; 2 - 3 - // Example showing typelex as source of truth for atproto lexicons 4 - 5 - // ============ Common Types ============ 6 - 7 - namespace app.example.defs { 8 - @doc("Type of notification") 9 - union notificationType { 10 - string, 11 - 12 - Like: "like", 13 - Repost: "repost", 14 - Follow: "follow", 15 - Mention: "mention", 16 - Reply: "reply", 17 - } 18 - 19 - @doc("Reference to a post") 20 - model PostRef { 21 - @doc("AT URI of the post") 22 - @required 23 - uri: string; 24 - 25 - @doc("CID of the post") 26 - @required 27 - cid: string; 28 - } 29 - 30 - @doc("Reference to a parent post in a reply chain") 31 - model ReplyRef { 32 - @doc("Root post in the thread") 33 - @required 34 - root: PostRef; 35 - 36 - @doc("Direct parent post being replied to") 37 - @required 38 - parent: PostRef; 39 - } 40 - 41 - @doc("Text entity (mention, link, or tag)") 42 - model Entity { 43 - @doc("Start index in text") 44 - @required 45 - start: int32; 2 + import "./externals.tsp"; 46 3 47 - @doc("End index in text") 48 - @required 49 - end: int32; 4 + namespace xyz.statusphere.defs { 5 + model StatusView { 6 + @required uri: atUri; 50 7 51 - @doc("Entity type") 52 8 @required 53 - type: string; 9 + @minLength(1) 10 + @maxGraphemes(1) 11 + @maxLength(32) 12 + status: string; 54 13 55 - @doc("Entity value (handle, URL, or tag)") 56 - @required 57 - value: string; 14 + @required createdAt: datetime; 15 + @required profile: ProfileView; 58 16 } 59 - } 60 17 61 - // ============ Records ============ 62 - 63 - namespace app.example.post { 64 - @rec("tid") 65 - @doc("A post in the feed") 66 - model Main { 67 - @doc("Post text content") 68 - @required 69 - text: string; 70 - 71 - @doc("Creation timestamp") 72 - @required 73 - createdAt: datetime; 74 - 75 - @doc("Languages the post is written in") 76 - langs?: string[]; 77 - 78 - @doc("Referenced entities in the post") 79 - entities?: app.example.defs.Entity[]; 80 - 81 - @doc("Post the user is replying to") 82 - reply?: app.example.defs.ReplyRef; 18 + model ProfileView { 19 + @required did: did; 20 + @required handle: handle; 83 21 } 84 22 } 85 23 86 - namespace app.example.follow { 24 + namespace xyz.statusphere.status { 87 25 @rec("tid") 88 - @doc("A follow relationship") 89 26 model Main { 90 - @doc("DID of the account being followed") 91 27 @required 92 - subject: string; 28 + @minLength(1) 29 + @maxGraphemes(1) 30 + @maxLength(32) 31 + status: string; 93 32 94 - @doc("When the follow was created") 95 - @required 96 - createdAt: datetime; 33 + @required createdAt: datetime; 97 34 } 98 35 } 99 36 100 - namespace app.example.like { 101 - @rec("tid") 102 - @doc("A like on a post") 103 - model Main { 104 - @doc("Post being liked") 105 - @required 106 - subject: app.example.defs.PostRef; 107 - 108 - @doc("When the like was created") 109 - @required 110 - createdAt: datetime; 111 - } 37 + namespace xyz.statusphere.sendStatus { 38 + @procedure 39 + @doc("Send a status into the ATmosphere.") 40 + op main( 41 + input: { 42 + @required 43 + @minLength(1) 44 + @maxGraphemes(1) 45 + @maxLength(32) 46 + status: string; 47 + }, 48 + ): { 49 + @required status: xyz.statusphere.defs.StatusView; 50 + }; 112 51 } 113 52 114 - namespace app.example.repost { 115 - @rec("tid") 116 - @doc("A repost of another post") 117 - model Main { 118 - @doc("Post being reposted") 119 - @required 120 - subject: app.example.defs.PostRef; 121 - 122 - @doc("When the repost was created") 123 - @required 124 - createdAt: datetime; 125 - } 53 + namespace xyz.statusphere.getStatuses { 54 + @query 55 + @doc("Get a list of the most recent statuses on the network.") 56 + op main(@minValue(1) @maxValue(100) limit?: integer = 50): { 57 + @required statuses: xyz.statusphere.defs.StatusView[]; 58 + }; 126 59 } 127 60 128 - namespace app.example.profile { 129 - @rec("self") 130 - @doc("User profile information") 131 - model Main { 132 - @doc("Display name") 133 - displayName?: string; 134 - 135 - @doc("Profile description") 136 - description?: string; 137 - 138 - @doc("Profile avatar image") 139 - avatar?: string; 140 - 141 - @doc("Profile banner image") 142 - banner?: string; 143 - } 61 + namespace xyz.statusphere.getUser { 62 + @query 63 + @doc("Get the current user's profile and status.") 64 + op main(): { 65 + @required profile: app.bsky.actor.defs.ProfileView; 66 + status?: xyz.statusphere.defs.StatusView; 67 + }; 144 68 }
+2 -3
packages/playground/package.json
··· 4 4 "private": true, 5 5 "type": "module", 6 6 "scripts": { 7 - "build:samples": "node samples/build.js", 8 - "dev": "npm run build:samples && vite", 9 - "build": "npm run build:samples && vite build", 7 + "dev": "vite", 8 + "build": "vite build", 10 9 "preview": "vite preview" 11 10 }, 12 11 "dependencies": {
-33
packages/playground/samples/build.js
··· 1 - // @ts-check 2 - import { writeFileSync, mkdirSync } from "fs"; 3 - import { dirname, resolve, join } from "path"; 4 - import { fileURLToPath } from "url"; 5 - import { lexicons, bundleLexicon } from "./index.js"; 6 - 7 - const __dirname = dirname(fileURLToPath(import.meta.url)); 8 - const outputDir = resolve(__dirname, "dist"); 9 - 10 - // Create output directory 11 - mkdirSync(outputDir, { recursive: true }); 12 - 13 - // Write each bundled lexicon to disk 14 - const samplesList = {}; 15 - 16 - for (const [namespace, lexicon] of lexicons) { 17 - const bundled = bundleLexicon(namespace); 18 - const filename = `${namespace}.tsp`; 19 - const filepath = join(outputDir, filename); 20 - 21 - writeFileSync(filepath, bundled); 22 - 23 - samplesList[namespace] = { 24 - filename: `samples/dist/${filename}`, 25 - preferredEmitter: "@typelex/emitter", 26 - }; 27 - } 28 - 29 - // Write the samples index 30 - const samplesIndex = `export default ${JSON.stringify(samplesList, null, 2)};`; 31 - writeFileSync(join(outputDir, "samples.js"), samplesIndex); 32 - 33 - console.log(`Wrote ${Object.keys(samplesList).length} bundled samples to disk`);
+28 -105
packages/playground/samples/index.js
··· 6 6 const __dirname = dirname(fileURLToPath(import.meta.url)); 7 7 8 8 // Get all tsp files 9 - function getAllTspFiles(dir, baseDir = dir) { 9 + function getAllFiles(dir, baseDir = dir) { 10 10 const files = []; 11 11 const entries = readdirSync(dir); 12 12 ··· 15 15 const stat = statSync(fullPath); 16 16 17 17 if (stat.isDirectory()) { 18 - files.push(...getAllTspFiles(fullPath, baseDir)); 18 + files.push(...getAllFiles(fullPath, baseDir)); 19 19 } else if (entry.endsWith(".tsp")) { 20 20 files.push(relative(baseDir, fullPath)); 21 21 } ··· 24 24 return files.sort(); 25 25 } 26 26 27 - // Extract dependencies from a file 28 - function extractDependencies(content) { 29 - const deps = new Set(); 30 - // Match namespace references like "com.atproto.label.defs.Label" or "com.atproto.repo.strongRef.Main" 31 - // Pattern: word.word.word... followed by dot and identifier starting with capital letter 32 - const pattern = 33 - /\b([a-z]+(?:\.[a-z]+)+(?:\.[a-z][a-zA-Z]*)*)\.[A-Z][a-zA-Z]*/g; 34 - const withoutDeclaration = content.replace(/namespace\s+[a-z.]+\s*\{/, ""); 27 + const integrationDir = join(__dirname, "../../emitter/test/integration"); 35 28 36 - const matches = withoutDeclaration.matchAll(pattern); 37 - for (const match of matches) { 38 - deps.add(match[1]); 39 - } 29 + // Get all test suite directories 30 + const testSuites = readdirSync(integrationDir).filter((name) => { 31 + const fullPath = join(integrationDir, name); 32 + return statSync(fullPath).isDirectory() && !name.startsWith("."); 33 + }); 40 34 41 - return Array.from(deps); 42 - } 35 + // Load all lexicons from test suites 36 + const lexicons = new Map(); // namespace -> { file, content, suite } 43 37 44 - const atprotoInputDir = join( 45 - __dirname, 46 - "../../emitter/test/integration/atproto/input", 47 - ); 48 - const lexiconExamplesDir = join( 49 - __dirname, 50 - "../../emitter/test/integration/lexicon-examples/input", 51 - ); 38 + for (const suite of testSuites) { 39 + const inputDir = join(integrationDir, suite, "input"); 40 + const inputFiles = getAllFiles(inputDir).filter((f) => f.endsWith(".tsp")); 52 41 53 - const atprotoFiles = getAllTspFiles(atprotoInputDir); 54 - const lexiconExampleFiles = getAllTspFiles(lexiconExamplesDir); 42 + for (const file of inputFiles) { 43 + const fullPath = join(inputDir, file); 44 + const content = readFileSync(fullPath, "utf-8"); 45 + const namespace = file.replace(/\.tsp$/, "").replace(/\//g, "."); 55 46 56 - // Build dependency graph 57 - const lexicons = new Map(); // namespace -> { file, content, deps } 58 - 59 - // Process atproto files 60 - for (const file of atprotoFiles) { 61 - const fullPath = join(atprotoInputDir, file); 62 - const content = readFileSync(fullPath, "utf-8"); 63 - const namespace = file.replace(/\.tsp$/, "").replace(/\//g, "."); 64 - const deps = extractDependencies(content); 65 - 66 - lexicons.set(namespace, { file: `atproto/${file}`, content, deps }); 67 - } 68 - 69 - // Process lexicon-examples files 70 - for (const file of lexiconExampleFiles) { 71 - const fullPath = join(lexiconExamplesDir, file); 72 - const content = readFileSync(fullPath, "utf-8"); 73 - const namespace = file.replace(/\.tsp$/, "").replace(/\//g, "."); 74 - const deps = extractDependencies(content); 75 - 76 - lexicons.set(namespace, { file: `examples/${file}`, content, deps }); 77 - } 78 - 79 - // Recursively collect all dependencies (topological sort) 80 - function collectDependencies( 81 - namespace, 82 - collected = new Set(), 83 - visiting = new Set(), 84 - ) { 85 - if (collected.has(namespace)) return; 86 - if (visiting.has(namespace)) return; // circular dependency 87 - 88 - const lexicon = lexicons.get(namespace); 89 - if (!lexicon) return; 90 - 91 - visiting.add(namespace); 92 - 93 - // First collect all dependencies 94 - for (const dep of lexicon.deps) { 95 - collectDependencies(dep, collected, visiting); 47 + lexicons.set(namespace, { file, content, suite, fullPath }); 96 48 } 97 - 98 - visiting.delete(namespace); 99 - collected.add(namespace); 100 49 } 101 50 102 - // Bundle a lexicon with all its dependencies 103 - function bundleLexicon(namespace) { 104 - const collected = new Set(); 105 - collectDependencies(namespace, collected); 106 - 107 - // Put the main lexicon FIRST, then its dependencies 108 - const mainLexicon = lexicons.get(namespace); 109 - const deps = Array.from(collected).filter((ns) => ns !== namespace); 110 - 111 - let bundled = 'import "@typelex/emitter";\n\n'; 112 - 113 - // Main lexicon first (so it shows in the playground) 114 - if (mainLexicon) { 115 - const contentWithoutImport = mainLexicon.content.replace( 116 - /^import "@typelex\/emitter";\s*\n/, 117 - "", 118 - ); 119 - bundled += `// ${mainLexicon.file}\n${contentWithoutImport}\n`; 120 - } 121 - 122 - // Then dependencies 123 - for (const ns of deps) { 124 - const lexicon = lexicons.get(ns); 125 - if (!lexicon) continue; 126 - 127 - const contentWithoutImport = lexicon.content.replace( 128 - /^import "@typelex\/emitter";\s*\n/, 129 - "", 130 - ); 131 - bundled += `// ${lexicon.file}\n${contentWithoutImport}\n`; 132 - } 133 - 134 - return bundled; 51 + // Build samples list for playground 52 + const samplesList = {}; 53 + for (const [namespace, lexicon] of lexicons) { 54 + samplesList[namespace] = { 55 + filename: relative(join(__dirname, ".."), lexicon.fullPath), 56 + preferredEmitter: "@typelex/emitter", 57 + }; 135 58 } 136 59 137 - // Export for build script 138 - export { lexicons, bundleLexicon }; 60 + export { lexicons }; 61 + export default samplesList; 139 62 140 - console.log(`Loaded ${lexicons.size} lexicons for bundling`); 63 + console.log(`Loaded ${lexicons.size} lexicons`);
+1 -1
packages/playground/vite.config.ts
··· 1 1 import { definePlaygroundViteConfig } from "@typespec/playground/vite"; 2 2 import { defineConfig } from "vite"; 3 - import samples from "./samples/dist/samples.js"; 3 + import samples from "./samples/index.js"; 4 4 5 5 const playgroundConfig = definePlaygroundViteConfig({ 6 6 defaultEmitter: "@typelex/emitter",
+1
packages/website/package.json
··· 1 1 { 2 2 "name": "website", 3 3 "type": "module", 4 + "private": true, 4 5 "version": "0.0.1", 5 6 "scripts": { 6 7 "dev": "astro dev",
+14
packages/website/src/components/CodeBlock.astro
··· 1 + --- 2 + import { highlightCode } from '../utils/shiki'; 3 + 4 + interface Props { 5 + lang: 'typespec' | 'json' | 'bash'; 6 + code?: string; 7 + } 8 + 9 + const { lang, code } = Astro.props; 10 + const codeContent = code || await Astro.slots.render('default'); 11 + const highlighted = await highlightCode(codeContent.trim(), lang); 12 + --- 13 + 14 + <pre set:html={highlighted} />
+62
packages/website/src/components/ComparisonBlock.astro
··· 1 + --- 2 + import { highlightCode } from '../utils/shiki'; 3 + import { compileToJson } from '../utils/compile'; 4 + import { createPlaygroundUrl } from '../utils/playground-url'; 5 + import stringify from 'json-stringify-pretty-compact'; 6 + import { mkdtempSync, writeFileSync, rmSync } from 'fs'; 7 + import { join } from 'path'; 8 + import { tmpdir } from 'os'; 9 + 10 + interface Props { 11 + code: string; 12 + hero?: boolean; 13 + } 14 + 15 + const { code, hero = false } = Astro.props; 16 + 17 + // Create temporary file for compilation 18 + const tmpDir = mkdtempSync(join(tmpdir(), 'typelex-')); 19 + const tmpFile = join(tmpDir, 'example.tsp'); 20 + writeFileSync(tmpFile, code); 21 + 22 + let lexiconJson: string; 23 + let lexicon: string; 24 + 25 + try { 26 + lexiconJson = await compileToJson(tmpFile); 27 + lexicon = stringify(JSON.parse(lexiconJson), { maxLength: hero ? 50 : 80 }); 28 + } finally { 29 + rmSync(tmpDir, { recursive: true, force: true }); 30 + } 31 + 32 + const typelexHtml = await highlightCode(code, 'typespec'); 33 + const lexiconHtml = await highlightCode(lexicon, 'json'); 34 + const playgroundUrl = createPlaygroundUrl(code); 35 + 36 + const panelClass = hero ? 'hero-panel' : 'code-panel'; 37 + const headerClass = hero ? 'hero-header' : 'code-header'; 38 + const blockClass = hero ? 'hero-code' : 'code-block'; 39 + --- 40 + 41 + <figure class:list={[hero ? 'hero-comparison' : 'comparison']}> 42 + <div class="comparison-content"> 43 + <div class={panelClass}> 44 + <p class={headerClass}> 45 + Typelex 46 + <a href={playgroundUrl} target="_blank" rel="noopener noreferrer" class="code-playground-link" aria-label="Open in playground"> 47 + <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 48 + <path d="M6.5 3.5C6.5 3.22386 6.72386 3 7 3H13C13.2761 3 13.5 3.22386 13.5 3.5V9.5C13.5 9.77614 13.2761 10 13 10C12.7239 10 12.5 9.77614 12.5 9.5V4.70711L6.85355 10.3536C6.65829 10.5488 6.34171 10.5488 6.14645 10.3536C5.95118 10.1583 5.95118 9.84171 6.14645 9.64645L11.7929 4H7C6.72386 4 6.5 3.77614 6.5 3.5Z" fill="currentColor"/> 49 + <path d="M3 5.5C3 4.67157 3.67157 4 4.5 4H5C5.27614 4 5.5 4.22386 5.5 4.5C5.5 4.77614 5.27614 5 5 5H4.5C4.22386 5 4 5.22386 4 5.5V11.5C4 11.7761 4.22386 12 4.5 12H10.5C10.7761 12 11 11.7761 11 11.5V11C11 10.7239 11.2239 10.5 11.5 10.5C11.7761 10.5 12 10.7239 12 11V11.5C12 12.3284 11.3284 13 10.5 13H4.5C3.67157 13 3 12.3284 3 11.5V5.5Z" fill="currentColor"/> 50 + </svg> 51 + </a> 52 + </p> 53 + <div class={blockClass} set:html={typelexHtml} /> 54 + </div> 55 + <div class={panelClass}> 56 + <p class={headerClass}> 57 + Lexicon 58 + </p> 59 + <div class={blockClass} set:html={lexiconHtml} /> 60 + </div> 61 + </div> 62 + </figure>
+193
packages/website/src/layouts/BaseLayout.astro
··· 1 + --- 2 + interface Props { 3 + title: string; 4 + description?: string; 5 + transparentNav?: boolean; 6 + } 7 + 8 + const { 9 + title, 10 + description = "An experimental TypeSpec syntax for AT Protocol Lexicons. Write Lexicons in a more readable syntax using TypeSpec.", 11 + transparentNav = false 12 + } = Astro.props; 13 + --- 14 + 15 + <!DOCTYPE html> 16 + <html lang="en"> 17 + <head> 18 + <meta charset="utf-8" /> 19 + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 20 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 21 + <meta name="generator" content={Astro.generator} /> 22 + <title>{title}</title> 23 + <meta name="description" content={description} /> 24 + 25 + <!-- Open Graph / Facebook --> 26 + <meta property="og:type" content="website" /> 27 + <meta property="og:url" content="https://typelex.org/" /> 28 + <meta property="og:title" content={title} /> 29 + <meta property="og:description" content={description} /> 30 + <meta property="og:image" content="https://typelex.org/og.png" /> 31 + 32 + <!-- Twitter --> 33 + <meta property="twitter:card" content="summary_large_image" /> 34 + <meta property="twitter:url" content="https://typelex.org/" /> 35 + <meta property="twitter:title" content={title} /> 36 + <meta property="twitter:description" content={description} /> 37 + <meta property="twitter:image" content="https://typelex.org/og.png" /> 38 + </head> 39 + <body> 40 + <nav class:list={["top-nav", { transparent: transparentNav }]}> 41 + <div class="nav-container"> 42 + <a href="/" class="logo">typelex</a> 43 + <div class="nav-links"> 44 + <a href="#install">Install</a> 45 + <a href="https://tangled.org/@danabra.mov/typelex/blob/main/DOCS.md" target="_blank" rel="noopener noreferrer">Docs</a> 46 + <a href="https://playground.typelex.org" target="_blank" rel="noopener noreferrer">Playground</a> 47 + </div> 48 + </div> 49 + </nav> 50 + 51 + <slot /> 52 + 53 + <script> 54 + // Smooth scroll to top when clicking logo 55 + document.addEventListener('DOMContentLoaded', () => { 56 + const logo = document.querySelector('.logo'); 57 + if (logo) { 58 + logo.addEventListener('click', (e) => { 59 + // Allow Ctrl/Cmd+click to open in new tab 60 + if (e.ctrlKey || e.metaKey || e.shiftKey) { 61 + return; 62 + } 63 + e.preventDefault(); 64 + window.scrollTo({ top: 0, behavior: 'smooth' }); 65 + }); 66 + } 67 + }); 68 + </script> 69 + 70 + {transparentNav && ( 71 + <script> 72 + const nav = document.querySelector('.top-nav'); 73 + const heroTitle = document.querySelector('header h1'); 74 + 75 + if (heroTitle && nav) { 76 + const handleScroll = () => { 77 + const titleRect = heroTitle.getBoundingClientRect(); 78 + 79 + if (titleRect.bottom < 16) { 80 + nav.classList.remove('transparent'); 81 + } else { 82 + nav.classList.add('transparent'); 83 + } 84 + }; 85 + 86 + window.addEventListener('scroll', handleScroll, { passive: true }); 87 + handleScroll(); 88 + } 89 + </script> 90 + )} 91 + </body> 92 + </html> 93 + 94 + <style is:global> 95 + * { 96 + margin: 0; 97 + padding: 0; 98 + box-sizing: border-box; 99 + } 100 + 101 + html { 102 + scroll-behavior: smooth; 103 + } 104 + 105 + body { 106 + font-family: system-ui, -apple-system, sans-serif; 107 + line-height: 1.6; 108 + color: #1e293b; 109 + background: #f8fafc; 110 + font-size: 16px; 111 + } 112 + 113 + @media (min-width: 768px) { 114 + body { 115 + font-size: 17px; 116 + } 117 + } 118 + 119 + .top-nav { 120 + position: sticky; 121 + top: 0; 122 + z-index: 100; 123 + background: rgba(255, 255, 255, 0.8); 124 + backdrop-filter: blur(10px); 125 + border-bottom: 1px solid #e2e8f0; 126 + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 127 + transition: all 0.3s ease; 128 + } 129 + 130 + .top-nav.transparent { 131 + background: rgba(255, 255, 255, 0); 132 + backdrop-filter: none; 133 + border-bottom-color: transparent; 134 + box-shadow: none; 135 + } 136 + 137 + .top-nav.transparent .logo { 138 + opacity: 0; 139 + transform: translateY(-100%); 140 + } 141 + 142 + .top-nav.transparent .nav-links a { 143 + opacity: 0.7; 144 + } 145 + 146 + .nav-container { 147 + max-width: 1104px; 148 + margin: 0 auto; 149 + padding: 1rem 2rem; 150 + display: flex; 151 + justify-content: space-between; 152 + align-items: center; 153 + } 154 + 155 + @media (min-width: 768px) { 156 + .nav-container { 157 + padding: 1rem 2rem; 158 + } 159 + } 160 + 161 + .logo { 162 + font-size: 1.25rem; 163 + font-weight: 800; 164 + background: linear-gradient(90deg, #4a9eff 0%, #7a8ef7 40%, #ff85c1 70%, #9b7ef7 100%); 165 + -webkit-background-clip: text; 166 + -webkit-text-fill-color: transparent; 167 + background-clip: text; 168 + text-decoration: none; 169 + transition: all 0.3s ease; 170 + padding-left: 80px; 171 + padding-right: 80px; 172 + margin-left: -80px; 173 + margin-right: -80px; 174 + } 175 + 176 + .nav-links { 177 + display: flex; 178 + gap: 1.5rem; 179 + align-items: center; 180 + } 181 + 182 + .nav-links a { 183 + color: #64748b; 184 + text-decoration: none; 185 + font-weight: 500; 186 + transition: all 0.3s ease; 187 + font-size: 0.9375rem; 188 + } 189 + 190 + .nav-links a:hover { 191 + color: #7a8ef7; 192 + } 193 + </style>
+445
packages/website/src/layouts/DocsLayout.astro
··· 1 + --- 2 + import BaseLayout from './BaseLayout.astro'; 3 + 4 + interface Props { 5 + title: string; 6 + } 7 + 8 + const { title } = Astro.props; 9 + --- 10 + 11 + <BaseLayout title={`${title} โ€“ typelex`}> 12 + <div class="docs-container"> 13 + <aside class="sidebar"> 14 + <div class="sidebar-content"> 15 + <h3>Documentation</h3> 16 + <nav class="sidebar-nav"> 17 + <a href="/docs" class:list={[{ active: Astro.url.pathname === '/docs' || Astro.url.pathname === '/docs/' }]}>Introduction</a> 18 + </nav> 19 + </div> 20 + </aside> 21 + 22 + <main class="docs-main"> 23 + <article class="docs-content"> 24 + <h1>{title}</h1> 25 + <slot /> 26 + </article> 27 + </main> 28 + </div> 29 + 30 + <script> 31 + document.addEventListener('DOMContentLoaded', () => { 32 + const scrollables = document.querySelectorAll('.code-panel:last-child .code-block'); 33 + 34 + // Update gradient mask based on scroll position 35 + scrollables.forEach(block => { 36 + const updateMask = () => { 37 + const isAtBottom = block.scrollHeight - block.scrollTop <= block.clientHeight + 5; 38 + if (isAtBottom) { 39 + block.style.maskImage = 'none'; 40 + block.style.webkitMaskImage = 'none'; 41 + } else { 42 + block.style.maskImage = 'linear-gradient(to bottom, black calc(100% - 150px), transparent 100%)'; 43 + block.style.webkitMaskImage = 'linear-gradient(to bottom, black calc(100% - 150px), transparent 100%)'; 44 + } 45 + }; 46 + 47 + block.addEventListener('scroll', updateMask); 48 + updateMask(); // Initial check 49 + }); 50 + 51 + // Freeze inner scrollable blocks while scrolling the page 52 + let scrollTimeout; 53 + const freezeInnerScroll = () => { 54 + document.body.classList.add('outer-scrolling'); 55 + 56 + clearTimeout(scrollTimeout); 57 + scrollTimeout = setTimeout(() => { 58 + document.body.classList.remove('outer-scrolling'); 59 + }, 150); 60 + }; 61 + 62 + // Listen for both scroll and wheel events to catch scrolling early 63 + window.addEventListener('scroll', freezeInnerScroll, { passive: true }); 64 + window.addEventListener('wheel', (e) => { 65 + // Only freeze if the wheel event is not inside a scrollable block 66 + const target = e.target; 67 + const isInsideScrollable = target.closest('.code-panel:last-child .code-block'); 68 + if (!isInsideScrollable) { 69 + freezeInnerScroll(); 70 + } 71 + }, { passive: true }); 72 + }); 73 + </script> 74 + </BaseLayout> 75 + 76 + <style is:global> 77 + .docs-container { 78 + max-width: 1400px; 79 + margin: 0 auto; 80 + display: grid; 81 + grid-template-columns: 250px 1fr; 82 + gap: 3rem; 83 + padding: 2rem 1.5rem; 84 + } 85 + 86 + @media (max-width: 968px) { 87 + .docs-container { 88 + grid-template-columns: 1fr; 89 + gap: 2rem; 90 + } 91 + 92 + .sidebar { 93 + position: static; 94 + border-right: none; 95 + border-bottom: 1px solid #e2e8f0; 96 + padding-bottom: 2rem; 97 + } 98 + } 99 + 100 + .sidebar { 101 + position: sticky; 102 + top: 5rem; 103 + height: fit-content; 104 + } 105 + 106 + .sidebar-content h3 { 107 + font-size: 0.875rem; 108 + text-transform: uppercase; 109 + letter-spacing: 0.05em; 110 + color: #94a3b8; 111 + margin-bottom: 1rem; 112 + font-weight: 600; 113 + } 114 + 115 + .sidebar-nav { 116 + display: flex; 117 + flex-direction: column; 118 + gap: 0.25rem; 119 + } 120 + 121 + .sidebar-nav a { 122 + color: #64748b; 123 + text-decoration: none; 124 + padding: 0.5rem 0.75rem; 125 + border-radius: 6px; 126 + transition: all 0.2s ease; 127 + font-weight: 500; 128 + } 129 + 130 + .sidebar-nav a:hover { 131 + background: #f1f5f9; 132 + color: #1e293b; 133 + } 134 + 135 + .sidebar-nav a.active { 136 + background: linear-gradient(135deg, #7a8ef7 0%, #9483f7 70%, #b87ed8 100%); 137 + color: white; 138 + font-weight: 600; 139 + } 140 + 141 + .docs-main { 142 + min-width: 0; 143 + max-width: 800px; 144 + } 145 + 146 + .docs-content { 147 + padding-bottom: 4rem; 148 + } 149 + 150 + .docs-content h1 { 151 + font-size: 2.5rem; 152 + font-weight: 800; 153 + margin: 0 0 2rem 0; 154 + background: linear-gradient(90deg, #4a9eff 0%, #7a8ef7 40%, #ff85c1 70%, #9b7ef7 100%); 155 + -webkit-background-clip: text; 156 + -webkit-text-fill-color: transparent; 157 + background-clip: text; 158 + } 159 + 160 + .docs-content h2 { 161 + font-size: 1.875rem; 162 + font-weight: 700; 163 + margin-top: 3rem; 164 + margin-bottom: 1.5rem; 165 + color: #1e293b; 166 + } 167 + 168 + .docs-content h3 { 169 + font-size: 1.5rem; 170 + font-weight: 600; 171 + margin-top: 2rem; 172 + margin-bottom: 1rem; 173 + color: #334155; 174 + } 175 + 176 + .docs-content h4 { 177 + font-size: 1.25rem; 178 + font-weight: 600; 179 + margin-top: 1.5rem; 180 + margin-bottom: 0.75rem; 181 + color: #475569; 182 + } 183 + 184 + .docs-content p { 185 + margin-bottom: 1.25rem; 186 + line-height: 1.8; 187 + color: #475569; 188 + } 189 + 190 + .docs-content a { 191 + color: #6366f1; 192 + text-decoration: none; 193 + border-bottom: 1px solid #c7d2fe; 194 + transition: all 0.2s ease; 195 + } 196 + 197 + .docs-content a:hover { 198 + color: #4f46e5; 199 + border-bottom-color: #6366f1; 200 + } 201 + 202 + .docs-content ul, .docs-content ol { 203 + margin-bottom: 1.5rem; 204 + padding-left: 2rem; 205 + } 206 + 207 + .docs-content li { 208 + margin-bottom: 0.5rem; 209 + line-height: 1.8; 210 + color: #475569; 211 + } 212 + 213 + .docs-content code { 214 + font-family: 'Monaco', 'Menlo', monospace; 215 + font-size: 0.875em; 216 + background: #f1f5f9; 217 + padding: 0.2em 0.4em; 218 + border-radius: 4px; 219 + color: #e879b9; 220 + } 221 + 222 + .docs-content pre { 223 + background: #1e1b29; 224 + border-radius: 8px; 225 + padding: 1rem; 226 + overflow-x: auto; 227 + margin-bottom: 1.5rem; 228 + } 229 + 230 + @media (min-width: 768px) { 231 + .docs-content pre { 232 + padding: 1.25rem; 233 + } 234 + } 235 + 236 + .docs-content pre code { 237 + background: transparent; 238 + padding: 0; 239 + color: inherit; 240 + font-size: 0.75rem; 241 + line-height: 1.6; 242 + } 243 + 244 + @media (min-width: 768px) { 245 + .docs-content pre code { 246 + font-size: 0.875rem; 247 + line-height: 1.7; 248 + } 249 + } 250 + 251 + .docs-content table { 252 + width: 100%; 253 + border-collapse: collapse; 254 + margin-bottom: 1.5rem; 255 + font-size: 0.9375rem; 256 + } 257 + 258 + .docs-content th, 259 + .docs-content td { 260 + text-align: left; 261 + padding: 0.75rem 1rem; 262 + border: 1px solid #e2e8f0; 263 + } 264 + 265 + .docs-content th { 266 + background: #f8fafc; 267 + font-weight: 600; 268 + color: #1e293b; 269 + } 270 + 271 + .docs-content td { 272 + color: #475569; 273 + } 274 + 275 + .docs-content blockquote { 276 + border-left: 4px solid #7a8ef7; 277 + padding-left: 1.5rem; 278 + margin: 1.5rem 0; 279 + color: #64748b; 280 + font-style: italic; 281 + } 282 + 283 + .comparison { 284 + background: #1e1b29; 285 + border-radius: 12px; 286 + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 287 + overflow: hidden; 288 + margin: 2rem 0; 289 + } 290 + 291 + .comparison-content { 292 + position: relative; 293 + padding: 0.75rem; 294 + display: grid; 295 + grid-template-columns: 1fr; 296 + gap: 1.5rem; 297 + } 298 + 299 + @media (min-width: 768px) { 300 + .comparison-content { 301 + padding: 1rem; 302 + grid-template-columns: 1fr 1fr; 303 + gap: 2rem; 304 + } 305 + } 306 + 307 + .code-panel { 308 + position: relative; 309 + min-width: 0; 310 + overflow: hidden; 311 + text-align: left; 312 + } 313 + 314 + .code-header { 315 + padding: 0.5rem 1rem; 316 + background: #252231; 317 + border-radius: 8px 8px 0 0; 318 + font-size: 0.75rem; 319 + font-weight: 600; 320 + text-transform: uppercase; 321 + letter-spacing: 0.05em; 322 + margin: 0; 323 + color: #94a3b8; 324 + display: flex; 325 + align-items: center; 326 + justify-content: space-between; 327 + } 328 + 329 + @media (min-width: 768px) { 330 + .code-header { 331 + font-size: 0.8125rem; 332 + padding: 0.625rem 1rem; 333 + } 334 + } 335 + 336 + .code-block { 337 + position: relative; 338 + text-align: left; 339 + } 340 + 341 + .code-panel:last-child .code-block { 342 + overflow-y: auto; 343 + max-height: 400px; 344 + -webkit-mask-image: linear-gradient(to bottom, black calc(100% - 100px), transparent 100%); 345 + mask-image: linear-gradient(to bottom, black calc(100% - 100px), transparent 100%); 346 + } 347 + 348 + /* Freeze inner scrollables when scrolling the page */ 349 + body.outer-scrolling .code-panel:last-child .code-block { 350 + pointer-events: none; 351 + overflow-y: hidden; 352 + } 353 + 354 + @media (min-width: 768px) { 355 + .code-panel:first-child { 356 + position: relative; 357 + z-index: 1; 358 + } 359 + 360 + .code-panel:last-child { 361 + position: absolute; 362 + top: 1rem; 363 + bottom: 1rem; 364 + right: 1rem; 365 + left: calc(50% + 1rem); 366 + } 367 + 368 + .code-panel:last-child .code-block { 369 + max-height: none; 370 + height: 100%; 371 + padding-bottom: 1.5rem; 372 + -webkit-mask-image: linear-gradient(to bottom, black calc(100% - 150px), transparent 100%); 373 + mask-image: linear-gradient(to bottom, black calc(100% - 150px), transparent 100%); 374 + } 375 + 376 + body.outer-scrolling .code-panel:last-child .code-block { 377 + pointer-events: none; 378 + overflow-y: hidden; 379 + } 380 + } 381 + 382 + .code-block pre { 383 + margin: 0; 384 + padding: 1rem; 385 + background: transparent !important; 386 + overflow-x: auto; 387 + overflow-y: visible; 388 + -webkit-overflow-scrolling: touch; 389 + max-width: 100%; 390 + } 391 + 392 + @media (min-width: 768px) { 393 + .code-block pre { 394 + padding: 1.5rem; 395 + } 396 + } 397 + 398 + .code-block code { 399 + font-family: 'Monaco', 'Menlo', monospace; 400 + font-size: 0.75rem !important; 401 + line-height: 1.6; 402 + white-space: pre; 403 + text-align: left; 404 + } 405 + 406 + @media (min-width: 768px) { 407 + .code-block code { 408 + font-size: 0.875rem !important; 409 + } 410 + } 411 + 412 + .code-block pre code, 413 + .code-block pre code * { 414 + font-size: inherit !important; 415 + } 416 + 417 + .code-playground-link { 418 + display: inline-flex; 419 + align-items: center; 420 + justify-content: center; 421 + color: #94a3b8; 422 + transition: all 0.2s ease; 423 + text-decoration: none; 424 + opacity: 0.4; 425 + padding: 0.125rem; 426 + border-bottom: none !important; 427 + } 428 + 429 + .code-playground-link:hover { 430 + color: #c7d2fe; 431 + opacity: 1; 432 + } 433 + 434 + .code-playground-link svg { 435 + width: 1rem; 436 + height: 1rem; 437 + } 438 + 439 + @media (min-width: 768px) { 440 + .code-playground-link svg { 441 + width: 1.125rem; 442 + height: 1.125rem; 443 + } 444 + } 445 + </style>
+45 -234
packages/website/src/pages/index.astro
··· 1 1 --- 2 + import BaseLayout from '../layouts/BaseLayout.astro'; 3 + import ComparisonBlock from '../components/ComparisonBlock.astro'; 2 4 import { highlightCode } from '../utils/shiki'; 3 - import { compileToJson } from '../utils/compile'; 4 5 import { createPlaygroundUrl } from '../utils/playground-url'; 5 - import stringify from 'json-stringify-pretty-compact'; 6 - import { mkdtempSync, writeFileSync, rmSync } from 'fs'; 7 - import { join } from 'path'; 8 - import { tmpdir } from 'os'; 9 6 10 7 // Define examples inline 11 8 const examples = [ 12 9 { 13 10 title: "Records and properties", 14 - typelex: `import "@typelex/emitter"; 11 + code: `import "@typelex/emitter"; 15 12 16 13 namespace fm.teal.alpha.feed.play { 17 14 @rec("tid") 18 15 model Main { 19 16 @maxItems(10) 20 17 artistNames?: string[]; 21 - 18 + 22 19 @required 23 20 @minLength(1) 24 21 @maxLength(256) ··· 31 28 }, 32 29 { 33 30 title: "Refs and unions", 34 - typelex: `import "@typelex/emitter"; 31 + code: `import "@typelex/emitter"; 35 32 36 33 namespace app.bsky.feed.post { 37 34 @rec("tid") ··· 66 63 }, 67 64 { 68 65 title: "Queries and params", 69 - typelex: `import "@typelex/emitter"; 66 + code: `import "@typelex/emitter"; 70 67 71 68 namespace com.atproto.repo.listRecords { 72 69 @query ··· 99 96 }, 100 97 ]; 101 98 102 - // Compile examples 103 - const highlighted = await Promise.all( 104 - examples.map(async (ex) => { 105 - // Create temporary file for compilation 106 - const tmpDir = mkdtempSync(join(tmpdir(), 'typelex-')); 107 - const tmpFile = join(tmpDir, 'example.tsp'); 108 - writeFileSync(tmpFile, ex.typelex); 109 - 110 - try { 111 - const lexiconJson = await compileToJson(tmpFile); 112 - const lexicon = stringify(JSON.parse(lexiconJson), { maxLength: 80 }); 113 - 114 - return { 115 - ...ex, 116 - typelexHtml: await highlightCode(ex.typelex, 'typespec'), 117 - lexiconHtml: await highlightCode(lexicon, 'json'), 118 - playgroundUrl: createPlaygroundUrl(ex.typelex), 119 - }; 120 - } finally { 121 - rmSync(tmpDir, { recursive: true, force: true }); 122 - } 123 - }) 124 - ); 125 - --- 126 - 127 - <!DOCTYPE html> 128 - <html lang="en"> 129 - <head> 130 - <meta charset="utf-8" /> 131 - <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> 132 - <meta name="viewport" content="width=device-width, initial-scale=1" /> 133 - <meta name="generator" content={Astro.generator} /> 134 - <title>typelex โ€“ An experimental TypeSpec syntax for Lexicon</title> 135 - <meta name="description" content="An experimental TypeSpec syntax for AT Protocol Lexicons. Write Lexicons in a more readable syntax using TypeSpec." /> 136 - 137 - <!-- Open Graph / Facebook --> 138 - <meta property="og:type" content="website" /> 139 - <meta property="og:url" content="https://typelex.org/" /> 140 - <meta property="og:title" content="typelex โ€“ An experimental TypeSpec syntax for Lexicon" /> 141 - <meta property="og:description" content="An experimental TypeSpec syntax for AT Protocol Lexicons. Write Lexicons in a more readable syntax using TypeSpec." /> 142 - <meta property="og:image" content="https://typelex.org/og.png" /> 143 - 144 - <!-- Twitter --> 145 - <meta property="twitter:card" content="summary_large_image" /> 146 - <meta property="twitter:url" content="https://typelex.org/" /> 147 - <meta property="twitter:title" content="typelex โ€“ An experimental TypeSpec syntax for Lexicon" /> 148 - <meta property="twitter:description" content="An experimental TypeSpec syntax for AT Protocol Lexicons. Write Lexicons in a more readable syntax using TypeSpec." /> 149 - <meta property="twitter:image" content="https://typelex.org/og.png" /> 150 - </head> 151 - <body> 152 - <main class="container"> 153 - <header> 154 - <h1>typelex</h1> 155 - <p class="tagline">An experimental <a href="https://typespec.io" target="_blank" rel="noopener noreferrer">TypeSpec</a> syntax for <a href="https://atproto.com/specs/lexicon" target="_blank" rel="noopener noreferrer">Lexicon</a></p> 156 - 157 - <figure class="hero-comparison"> 158 - <div class="comparison-content"> 159 - <div class="hero-panel"> 160 - <p class="hero-header"> 161 - Typelex 162 - <a href={createPlaygroundUrl(`import "@typelex/emitter"; 99 + const heroCode = `import "@typelex/emitter"; 163 100 164 101 namespace app.bsky.actor.profile { 165 102 @rec("self") ··· 172 109 @maxGraphemes(256) 173 110 description?: string; 174 111 } 175 - }`)} target="_blank" rel="noopener noreferrer" class="code-playground-link" aria-label="Open in playground"> 176 - <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 177 - <path d="M6.5 3.5C6.5 3.22386 6.72386 3 7 3H13C13.2761 3 13.5 3.22386 13.5 3.5V9.5C13.5 9.77614 13.2761 10 13 10C12.7239 10 12.5 9.77614 12.5 9.5V4.70711L6.85355 10.3536C6.65829 10.5488 6.34171 10.5488 6.14645 10.3536C5.95118 10.1583 5.95118 9.84171 6.14645 9.64645L11.7929 4H7C6.72386 4 6.5 3.77614 6.5 3.5Z" fill="currentColor"/> 178 - <path d="M3 5.5C3 4.67157 3.67157 4 4.5 4H5C5.27614 4 5.5 4.22386 5.5 4.5C5.5 4.77614 5.27614 5 5 5H4.5C4.22386 5 4 5.22386 4 5.5V11.5C4 11.7761 4.22386 12 4.5 12H10.5C10.7761 12 11 11.7761 11 11.5V11C11 10.7239 11.2239 10.5 11.5 10.5C11.7761 10.5 12 10.7239 12 11V11.5C12 12.3284 11.3284 13 10.5 13H4.5C3.67157 13 3 12.3284 3 11.5V5.5Z" fill="currentColor"/> 179 - </svg> 180 - </a> 181 - </p> 182 - <div class="hero-code" set:html={await highlightCode(`import "@typelex/emitter"; 112 + }`; 113 + 114 + const installCode = `import "@typelex/emitter"; 115 + import "./externals.tsp"; 183 116 184 - namespace app.bsky.actor.profile { 185 - @rec("self") 117 + namespace com.myapp.example.profile { 118 + /** My profile. */ 119 + @rec("literal:self") 186 120 model Main { 187 - @maxLength(64) 188 - @maxGraphemes(64) 189 - displayName?: string; 190 - 191 - @maxLength(256) 121 + /** Free-form profile description.*/ 192 122 @maxGraphemes(256) 193 123 description?: string; 194 124 } 195 - }`, 'typespec')} /> 196 - </div> 197 - <div class="hero-panel"> 198 - <p class="hero-header"> 199 - Lexicon 200 - </p> 201 - <div class="hero-code" set:html={await highlightCode(stringify({ 202 - "lexicon": 1, 203 - "id": "app.bsky.actor.profile", 204 - "defs": { 205 - "main": { 206 - "type": "record", 207 - "key": "self", 208 - "record": { 209 - "type": "object", 210 - "properties": { 211 - "displayName": { 212 - "type": "string", 213 - "maxLength": 64, 214 - "maxGraphemes": 64 215 - }, 216 - "description": { 217 - "type": "string", 218 - "maxLength": 256, 219 - "maxGraphemes": 256 220 - } 221 - } 222 - } 223 - } 224 - } 225 - }, { maxLength: 50 }), 'json')} /> 226 - </div> 227 - </div> 228 - </figure> 125 + }`; 126 + --- 127 + 128 + <BaseLayout title="typelex โ€“ An experimental TypeSpec syntax for Lexicon" transparentNav={true}> 129 + <main class="container"> 130 + <header> 131 + <h1>typelex</h1> 132 + <p class="tagline">An experimental <a href="https://typespec.io" target="_blank" rel="noopener noreferrer">TypeSpec</a> syntax for <a href="https://atproto.com/specs/lexicon" target="_blank" rel="noopener noreferrer">Lexicon</a></p> 133 + 134 + <ComparisonBlock code={heroCode} hero={true} /> 229 135 230 136 <p class="hero-description"> 231 137 Typelex lets you write AT <a target="_blank" href="https://atproto.com/specs/lexicon">Lexicons</a> in a more readable syntax. <br /> ··· 234 140 235 141 <nav class="hero-actions"> 236 142 <a href="#install" class="install-cta">Try It</a> 237 - <a href="https://tangled.org/@danabra.mov/typelex/blob/main/DOCS.md" target="_blank" rel="noopener noreferrer" class="star-btn"> 143 + <a target="_blank" href="https://tangled.org/@danabra.mov/typelex/blob/main/DOCS.md" class="star-btn"> 238 144 Read Docs 239 145 </a> 240 146 </nav> ··· 242 148 243 149 <hr class="separator" /> 244 150 245 - {highlighted.map(({ title, typelexHtml, lexiconHtml, playgroundUrl }) => ( 151 + {examples.map(({ title, code }) => ( 246 152 <section> 247 153 <h2>{title}</h2> 248 - <figure class="comparison"> 249 - <div class="comparison-content"> 250 - <div class="code-panel"> 251 - <p class="code-header"> 252 - Typelex 253 - <a href={playgroundUrl} target="_blank" rel="noopener noreferrer" class="code-playground-link" aria-label="Open in playground"> 254 - <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 255 - <path d="M6.5 3.5C6.5 3.22386 6.72386 3 7 3H13C13.2761 3 13.5 3.22386 13.5 3.5V9.5C13.5 9.77614 13.2761 10 13 10C12.7239 10 12.5 9.77614 12.5 9.5V4.70711L6.85355 10.3536C6.65829 10.5488 6.34171 10.5488 6.14645 10.3536C5.95118 10.1583 5.95118 9.84171 6.14645 9.64645L11.7929 4H7C6.72386 4 6.5 3.77614 6.5 3.5Z" fill="currentColor"/> 256 - <path d="M3 5.5C3 4.67157 3.67157 4 4.5 4H5C5.27614 4 5.5 4.22386 5.5 4.5C5.5 4.77614 5.27614 5 5 5H4.5C4.22386 5 4 5.22386 4 5.5V11.5C4 11.7761 4.22386 12 4.5 12H10.5C10.7761 12 11 11.7761 11 11.5V11C11 10.7239 11.2239 10.5 11.5 10.5C11.7761 10.5 12 10.7239 12 11V11.5C12 12.3284 11.3284 13 10.5 13H4.5C3.67157 13 3 12.3284 3 11.5V5.5Z" fill="currentColor"/> 257 - </svg> 258 - </a> 259 - </p> 260 - <div class="code-block" set:html={typelexHtml} /> 261 - </div> 262 - <div class="code-panel"> 263 - <p class="code-header"> 264 - Lexicon 265 - </p> 266 - <div class="code-block" set:html={lexiconHtml} /> 267 - </div> 268 - </div> 269 - </figure> 154 + <ComparisonBlock code={code} /> 270 155 </section> 271 156 ))} 272 157 ··· 282 167 <div class="step-number">0</div> 283 168 <div class="step-content"> 284 169 <h3>Try the playground</h3> 285 - <p class="step-description">Experiment with typelex in your browser before installing.</p> 286 170 <a href="https://playground.typelex.org" target="_blank" rel="noopener noreferrer" class="playground-button"> 287 171 Open Playground 288 172 </a> 173 + <p class="step-description">Experiment with typelex in your browser before installing.</p> 289 174 </div> 290 175 </div> 291 176 292 177 <div class="install-step"> 293 178 <div class="step-number">1</div> 294 179 <div class="step-content"> 295 - <h3>Install packages</h3> 296 - <figure class="install-box" set:html={await highlightCode('npm install -D @typespec/compiler @typelex/emitter', 'bash')} /> 180 + <h3>Add typelex to your app</h3> 181 + <figure class="install-box" set:html={await highlightCode('npx @typelex/cli init', 'bash')} /> 182 + <p class="step-description">This will add a few things to your <code>package.json</code> and create a <code>typelex/</code> folder.</p> 297 183 </div> 298 184 </div> 299 185 300 186 <div class="install-step"> 301 187 <div class="step-number">2</div> 302 188 <div class="step-content"> 303 - <h3>Create <code>typelex/main.tsp</code></h3> 304 - <figure class="install-box install-box-with-link"> 305 - <a href={createPlaygroundUrl(`import "@typelex/emitter"; 306 - 307 - namespace com.example.actor.profile { 308 - /** My profile. */ 309 - @rec("literal:self") 310 - model Main { 311 - /** Free-form profile description.*/ 312 - @maxGraphemes(256) 313 - description?: string; 314 - } 315 - }`)} target="_blank" rel="noopener noreferrer" class="install-playground-link" aria-label="Open in playground"> 316 - <svg width="14" height="14" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> 317 - <path d="M6.5 3.5C6.5 3.22386 6.72386 3 7 3H13C13.2761 3 13.5 3.22386 13.5 3.5V9.5C13.5 9.77614 13.2761 10 13 10C12.7239 10 12.5 9.77614 12.5 9.5V4.70711L6.85355 10.3536C6.65829 10.5488 6.34171 10.5488 6.14645 10.3536C5.95118 10.1583 5.95118 9.84171 6.14645 9.64645L11.7929 4H7C6.72386 4 6.5 3.77614 6.5 3.5Z" fill="currentColor"/> 318 - <path d="M3 5.5C3 4.67157 3.67157 4 4.5 4H5C5.27614 4 5.5 4.22386 5.5 4.5C5.5 4.77614 5.27614 5 5 5H4.5C4.22386 5 4 5.22386 4 5.5V11.5C4 11.7761 4.22386 12 4.5 12H10.5C10.7761 12 11 11.7761 11 11.5V11C11 10.7239 11.2239 10.5 11.5 10.5C11.7761 10.5 12 10.7239 12 11V11.5C12 12.3284 11.3284 13 10.5 13H4.5C3.67157 13 3 12.3284 3 11.5V5.5Z" fill="currentColor"/> 319 - </svg> 320 - </a> 321 - <div set:html={await highlightCode(`import "@typelex/emitter"; 322 - 323 - namespace com.example.actor.profile { 324 - /** My profile. */ 325 - @rec("literal:self") 326 - model Main { 327 - /** Free-form profile description.*/ 328 - @maxGraphemes(256) 329 - description?: string; 330 - } 331 - }`, 'typespec')} /> 332 - </figure> 189 + <h3>Write your lexicons in <code>typelex/main.tsp</code></h3> 190 + <figure class="install-box" set:html={await highlightCode(installCode, 'typespec')} /> 191 + <p class="step-description">Your app's lexicons go here. They may reference any external ones from <code>lexicons/</code>.</p> 333 192 </div> 334 - <p class="step-description">Or grab any example Lexicon <a target=_blank href="https://playground.typelex.org/">from the Playground</a>.</p> 335 193 </div> 336 194 337 195 <div class="install-step"> 338 196 <div class="step-number">3</div> 339 197 <div class="step-content"> 340 - <h3>Create <code><a href="https://typespec.io/docs/handbook/configuration/configuration/" target="_blank" rel="noopener noreferrer">tspconfig.yaml</a></code></h3> 341 - <figure class="install-box" set:html={await highlightCode(`emit: 342 - - "@typelex/emitter" 343 - options: 344 - "@typelex/emitter": 345 - output-dir: "./lexicons"`, 'yaml')} /> 198 + <h3>Compile your lexicons</h3> 199 + <figure class="install-box" set:html={await highlightCode(`npm run build:typelex`, 'bash')} /> 200 + <p class="step-description">Your appโ€™s compiled lexicons will appear in <code>lexicons/</code> alongside any external ones.</p> 346 201 </div> 347 202 </div> 348 203 349 204 <div class="install-step"> 350 205 <div class="step-number">4</div> 351 206 <div class="step-content"> 352 - <h3>Add a build script to <code>package.json</code></h3> 353 - <figure class="install-box" set:html={await highlightCode(`{ 354 - "scripts": { 355 - // ... 356 - "build:lexicons": "tsp compile typelex/main.tsp" 357 - } 358 - }`, 'json')} /> 359 - </div> 360 - </div> 361 - 362 - <div class="install-step"> 363 - <div class="step-number">5</div> 364 - <div class="step-content"> 365 - <h3>Generate Lexicon files</h3> 366 - <figure class="install-box" set:html={await highlightCode(`npm run build:lexicons`, 'bash')} /> 367 - <p class="step-description">Lexicon files will be generated in the <code>output-dir</code> from your <code>tspconfig.yaml</code> config.</p> 368 - </div> 369 - </div> 370 - 371 - <div class="install-step"> 372 - <div class="step-number">6</div> 373 - <div class="step-content"> 374 207 <h3>Set up VS Code</h3> 375 208 <p class="step-description">Install the <a href="https://typespec.io/docs/introduction/editor/vscode/" target="_blank" rel="noopener noreferrer">TypeSpec for VS Code extension</a> for syntax highlighting and IntelliSense.</p> 376 209 </div> 377 210 </div> 378 211 379 212 <div class="install-step"> 380 - <div class="step-number">7</div> 213 + <div class="step-number">5</div> 381 214 <div class="step-content"> 382 - <h3>Read the docs</h3> 215 + <h3>Learn more</h3> 383 216 <p class="step-description">Check out the <a href="https://tangled.org/@danabra.mov/typelex/blob/main/DOCS.md" target="_blank" rel="noopener noreferrer">documentation</a> to learn more.</p> 384 217 </div> 385 218 </div> ··· 392 225 <p>This is my personal hobby project and is not affiliated with AT or endorsed by anyone.</p> 393 226 <p>Who knows if this is a good idea?</p> 394 227 </footer> 395 - </main> 228 + </main> 396 229 397 - <script> 230 + <script> 398 231 document.addEventListener('DOMContentLoaded', () => { 399 232 const scrollables = document.querySelectorAll('.code-panel:last-child .code-block, .hero-panel:last-child .hero-code'); 400 233 ··· 437 270 } 438 271 }, { passive: true }); 439 272 }); 440 - </script> 441 - </body> 442 - </html> 273 + </script> 274 + </BaseLayout> 443 275 444 276 <style is:global> 445 - * { 446 - margin: 0; 447 - padding: 0; 448 - box-sizing: border-box; 449 - } 450 - 451 - html { 452 - scroll-behavior: smooth; 453 - } 454 - 455 277 body { 456 - font-family: system-ui, -apple-system, sans-serif; 457 - line-height: 1.6; 458 - color: #1e293b; 459 - background: #f8fafc; 460 - font-size: 16px; 461 278 position: relative; 462 279 overflow-x: hidden; 463 280 } ··· 473 290 border-radius: 50%; 474 291 pointer-events: none; 475 292 z-index: 0; 476 - } 477 - 478 - @media (min-width: 768px) { 479 - body { 480 - font-size: 17px; 481 - } 482 293 } 483 294 484 295 .container { ··· 781 592 .install-section { 782 593 margin: 0; 783 594 padding: 0; 595 + scroll-margin-top: 5rem; 784 596 } 785 597 786 598 .install-section h2 { ··· 1218 1030 1219 1031 .playground-button { 1220 1032 display: inline-block; 1221 - margin-top: 1.25rem; 1222 1033 padding: 0.875rem 2rem; 1223 1034 background: linear-gradient(135deg, #7a8ef7 0%, #9483f7 70%, #b87ed8 100%); 1224 1035 color: white;
+46 -3
pnpm-lock.yaml
··· 12 12 specifier: ^5.0.0 13 13 version: 5.9.3 14 14 15 + packages/cli: 16 + dependencies: 17 + '@typespec/compiler': 18 + specifier: ^1.4.0 19 + version: 1.4.0(@types/node@20.19.19) 20 + globby: 21 + specifier: ^14.0.0 22 + version: 14.1.0 23 + picocolors: 24 + specifier: ^1.1.1 25 + version: 1.1.1 26 + yargs: 27 + specifier: ^18.0.0 28 + version: 18.0.0 29 + devDependencies: 30 + '@typelex/emitter': 31 + specifier: workspace:* 32 + version: link:../emitter 33 + '@types/node': 34 + specifier: ^20.0.0 35 + version: 20.19.19 36 + '@types/yargs': 37 + specifier: ^17.0.33 38 + version: 17.0.33 39 + typescript: 40 + specifier: ^5.0.0 41 + version: 5.9.3 42 + vitest: 43 + specifier: ^1.0.0 44 + version: 1.6.1(@types/node@20.19.19) 45 + 15 46 packages/emitter: 16 47 dependencies: 17 48 '@typespec/compiler': ··· 48 79 '@atproto/xrpc-server': 49 80 specifier: ^0.9.5 50 81 version: 0.9.5 82 + '@typelex/cli': 83 + specifier: workspace:* 84 + version: link:../cli 51 85 '@typelex/emitter': 52 86 specifier: workspace:* 53 87 version: link:../emitter 54 - '@typespec/compiler': 55 - specifier: ^1.4.0 56 - version: 1.4.0(@types/node@20.19.19) 57 88 devDependencies: 58 89 typescript: 59 90 specifier: ^5.0.0 ··· 1705 1736 1706 1737 '@types/unist@3.0.3': 1707 1738 resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} 1739 + 1740 + '@types/yargs-parser@21.0.3': 1741 + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} 1742 + 1743 + '@types/yargs@17.0.33': 1744 + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} 1708 1745 1709 1746 '@typespec/asset-emitter@0.74.0': 1710 1747 resolution: {integrity: sha512-DWIdlSNhRgBeZ8exfqubfUn0H6mRg4gr0s7zLTdBMUEDHL3Yh0ljnRPkd8AXTZhoW3maTFT69loWTrqx09T5oQ==} ··· 7482 7519 csstype: 3.1.3 7483 7520 7484 7521 '@types/unist@3.0.3': {} 7522 + 7523 + '@types/yargs-parser@21.0.3': {} 7524 + 7525 + '@types/yargs@17.0.33': 7526 + dependencies: 7527 + '@types/yargs-parser': 21.0.3 7485 7528 7486 7529 '@typespec/asset-emitter@0.74.0(@typespec/compiler@1.4.0(@types/node@20.19.19))': 7487 7530 dependencies:
+229
scripts/publish-all.sh
··· 1 + #!/bin/bash 2 + set -e 3 + 4 + # Usage: ./scripts/publish-all.sh <version> [--dry] 5 + # Example: ./scripts/publish-all.sh 0.4.0 6 + # Example: ./scripts/publish-all.sh 0.4.0 --dry 7 + 8 + if [ -z "$1" ]; then 9 + echo "Error: Version argument required" 10 + echo "Usage: ./scripts/publish-all.sh <version> [--dry]" 11 + echo "Example: ./scripts/publish-all.sh 0.4.0" 12 + echo "Example: ./scripts/publish-all.sh 0.4.0 --dry" 13 + exit 1 14 + fi 15 + 16 + VERSION="$1" 17 + DRY_RUN=false 18 + 19 + if [ "$2" = "--dry" ]; then 20 + DRY_RUN=true 21 + fi 22 + 23 + # Validate version format (basic semver check) 24 + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then 25 + echo "Error: Invalid version format. Use semver format (e.g., 0.4.0 or 0.4.0-beta.1)" 26 + exit 1 27 + fi 28 + 29 + echo "๐Ÿ“ฆ Publishing all packages at version $VERSION" 30 + echo "" 31 + 32 + # Get the root directory 33 + ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" 34 + cd "$ROOT_DIR" 35 + 36 + # Find all package.json files in packages/* 37 + ALL_PACKAGES=($(find packages -maxdepth 2 -name "package.json" -not -path "*/node_modules/*" | sort)) 38 + 39 + # Filter out private packages and topologically sort by dependencies 40 + PACKAGES=($(node -e " 41 + const fs = require('fs'); 42 + const allPackages = process.argv.slice(1); 43 + 44 + // Filter out private packages 45 + const packages = allPackages.filter(path => { 46 + const pkg = JSON.parse(fs.readFileSync(path, 'utf-8')); 47 + return !pkg.private; 48 + }); 49 + 50 + // Build dependency graph 51 + const graph = new Map(); 52 + const pkgNames = new Map(); 53 + 54 + packages.forEach(path => { 55 + const pkg = JSON.parse(fs.readFileSync(path, 'utf-8')); 56 + pkgNames.set(pkg.name, path); 57 + 58 + const deps = new Set(); 59 + [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies].forEach(depObj => { 60 + if (depObj) { 61 + Object.keys(depObj).forEach(dep => { 62 + if (dep.startsWith('@typelex/')) { 63 + deps.add(dep); 64 + } 65 + }); 66 + } 67 + }); 68 + 69 + graph.set(pkg.name, deps); 70 + }); 71 + 72 + // Topological sort - packages with more dependents first 73 + const sorted = []; 74 + const processed = new Set(); 75 + 76 + function visit(pkgName) { 77 + if (processed.has(pkgName)) return; 78 + processed.add(pkgName); 79 + 80 + // Visit all dependencies first 81 + const deps = graph.get(pkgName) || new Set(); 82 + deps.forEach(dep => { 83 + if (graph.has(dep)) { 84 + visit(dep); 85 + } 86 + }); 87 + 88 + sorted.push(pkgName); 89 + } 90 + 91 + // Visit all packages 92 + graph.forEach((_, pkgName) => visit(pkgName)); 93 + 94 + // Output sorted package paths 95 + sorted.forEach(name => { 96 + if (pkgNames.has(name)) { 97 + console.log(pkgNames.get(name)); 98 + } 99 + }); 100 + " "${ALL_PACKAGES[@]}")) 101 + 102 + if [ ${#PACKAGES[@]} -eq 0 ]; then 103 + echo "Error: No publishable packages found in packages/" 104 + exit 1 105 + fi 106 + 107 + echo "Found ${#PACKAGES[@]} publishable packages (topologically sorted):" 108 + for pkg in "${PACKAGES[@]}"; do 109 + PKG_NAME=$(node -p "require('./$pkg').name") 110 + echo " - $PKG_NAME" 111 + done 112 + echo "" 113 + 114 + # Update all package.json files with the new version 115 + echo "๐Ÿ”„ Updating versions in all packages..." 116 + for pkg in "${PACKAGES[@]}"; do 117 + PKG_DIR=$(dirname "$pkg") 118 + PKG_NAME=$(node -p "require('./$pkg').name") 119 + 120 + echo " Updating $PKG_NAME..." 121 + 122 + # Update version 123 + node -e " 124 + const fs = require('fs'); 125 + const path = '$pkg'; 126 + const pkg = require('./' + path); 127 + pkg.version = '$VERSION'; 128 + 129 + // Helper to preserve semver prefix (^, ~, etc.) and workspace: protocol 130 + function updateVersion(currentVersion, newVersion) { 131 + // Preserve workspace: protocol for monorepo 132 + if (currentVersion.startsWith('workspace:')) { 133 + return currentVersion; 134 + } 135 + // Preserve semver prefix 136 + const match = currentVersion.match(/^([~^>=<]*)(.*)$/); 137 + if (match) { 138 + return match[1] + newVersion; 139 + } 140 + return newVersion; 141 + } 142 + 143 + // Helper to update dependencies 144 + function updateDeps(deps) { 145 + if (!deps) return; 146 + for (const dep in deps) { 147 + if (dep.startsWith('@typelex/')) { 148 + deps[dep] = updateVersion(deps[dep], '$VERSION'); 149 + } 150 + } 151 + } 152 + 153 + updateDeps(pkg.dependencies); 154 + updateDeps(pkg.devDependencies); 155 + updateDeps(pkg.peerDependencies); 156 + 157 + fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n'); 158 + " 159 + done 160 + 161 + echo "" 162 + echo "โœ… All versions updated to $VERSION" 163 + echo "" 164 + 165 + if [ "$DRY_RUN" = true ]; then 166 + echo "โœ… Dry run complete! Version updates have been applied." 167 + echo "" 168 + echo "๐Ÿ“‹ Updated packages:" 169 + for pkg in "${PACKAGES[@]}"; do 170 + PKG_NAME=$(node -p "require('./$pkg').name") 171 + echo " - $PKG_NAME@$VERSION" 172 + done 173 + echo "" 174 + echo "๐Ÿ’ก Review the changes, then run without --dry to publish." 175 + exit 0 176 + fi 177 + 178 + # Ask for confirmation 179 + read -p "๐Ÿš€ Ready to publish all packages to npm. Continue? (y/N) " -n 1 -r 180 + echo 181 + if [[ ! $REPLY =~ ^[Yy]$ ]]; then 182 + echo "โŒ Publish cancelled" 183 + exit 1 184 + fi 185 + 186 + echo "" 187 + echo "๐Ÿ“ค Publishing packages..." 188 + echo "" 189 + 190 + # Publish each package 191 + PUBLISHED=() 192 + FAILED=() 193 + 194 + for pkg in "${PACKAGES[@]}"; do 195 + PKG_DIR=$(dirname "$pkg") 196 + PKG_NAME=$(node -p "require('./$pkg').name") 197 + 198 + echo "Publishing $PKG_NAME..." 199 + 200 + if (cd "$PKG_DIR" && npm publish --access public); then 201 + echo " โœ… $PKG_NAME published successfully" 202 + PUBLISHED+=("$PKG_NAME") 203 + else 204 + echo " โŒ $PKG_NAME failed to publish" 205 + FAILED+=("$PKG_NAME") 206 + fi 207 + 208 + echo "" 209 + done 210 + 211 + # Summary 212 + echo "๐Ÿ“Š Summary:" 213 + echo "" 214 + echo "Published (${#PUBLISHED[@]}):" 215 + for pkg in "${PUBLISHED[@]}"; do 216 + echo " โœ… $pkg" 217 + done 218 + 219 + if [ ${#FAILED[@]} -gt 0 ]; then 220 + echo "" 221 + echo "Failed (${#FAILED[@]}):" 222 + for pkg in "${FAILED[@]}"; do 223 + echo " โŒ $pkg" 224 + done 225 + exit 1 226 + fi 227 + 228 + echo "" 229 + echo "๐ŸŽ‰ All packages published successfully!"