-1
.gitignore
-1
.gitignore
+13
CHANGELOG.md
+13
CHANGELOG.md
+169
-14
DOCS.md
+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
+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
+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",
+66
packages/cli/README.md
+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
+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
+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
+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
+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
packages/cli/src/index.ts
···
1
+
export { compileCommand } from "./commands/compile.js";
+38
packages/cli/src/utils/ensure-imports.ts
+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
+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
+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
+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
+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
+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
+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
+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
+8
packages/cli/test/scenarios/basic/expected/package.json
+10
packages/cli/test/scenarios/basic/expected/typelex/externals.tsp
+10
packages/cli/test/scenarios/basic/expected/typelex/externals.tsp
+12
packages/cli/test/scenarios/basic/expected/typelex/main.tsp
+12
packages/cli/test/scenarios/basic/expected/typelex/main.tsp
+27
packages/cli/test/scenarios/basic/project/lexicons/com/atproto/label/defs.json
+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
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
+10
packages/cli/test/scenarios/basic/test.ts
+25
packages/cli/test/scenarios/init-preserves-main/expected/lexicons/com/example/custom.json
+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
+8
packages/cli/test/scenarios/init-preserves-main/expected/package.json
+4
packages/cli/test/scenarios/init-preserves-main/expected/typelex/externals.tsp
+4
packages/cli/test/scenarios/init-preserves-main/expected/typelex/externals.tsp
+10
packages/cli/test/scenarios/init-preserves-main/expected/typelex/main.tsp
+10
packages/cli/test/scenarios/init-preserves-main/expected/typelex/main.tsp
+1
packages/cli/test/scenarios/init-preserves-main/project/package.json
+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
+10
packages/cli/test/scenarios/init-preserves-main/project/typelex/main.tsp
+9
packages/cli/test/scenarios/init-preserves-main/test.ts
+9
packages/cli/test/scenarios/init-preserves-main/test.ts
+21
packages/cli/test/scenarios/missing-dependency/expected/lexicons/com/external/media/defs.json
+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
+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
+8
packages/cli/test/scenarios/missing-dependency/expected/package.json
+9
packages/cli/test/scenarios/missing-dependency/expected/typelex/externals.tsp
+9
packages/cli/test/scenarios/missing-dependency/expected/typelex/externals.tsp
+10
packages/cli/test/scenarios/missing-dependency/expected/typelex/main.tsp
+10
packages/cli/test/scenarios/missing-dependency/expected/typelex/main.tsp
+1
packages/cli/test/scenarios/missing-dependency/project/package.json
+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
+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
+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
+8
packages/cli/test/scenarios/nested-init/expected/package.json
+4
packages/cli/test/scenarios/nested-init/expected/typelex/externals.tsp
+4
packages/cli/test/scenarios/nested-init/expected/typelex/externals.tsp
+12
packages/cli/test/scenarios/nested-init/expected/typelex/main.tsp
+12
packages/cli/test/scenarios/nested-init/expected/typelex/main.tsp
+1
packages/cli/test/scenarios/nested-init/project/package.json
+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
+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
+8
packages/cli/test/scenarios/parent-lexicons/expected1/app/package.json
+10
packages/cli/test/scenarios/parent-lexicons/expected1/app/typelex/externals.tsp
+10
packages/cli/test/scenarios/parent-lexicons/expected1/app/typelex/externals.tsp
+12
packages/cli/test/scenarios/parent-lexicons/expected1/app/typelex/main.tsp
+12
packages/cli/test/scenarios/parent-lexicons/expected1/app/typelex/main.tsp
+27
packages/cli/test/scenarios/parent-lexicons/expected1/lexicons/com/atproto/label/defs.json
+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
+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
+8
packages/cli/test/scenarios/parent-lexicons/expected2/app/package.json
+10
packages/cli/test/scenarios/parent-lexicons/expected2/app/typelex/externals.tsp
+10
packages/cli/test/scenarios/parent-lexicons/expected2/app/typelex/externals.tsp
+13
packages/cli/test/scenarios/parent-lexicons/expected2/app/typelex/main.tsp
+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
+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
+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
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
+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
+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
+14
packages/cli/test/scenarios/reserved-keywords/expected/lexicons/app/bsky/feed/post/record.json
+14
packages/cli/test/scenarios/reserved-keywords/expected/lexicons/com/atproto/server/defs.json
+14
packages/cli/test/scenarios/reserved-keywords/expected/lexicons/com/atproto/server/defs.json
+21
packages/cli/test/scenarios/reserved-keywords/expected/lexicons/pub/leaflet/example/profile.json
+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
+7
packages/cli/test/scenarios/reserved-keywords/expected/package.json
+14
packages/cli/test/scenarios/reserved-keywords/expected/typelex/externals.tsp
+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
+12
packages/cli/test/scenarios/reserved-keywords/expected/typelex/main.tsp
+14
packages/cli/test/scenarios/reserved-keywords/project/lexicons/app/bsky/feed/post/record.json
+14
packages/cli/test/scenarios/reserved-keywords/project/lexicons/app/bsky/feed/post/record.json
+14
packages/cli/test/scenarios/reserved-keywords/project/lexicons/com/atproto/server/defs.json
+14
packages/cli/test/scenarios/reserved-keywords/project/lexicons/com/atproto/server/defs.json
+4
packages/cli/test/scenarios/reserved-keywords/project/package.json
+4
packages/cli/test/scenarios/reserved-keywords/project/package.json
+10
packages/cli/test/scenarios/reserved-keywords/test.ts
+10
packages/cli/test/scenarios/reserved-keywords/test.ts
+1
packages/cli/test/scenarios/validation-errors/project/package.json
+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
+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
+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
+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
+8
packages/cli/test/scenarios/with-external-lexicons/expected1/package.json
+10
packages/cli/test/scenarios/with-external-lexicons/expected1/typelex/externals.tsp
+10
packages/cli/test/scenarios/with-external-lexicons/expected1/typelex/externals.tsp
+12
packages/cli/test/scenarios/with-external-lexicons/expected1/typelex/main.tsp
+12
packages/cli/test/scenarios/with-external-lexicons/expected1/typelex/main.tsp
+27
packages/cli/test/scenarios/with-external-lexicons/expected2/lexicons/com/atproto/label/defs.json
+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
+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
+8
packages/cli/test/scenarios/with-external-lexicons/expected2/package.json
+10
packages/cli/test/scenarios/with-external-lexicons/expected2/typelex/externals.tsp
+10
packages/cli/test/scenarios/with-external-lexicons/expected2/typelex/externals.tsp
+13
packages/cli/test/scenarios/with-external-lexicons/expected2/typelex/main.tsp
+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
+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
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
+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
+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
+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
+4
packages/cli/typelex/externals.tsp
+10
packages/cli/vitest.config.ts
+10
packages/cli/vitest.config.ts
+65
packages/emitter/lib/decorators.tsp
+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
packages/emitter/package.json
+18
packages/emitter/src/decorators.ts
+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
+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
+2
packages/emitter/src/tsp-index.ts
+48
packages/emitter/test/integration/atproto/input/app/bsky/actor/defs.tsp
+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
packages/emitter/test/integration/atproto/input/app/bsky/actor/getPreferences.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/getProfile.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/getProfile.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/getProfiles.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/getProfiles.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/getSuggestions.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/getSuggestions.tsp
+12
packages/emitter/test/integration/atproto/input/app/bsky/actor/profile.tsp
+12
packages/emitter/test/integration/atproto/input/app/bsky/actor/profile.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/putPreferences.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/putPreferences.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/searchActors.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/searchActors.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/searchActorsTypeahead.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/searchActorsTypeahead.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/status.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/actor/status.tsp
+14
packages/emitter/test/integration/atproto/input/app/bsky/bookmark/defs.tsp
+14
packages/emitter/test/integration/atproto/input/app/bsky/bookmark/defs.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/bookmark/getBookmarks.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/bookmark/getBookmarks.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/embed/images.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/embed/images.tsp
+54
packages/emitter/test/integration/atproto/input/app/bsky/embed/record.tsp
+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
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
+7
packages/emitter/test/integration/atproto/input/app/bsky/embed/video.tsp
+49
packages/emitter/test/integration/atproto/input/app/bsky/feed/defs.tsp
+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
+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
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getActorFeeds.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getActorLikes.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getActorLikes.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getAuthorFeed.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getAuthorFeed.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeed.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeed.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeedGenerator.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeedGenerator.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeedGenerators.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeedGenerators.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeedSkeleton.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getFeedSkeleton.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getLikes.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getLikes.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getListFeed.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getListFeed.tsp
+10
packages/emitter/test/integration/atproto/input/app/bsky/feed/getPostThread.tsp
+10
packages/emitter/test/integration/atproto/input/app/bsky/feed/getPostThread.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getPosts.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getPosts.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getQuotes.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getQuotes.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getRepostedBy.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getRepostedBy.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getSuggestedFeeds.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getSuggestedFeeds.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getTimeline.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/getTimeline.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/like.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/like.tsp
+42
packages/emitter/test/integration/atproto/input/app/bsky/feed/post.tsp
+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
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/repost.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/searchPosts.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/searchPosts.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/sendInteractions.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/feed/sendInteractions.tsp
+23
packages/emitter/test/integration/atproto/input/app/bsky/graph/defs.tsp
+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
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getActorStarterPacks.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getBlocks.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getBlocks.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getFollowers.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getFollowers.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getFollows.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getFollows.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getKnownFollowers.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getKnownFollowers.tsp
+8
packages/emitter/test/integration/atproto/input/app/bsky/graph/getList.tsp
+8
packages/emitter/test/integration/atproto/input/app/bsky/graph/getList.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getListBlocks.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getListBlocks.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getListMutes.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getListMutes.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getLists.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getLists.tsp
+8
packages/emitter/test/integration/atproto/input/app/bsky/graph/getListsWithMembership.tsp
+8
packages/emitter/test/integration/atproto/input/app/bsky/graph/getListsWithMembership.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getMutes.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getMutes.tsp
+8
packages/emitter/test/integration/atproto/input/app/bsky/graph/getRelationships.tsp
+8
packages/emitter/test/integration/atproto/input/app/bsky/graph/getRelationships.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getStarterPack.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getStarterPack.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getStarterPacks.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getStarterPacks.tsp
+8
packages/emitter/test/integration/atproto/input/app/bsky/graph/getStarterPacksWithMembership.tsp
+8
packages/emitter/test/integration/atproto/input/app/bsky/graph/getStarterPacksWithMembership.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getSuggestedFollowsByActor.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/getSuggestedFollowsByActor.tsp
+17
packages/emitter/test/integration/atproto/input/app/bsky/graph/list.tsp
+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
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/searchStarterPacks.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/starterpack.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/graph/starterpack.tsp
+20
packages/emitter/test/integration/atproto/input/app/bsky/labeler/defs.tsp
+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
+8
packages/emitter/test/integration/atproto/input/app/bsky/labeler/getServices.tsp
+18
packages/emitter/test/integration/atproto/input/app/bsky/labeler/service.tsp
+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
packages/emitter/test/integration/atproto/input/app/bsky/notification/getPreferences.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/notification/listActivitySubscriptions.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/notification/listActivitySubscriptions.tsp
+12
packages/emitter/test/integration/atproto/input/app/bsky/notification/listNotifications.tsp
+12
packages/emitter/test/integration/atproto/input/app/bsky/notification/listNotifications.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/notification/putActivitySubscription.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/notification/putActivitySubscription.tsp
+10
packages/emitter/test/integration/atproto/input/app/bsky/notification/putPreferencesV2.tsp
+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
+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
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getAgeAssuranceState.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getPopularFeedGenerators.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getPopularFeedGenerators.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getPostThreadOtherV2.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getPostThreadOtherV2.tsp
+15
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getPostThreadV2.tsp
+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
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getSuggestedFeeds.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getSuggestedStarterPacks.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getSuggestedStarterPacks.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getSuggestedUsers.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getSuggestedUsers.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getSuggestionsSkeleton.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getSuggestionsSkeleton.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getTrendingTopics.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getTrendingTopics.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getTrends.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getTrends.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getTrendsSkeleton.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/getTrendsSkeleton.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/initAgeAssurance.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/initAgeAssurance.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/searchActorsSkeleton.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/searchActorsSkeleton.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/searchPostsSkeleton.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/searchPostsSkeleton.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/searchStarterPacksSkeleton.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/unspecced/searchStarterPacksSkeleton.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/video/getJobStatus.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/video/getJobStatus.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/video/uploadVideo.tsp
+7
packages/emitter/test/integration/atproto/input/app/bsky/video/uploadVideo.tsp
+14
packages/emitter/test/integration/atproto/input/chat/bsky/actor/defs.tsp
+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
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/addReaction.tsp
+18
packages/emitter/test/integration/atproto/input/chat/bsky/convo/defs.tsp
+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
packages/emitter/test/integration/atproto/input/chat/bsky/convo/deleteMessageForSelf.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/getConvo.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/getConvo.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/getConvoAvailability.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/getConvoAvailability.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/getConvoForMembers.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/getConvoForMembers.tsp
+16
packages/emitter/test/integration/atproto/input/chat/bsky/convo/getLog.tsp
+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
+8
packages/emitter/test/integration/atproto/input/chat/bsky/convo/getMessages.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/listConvos.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/listConvos.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/muteConvo.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/muteConvo.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/removeReaction.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/removeReaction.tsp
+8
packages/emitter/test/integration/atproto/input/chat/bsky/convo/sendMessage.tsp
+8
packages/emitter/test/integration/atproto/input/chat/bsky/convo/sendMessage.tsp
+8
packages/emitter/test/integration/atproto/input/chat/bsky/convo/sendMessageBatch.tsp
+8
packages/emitter/test/integration/atproto/input/chat/bsky/convo/sendMessageBatch.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/unmuteConvo.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/unmuteConvo.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/updateRead.tsp
+7
packages/emitter/test/integration/atproto/input/chat/bsky/convo/updateRead.tsp
+8
packages/emitter/test/integration/atproto/input/chat/bsky/moderation/getMessageContext.tsp
+8
packages/emitter/test/integration/atproto/input/chat/bsky/moderation/getMessageContext.tsp
+8
packages/emitter/test/integration/atproto/input/com/atproto/admin/defs.tsp
+8
packages/emitter/test/integration/atproto/input/com/atproto/admin/defs.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/admin/getAccountInfo.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/admin/getAccountInfo.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/admin/getAccountInfos.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/admin/getAccountInfos.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/admin/getInviteCodes.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/admin/getInviteCodes.tsp
+14
packages/emitter/test/integration/atproto/input/com/atproto/admin/getSubjectStatus.tsp
+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
+7
packages/emitter/test/integration/atproto/input/com/atproto/admin/searchAccounts.tsp
+14
packages/emitter/test/integration/atproto/input/com/atproto/admin/updateSubjectStatus.tsp
+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
+7
packages/emitter/test/integration/atproto/input/com/atproto/identity/refreshIdentity.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/identity/resolveIdentity.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/identity/resolveIdentity.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/label/queryLabels.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/label/queryLabels.tsp
+8
packages/emitter/test/integration/atproto/input/com/atproto/label/subscribeLabels.tsp
+8
packages/emitter/test/integration/atproto/input/com/atproto/label/subscribeLabels.tsp
+17
packages/emitter/test/integration/atproto/input/com/atproto/moderation/createReport.tsp
+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
+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
+7
packages/emitter/test/integration/atproto/input/com/atproto/repo/applyWrites.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/repo/createRecord.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/repo/createRecord.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/repo/deleteRecord.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/repo/deleteRecord.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/repo/putRecord.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/repo/putRecord.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/server/getAccountInviteCodes.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/server/getAccountInviteCodes.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/sync/getHostStatus.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/sync/getHostStatus.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/sync/listHosts.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/sync/listHosts.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/temp/fetchLabels.tsp
+7
packages/emitter/test/integration/atproto/input/com/atproto/temp/fetchLabels.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/communication/createTemplate.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/communication/createTemplate.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/communication/listTemplates.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/communication/listTemplates.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/communication/updateTemplate.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/communication/updateTemplate.tsp
+34
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/defs.tsp
+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
+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
+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
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getEvent.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getRecord.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getRecord.tsp
+8
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getRecords.tsp
+8
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getRecords.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getRepo.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getRepo.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getReporterStats.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getReporterStats.tsp
+8
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getRepos.tsp
+8
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getRepos.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getSubjects.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/getSubjects.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/queryEvents.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/queryEvents.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/queryStatuses.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/queryStatuses.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/searchRepos.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/moderation/searchRepos.tsp
+10
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/addRule.tsp
+10
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/addRule.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/queryEvents.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/queryEvents.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/queryRules.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/queryRules.tsp
+8
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/removeRule.tsp
+8
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/removeRule.tsp
+10
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/updateRule.tsp
+10
packages/emitter/test/integration/atproto/input/tools/ozone/safelink/updateRule.tsp
+10
packages/emitter/test/integration/atproto/input/tools/ozone/server/getConfig.tsp
+10
packages/emitter/test/integration/atproto/input/tools/ozone/server/getConfig.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/set/getValues.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/set/getValues.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/set/querySets.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/set/querySets.tsp
+8
packages/emitter/test/integration/atproto/input/tools/ozone/set/upsertSet.tsp
+8
packages/emitter/test/integration/atproto/input/tools/ozone/set/upsertSet.tsp
+10
packages/emitter/test/integration/atproto/input/tools/ozone/setting/defs.tsp
+10
packages/emitter/test/integration/atproto/input/tools/ozone/setting/defs.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/setting/listOptions.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/setting/listOptions.tsp
+15
packages/emitter/test/integration/atproto/input/tools/ozone/setting/upsertOption.tsp
+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
packages/emitter/test/integration/atproto/input/tools/ozone/signature/findCorrelation.tsp
+12
packages/emitter/test/integration/atproto/input/tools/ozone/signature/findRelatedAccounts.tsp
+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
+7
packages/emitter/test/integration/atproto/input/tools/ozone/signature/searchAccounts.tsp
+11
packages/emitter/test/integration/atproto/input/tools/ozone/team/addMember.tsp
+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
+7
packages/emitter/test/integration/atproto/input/tools/ozone/team/defs.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/team/listMembers.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/team/listMembers.tsp
+11
packages/emitter/test/integration/atproto/input/tools/ozone/team/updateMember.tsp
+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
+8
packages/emitter/test/integration/atproto/input/tools/ozone/verification/defs.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/verification/grantVerifications.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/verification/grantVerifications.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/verification/listVerifications.tsp
+7
packages/emitter/test/integration/atproto/input/tools/ozone/verification/listVerifications.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/defs.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/defs.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/getAccountInfo.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/getAccountInfo.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/getAccountInfos.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/getAccountInfos.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/getInviteCodes.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/getInviteCodes.tsp
+14
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/getSubjectStatus.tsp
+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
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/searchAccounts.tsp
+14
packages/emitter/test/integration/lexicon-examples/input/com/atproto/admin/updateSubjectStatus.tsp
+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
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/identity/refreshIdentity.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/identity/resolveIdentity.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/identity/resolveIdentity.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/label/queryLabels.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/label/queryLabels.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/label/subscribeLabels.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/label/subscribeLabels.tsp
+17
packages/emitter/test/integration/lexicon-examples/input/com/atproto/moderation/createReport.tsp
+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
+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
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/repo/applyWrites.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/repo/createRecord.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/repo/createRecord.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/repo/deleteRecord.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/repo/deleteRecord.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/repo/putRecord.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/repo/putRecord.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/server/getAccountInviteCodes.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/server/getAccountInviteCodes.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/sync/getHostStatus.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/sync/getHostStatus.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/sync/listHosts.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/sync/listHosts.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/temp/fetchLabels.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/com/atproto/temp/fetchLabels.tsp
+14
packages/emitter/test/integration/lexicon-examples/input/community/lexicon/bookmarks/bookmark.tsp
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+7
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/blocks/blockquote.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/blocks/bskyPost.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/blocks/bskyPost.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/blocks/header.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/blocks/header.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/blocks/text.tsp
+7
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/blocks/text.tsp
+17
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/blocks/unorderedList.tsp
+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
+12
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/comment.tsp
+12
packages/emitter/test/integration/lexicon-examples/input/pub/leaflet/document.tsp
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+8
packages/emitter/test/spec/external/input/test/external.tsp
+7
packages/emitter/test/spec/external/input/test/normal.tsp
+7
packages/emitter/test/spec/external/input/test/normal.tsp
+14
packages/emitter/test/spec/external/output/test/normal.json
+14
packages/emitter/test/spec/external/output/test/normal.json
+23
-1
packages/emitter/test/spec.test.ts
+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
-7
packages/example/.gitignore
+31
packages/example/lexicons/app/bsky/actor/defs.json
+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
+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
+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
+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
+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
+14
packages/example/lexicons/com/atproto/repo/defs.json
+57
packages/example/lexicons/com/atproto/repo/deleteRecord.json
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
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
+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
+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
-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
-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
-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
-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
-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
-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
+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
+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
+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
+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
+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
-5
packages/example/tspconfig.yaml
+82
packages/example/typelex/externals.tsp
+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
+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
+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
-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
+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
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
packages/website/package.json
+14
packages/website/src/components/CodeBlock.astro
+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
+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
+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
+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
+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
+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
+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!"