tangled
alpha
login
or
join now
tylur.dev
/
prototypey
prototypey.org - atproto lexicon typescript toolkit - mirror https://github.com/tylersayshi/prototypey
1
fork
atom
overview
issues
pulls
pipelines
rename namespace => lexicon
Tyler
3 months ago
55d5e4b1
b8ada449
+125
-125
13 changed files
expand all
collapse all
unified
split
README.md
packages
cli
README.md
src
commands
gen-emit.ts
tests
commands
gen-emit.test.ts
fixtures
simple-lexicon.ts
prototypey
src
lib.ts
tests
base-case.test.ts
bsky-actor.test.ts
bsky-feed.test.ts
infer.bench.ts
infer.test.ts
site
src
components
Playground.tsx
tests
components
Playground.test.tsx
+1
-1
README.md
···
29
**what you'd write:**
30
31
```typescript
32
-
const profileNamespace = lx.namespace("app.bsky.actor.profile", {
33
main: lx.record({
34
key: "self",
35
record: lx.object({
···
29
**what you'd write:**
30
31
```typescript
32
+
const profileNamespace = lx.lexicon("app.bsky.actor.profile", {
33
main: lx.record({
34
key: "self",
35
record: lx.object({
+2
-2
packages/cli/README.md
···
67
68
**What it does:**
69
70
-
- Scans TypeScript files for exported lexicon namespace definitions
71
-
- Extracts the `.json` property from each namespace
72
- Emits properly formatted JSON lexicon schema files
73
- Names output files by lexicon ID (e.g., `app.bsky.feed.post.json`)
74
···
67
68
**What it does:**
69
70
+
- Scans TypeScript files for exported lexicon definitions
71
+
- Extracts the `.json` property from each lexicon
72
- Emits properly formatted JSON lexicon schema files
73
- Names output files by lexicon ID (e.g., `app.bsky.feed.post.json`)
74
+10
-10
packages/cli/src/commands/gen-emit.ts
···
57
// Dynamically import the module
58
const module = await import(fileUrl);
59
60
-
// Find all exported namespaces
61
-
const namespaces: LexiconNamespace[] = [];
62
for (const key of Object.keys(module)) {
63
const exported = module[key];
64
-
// Check if it's a namespace with a json property
65
if (
66
exported &&
67
typeof exported === "object" &&
···
72
"id" in exported.json &&
73
"defs" in exported.json
74
) {
75
-
namespaces.push(exported as LexiconNamespace);
76
}
77
}
78
79
-
if (namespaces.length === 0) {
80
-
console.warn(` ⚠ ${sourcePath}: No lexicon namespaces found`);
81
return;
82
}
83
84
-
// Emit JSON for each namespace
85
-
for (const namespace of namespaces) {
86
-
const { id } = namespace.json;
87
const outputPath = join(outdir, `${id}.json`);
88
89
// Write the JSON file
90
await writeFile(
91
outputPath,
92
-
JSON.stringify(namespace.json, null, "\t"),
93
"utf-8",
94
);
95
···
57
// Dynamically import the module
58
const module = await import(fileUrl);
59
60
+
// Find all exported lexicons
61
+
const lexicons: LexiconNamespace[] = [];
62
for (const key of Object.keys(module)) {
63
const exported = module[key];
64
+
// Check if it's a lexicon with a json property
65
if (
66
exported &&
67
typeof exported === "object" &&
···
72
"id" in exported.json &&
73
"defs" in exported.json
74
) {
75
+
lexicons.push(exported as LexiconNamespace);
76
}
77
}
78
79
+
if (lexicons.length === 0) {
80
+
console.warn(` ⚠ ${sourcePath}: No lexicon lexicons found`);
81
return;
82
}
83
84
+
// Emit JSON for each lexicon
85
+
for (const lexicon of lexicons) {
86
+
const { id } = lexicon.json;
87
const outputPath = join(outdir, `${id}.json`);
88
89
// Write the JSON file
90
await writeFile(
91
outputPath,
92
+
JSON.stringify(lexicon.json, null, "\t"),
93
"utf-8",
94
);
95
+10
-10
packages/cli/tests/commands/gen-emit.test.ts
···
29
`
30
import { lx } from "prototypey";
31
32
-
export const profileNamespace = lx.namespace("app.bsky.actor.profile", {
33
main: lx.record({
34
key: "self",
35
record: lx.object({
···
85
`
86
import { lx } from "prototypey";
87
88
-
export const profile = lx.namespace("app.bsky.actor.profile", {
89
main: lx.record({
90
key: "self",
91
record: lx.object({
···
94
}),
95
});
96
97
-
export const post = lx.namespace("app.bsky.feed.post", {
98
main: lx.record({
99
key: "tid",
100
record: lx.object({
···
129
join(lexicons, "profile.ts"),
130
`
131
import { lx } from "prototypey";
132
-
export const schema = lx.namespace("app.bsky.actor.profile", {
133
main: lx.record({ key: "self", record: lx.object({}) }),
134
});
135
`,
···
139
join(lexicons, "post.ts"),
140
`
141
import { lx } from "prototypey";
142
-
export const schema = lx.namespace("app.bsky.feed.post", {
143
main: lx.record({ key: "tid", record: lx.object({}) }),
144
});
145
`,
···
169
`
170
import { lx } from "prototypey";
171
172
-
export const searchPosts = lx.namespace("app.bsky.feed.searchPosts", {
173
main: lx.query({
174
description: "Find posts matching search criteria",
175
parameters: lx.params({
···
238
`
239
import { lx } from "prototypey";
240
241
-
export const createPost = lx.namespace("com.atproto.repo.createRecord", {
242
main: lx.procedure({
243
description: "Create a record",
244
input: {
···
309
`
310
import { lx } from "prototypey";
311
312
-
export const subscribeRepos = lx.namespace("com.atproto.sync.subscribeRepos", {
313
main: lx.subscription({
314
description: "Repository event stream",
315
parameters: lx.params({
···
396
`
397
import { lx } from "prototypey";
398
399
-
export const feedDefs = lx.namespace("app.bsky.feed.defs", {
400
postView: lx.object({
401
uri: lx.string({ required: true, format: "at-uri" }),
402
cid: lx.string({ required: true, format: "cid" }),
···
460
`
461
import { lx } from "prototypey";
462
463
-
export const imagePost = lx.namespace("app.example.imagePost", {
464
main: lx.record({
465
key: "tid",
466
record: lx.object({
···
29
`
30
import { lx } from "prototypey";
31
32
+
export const profileNamespace = lx.lexicon("app.bsky.actor.profile", {
33
main: lx.record({
34
key: "self",
35
record: lx.object({
···
85
`
86
import { lx } from "prototypey";
87
88
+
export const profile = lx.lexicon("app.bsky.actor.profile", {
89
main: lx.record({
90
key: "self",
91
record: lx.object({
···
94
}),
95
});
96
97
+
export const post = lx.lexicon("app.bsky.feed.post", {
98
main: lx.record({
99
key: "tid",
100
record: lx.object({
···
129
join(lexicons, "profile.ts"),
130
`
131
import { lx } from "prototypey";
132
+
export const schema = lx.lexicon("app.bsky.actor.profile", {
133
main: lx.record({ key: "self", record: lx.object({}) }),
134
});
135
`,
···
139
join(lexicons, "post.ts"),
140
`
141
import { lx } from "prototypey";
142
+
export const schema = lx.lexicon("app.bsky.feed.post", {
143
main: lx.record({ key: "tid", record: lx.object({}) }),
144
});
145
`,
···
169
`
170
import { lx } from "prototypey";
171
172
+
export const searchPosts = lx.lexicon("app.bsky.feed.searchPosts", {
173
main: lx.query({
174
description: "Find posts matching search criteria",
175
parameters: lx.params({
···
238
`
239
import { lx } from "prototypey";
240
241
+
export const createPost = lx.lexicon("com.atproto.repo.createRecord", {
242
main: lx.procedure({
243
description: "Create a record",
244
input: {
···
309
`
310
import { lx } from "prototypey";
311
312
+
export const subscribeRepos = lx.lexicon("com.atproto.sync.subscribeRepos", {
313
main: lx.subscription({
314
description: "Repository event stream",
315
parameters: lx.params({
···
396
`
397
import { lx } from "prototypey";
398
399
+
export const feedDefs = lx.lexicon("app.bsky.feed.defs", {
400
postView: lx.object({
401
uri: lx.string({ required: true, format: "at-uri" }),
402
cid: lx.string({ required: true, format: "cid" }),
···
460
`
461
import { lx } from "prototypey";
462
463
+
export const imagePost = lx.lexicon("app.example.imagePost", {
464
main: lx.record({
465
key: "tid",
466
record: lx.object({
+1
-1
packages/cli/tests/fixtures/simple-lexicon.ts
···
1
import { lx } from "prototypey";
2
3
-
export const profileNamespace = lx.namespace("app.bsky.actor.profile", {
4
main: lx.record({
5
key: "self",
6
record: lx.object({
···
1
import { lx } from "prototypey";
2
3
+
export const profileNamespace = lx.lexicon("app.bsky.actor.profile", {
4
main: lx.record({
5
key: "self",
6
record: lx.object({
+2
-2
packages/prototypey/src/lib.ts
···
563
} as T & { type: "subscription" };
564
},
565
/**
566
-
* Creates a lexicon namespace document.
567
* @see https://atproto.com/specs/lexicon#lexicon-document
568
*/
569
-
namespace<ID extends string, D extends LexiconNamespace["defs"]>(
570
id: ID,
571
defs: D,
572
): Namespace<{ lexicon: 1; id: ID; defs: D }> {
···
563
} as T & { type: "subscription" };
564
},
565
/**
566
+
* Creates a lexicon schema document.
567
* @see https://atproto.com/specs/lexicon#lexicon-document
568
*/
569
+
lexicon<ID extends string, D extends LexiconNamespace["defs"]>(
570
id: ID,
571
defs: D,
572
): Namespace<{ lexicon: 1; id: ID; defs: D }> {
+1
-1
packages/prototypey/tests/base-case.test.ts
···
2
import { lx } from "../src/lib.ts";
3
4
test("app.bsky.actor.profile", () => {
5
-
const profileNamespace = lx.namespace("app.bsky.actor.profile", {
6
main: lx.record({
7
key: "self",
8
record: lx.object({
···
2
import { lx } from "../src/lib.ts";
3
4
test("app.bsky.actor.profile", () => {
5
+
const profileNamespace = lx.lexicon("app.bsky.actor.profile", {
6
main: lx.record({
7
key: "self",
8
record: lx.object({
+2
-2
packages/prototypey/tests/bsky-actor.test.ts
···
830
});
831
});
832
833
-
test("app.bsky.actor.defs - full namespace", () => {
834
-
const actorDefs = lx.namespace("app.bsky.actor.defs", {
835
profileViewBasic: lx.object({
836
did: lx.string({ required: true, format: "did" }),
837
handle: lx.string({ required: true, format: "handle" }),
···
830
});
831
});
832
833
+
test("app.bsky.actor.defs - full lexicon", () => {
834
+
const actorDefs = lx.lexicon("app.bsky.actor.defs", {
835
profileViewBasic: lx.object({
836
did: lx.string({ required: true, format: "did" }),
837
handle: lx.string({ required: true, format: "handle" }),
+2
-2
packages/prototypey/tests/bsky-feed.test.ts
···
607
});
608
});
609
610
-
test("app.bsky.feed.defs - full namespace", () => {
611
-
const feedDefs = lx.namespace("app.bsky.feed.defs", {
612
postView: lx.object({
613
uri: lx.string({ required: true, format: "at-uri" }),
614
cid: lx.string({ required: true, format: "cid" }),
···
607
});
608
});
609
610
+
test("app.bsky.feed.defs - full lexicon", () => {
611
+
const feedDefs = lx.lexicon("app.bsky.feed.defs", {
612
postView: lx.object({
613
uri: lx.string({ required: true, format: "at-uri" }),
614
cid: lx.string({ required: true, format: "cid" }),
+5
-5
packages/prototypey/tests/infer.bench.ts
···
2
import { lx } from "../src/lib.ts";
3
4
bench("infer with simple object", () => {
5
-
const schema = lx.namespace("test.simple", {
6
main: lx.object({
7
id: lx.string({ required: true }),
8
name: lx.string({ required: true }),
···
12
}).types([741, "instantiations"]);
13
14
bench("infer with complex nested structure", () => {
15
-
const schema = lx.namespace("test.complex", {
16
user: lx.object({
17
handle: lx.string({ required: true }),
18
displayName: lx.string(),
···
35
}).types([1040, "instantiations"]);
36
37
bench("infer with circular reference", () => {
38
-
const ns = lx.namespace("test", {
39
user: lx.object({
40
name: lx.string({ required: true }),
41
posts: lx.array(lx.ref("#post")),
···
51
return ns.infer;
52
}).types([692, "instantiations"]);
53
54
-
bench("infer with app.bsky.feed.defs namespace", () => {
55
-
const schema = lx.namespace("app.bsky.feed.defs", {
56
viewerState: lx.object({
57
repost: lx.string({ format: "at-uri" }),
58
like: lx.string({ format: "at-uri" }),
···
2
import { lx } from "../src/lib.ts";
3
4
bench("infer with simple object", () => {
5
+
const schema = lx.lexicon("test.simple", {
6
main: lx.object({
7
id: lx.string({ required: true }),
8
name: lx.string({ required: true }),
···
12
}).types([741, "instantiations"]);
13
14
bench("infer with complex nested structure", () => {
15
+
const schema = lx.lexicon("test.complex", {
16
user: lx.object({
17
handle: lx.string({ required: true }),
18
displayName: lx.string(),
···
35
}).types([1040, "instantiations"]);
36
37
bench("infer with circular reference", () => {
38
+
const ns = lx.lexicon("test", {
39
user: lx.object({
40
name: lx.string({ required: true }),
41
posts: lx.array(lx.ref("#post")),
···
51
return ns.infer;
52
}).types([692, "instantiations"]);
53
54
+
bench("infer with app.bsky.feed.defs lexicon", () => {
55
+
const schema = lx.lexicon("app.bsky.feed.defs", {
56
viewerState: lx.object({
57
repost: lx.string({ format: "at-uri" }),
58
like: lx.string({ format: "at-uri" }),
+82
-82
packages/prototypey/tests/infer.test.ts
···
3
import { lx } from "../src/lib.ts";
4
5
test("InferNS produces expected type shape", () => {
6
-
const exampleLexicon = lx.namespace("com.example.post", {
7
main: lx.record({
8
key: "tid",
9
record: lx.object({
···
26
});
27
28
test("InferObject handles required fields", () => {
29
-
const schema = lx.namespace("test", {
30
main: lx.object({
31
required: lx.string({ required: true }),
32
optional: lx.string(),
···
41
});
42
43
test("InferObject handles nullable fields", () => {
44
-
const schema = lx.namespace("test", {
45
main: lx.object({
46
nullable: lx.string({ nullable: true, required: true }),
47
}),
···
57
// ============================================================================
58
59
test("InferType handles string primitive", () => {
60
-
const namespace = lx.namespace("test.string", {
61
main: lx.object({
62
simpleString: lx.string(),
63
}),
64
});
65
66
-
attest(namespace.infer).type.toString.snap(`{
67
$type: "test.string"
68
simpleString?: string | undefined
69
}`);
70
});
71
72
test("InferType handles integer primitive", () => {
73
-
const namespace = lx.namespace("test.integer", {
74
main: lx.object({
75
count: lx.integer(),
76
age: lx.integer({ minimum: 0, maximum: 120 }),
77
}),
78
});
79
80
-
attest(namespace.infer).type.toString.snap(`{
81
$type: "test.integer"
82
count?: number | undefined
83
age?: number | undefined
···
85
});
86
87
test("InferType handles boolean primitive", () => {
88
-
const namespace = lx.namespace("test.boolean", {
89
main: lx.object({
90
isActive: lx.boolean(),
91
hasAccess: lx.boolean({ required: true }),
92
}),
93
});
94
95
-
attest(namespace.infer).type.toString.snap(`{
96
$type: "test.boolean"
97
isActive?: boolean | undefined
98
hasAccess: boolean
···
100
});
101
102
test("InferType handles null primitive", () => {
103
-
const namespace = lx.namespace("test.null", {
104
main: lx.object({
105
nullValue: lx.null(),
106
}),
107
});
108
109
-
attest(namespace.infer).type.toString.snap(`{
110
$type: "test.null"
111
nullValue?: null | undefined
112
}`);
113
});
114
115
test("InferType handles unknown primitive", () => {
116
-
const namespace = lx.namespace("test.unknown", {
117
main: lx.object({
118
metadata: lx.unknown(),
119
}),
120
});
121
122
-
attest(namespace.infer).type.toString.snap(
123
'{ $type: "test.unknown"; metadata?: unknown }',
124
);
125
});
126
127
test("InferType handles bytes primitive", () => {
128
-
const namespace = lx.namespace("test.bytes", {
129
main: lx.object({
130
data: lx.bytes(),
131
}),
132
});
133
134
-
attest(namespace.infer).type.toString.snap(`{
135
$type: "test.bytes"
136
data?: Uint8Array<ArrayBufferLike> | undefined
137
}`);
138
});
139
140
test("InferType handles blob primitive", () => {
141
-
const namespace = lx.namespace("test.blob", {
142
main: lx.object({
143
image: lx.blob({ accept: ["image/png", "image/jpeg"] }),
144
}),
145
});
146
147
-
attest(namespace.infer).type.toString.snap(
148
'{ $type: "test.blob"; image?: Blob | undefined }',
149
);
150
});
···
154
// ============================================================================
155
156
test("InferToken handles basic token without enum", () => {
157
-
const namespace = lx.namespace("test.token", {
158
main: lx.object({
159
symbol: lx.token("A symbolic value"),
160
}),
161
});
162
163
-
attest(namespace.infer).type.toString.snap(`{
164
$type: "test.token"
165
symbol?: string | undefined
166
}`);
···
171
// ============================================================================
172
173
test("InferArray handles string arrays", () => {
174
-
const namespace = lx.namespace("test.array.string", {
175
main: lx.object({
176
tags: lx.array(lx.string()),
177
}),
178
});
179
180
-
attest(namespace.infer).type.toString.snap(`{
181
$type: "test.array.string"
182
tags?: string[] | undefined
183
}`);
184
});
185
186
test("InferArray handles integer arrays", () => {
187
-
const namespace = lx.namespace("test.array.integer", {
188
main: lx.object({
189
scores: lx.array(lx.integer(), { minLength: 1, maxLength: 10 }),
190
}),
191
});
192
193
-
attest(namespace.infer).type.toString.snap(`{
194
$type: "test.array.integer"
195
scores?: number[] | undefined
196
}`);
197
});
198
199
test("InferArray handles boolean arrays", () => {
200
-
const namespace = lx.namespace("test.array.boolean", {
201
main: lx.object({
202
flags: lx.array(lx.boolean()),
203
}),
204
});
205
206
-
attest(namespace.infer).type.toString.snap(`{
207
$type: "test.array.boolean"
208
flags?: boolean[] | undefined
209
}`);
210
});
211
212
test("InferArray handles unknown arrays", () => {
213
-
const namespace = lx.namespace("test.array.unknown", {
214
main: lx.object({
215
items: lx.array(lx.unknown()),
216
}),
217
});
218
219
-
attest(namespace.infer).type.toString.snap(`{
220
$type: "test.array.unknown"
221
items?: unknown[] | undefined
222
}`);
···
227
// ============================================================================
228
229
test("InferObject handles mixed optional and required fields", () => {
230
-
const namespace = lx.namespace("test.mixed", {
231
main: lx.object({
232
id: lx.string({ required: true }),
233
name: lx.string({ required: true }),
···
236
}),
237
});
238
239
-
attest(namespace.infer).type.toString.snap(`{
240
$type: "test.mixed"
241
age?: number | undefined
242
email?: string | undefined
···
246
});
247
248
test("InferObject handles all optional fields", () => {
249
-
const namespace = lx.namespace("test.allOptional", {
250
main: lx.object({
251
field1: lx.string(),
252
field2: lx.integer(),
···
254
}),
255
});
256
257
-
attest(namespace.infer).type.toString.snap(`{
258
$type: "test.allOptional"
259
field1?: string | undefined
260
field2?: number | undefined
···
263
});
264
265
test("InferObject handles all required fields", () => {
266
-
const namespace = lx.namespace("test.allRequired", {
267
main: lx.object({
268
field1: lx.string({ required: true }),
269
field2: lx.integer({ required: true }),
···
271
}),
272
});
273
274
-
attest(namespace.infer).type.toString.snap(`{
275
$type: "test.allRequired"
276
field1: string
277
field2: number
···
284
// ============================================================================
285
286
test("InferObject handles nullable optional field", () => {
287
-
const namespace = lx.namespace("test.nullableOptional", {
288
main: lx.object({
289
description: lx.string({ nullable: true }),
290
}),
291
});
292
293
-
attest(namespace.infer).type.toString.snap(`{
294
$type: "test.nullableOptional"
295
description?: string | null | undefined
296
}`);
297
});
298
299
test("InferObject handles multiple nullable fields", () => {
300
-
const namespace = lx.namespace("test.multipleNullable", {
301
main: lx.object({
302
field1: lx.string({ nullable: true }),
303
field2: lx.integer({ nullable: true }),
···
305
}),
306
});
307
308
-
attest(namespace.infer).type.toString.snap(`{
309
$type: "test.multipleNullable"
310
field1?: string | null | undefined
311
field2?: number | null | undefined
···
314
});
315
316
test("InferObject handles nullable and required field", () => {
317
-
const namespace = lx.namespace("test.nullableRequired", {
318
main: lx.object({
319
value: lx.string({ nullable: true, required: true }),
320
}),
321
});
322
323
-
attest(namespace.infer).type.toString.snap(`{
324
$type: "test.nullableRequired"
325
value: string | null
326
}`);
327
});
328
329
test("InferObject handles mixed nullable, required, and optional", () => {
330
-
const namespace = lx.namespace("test.mixedNullable", {
331
main: lx.object({
332
requiredNullable: lx.string({ required: true, nullable: true }),
333
optionalNullable: lx.string({ nullable: true }),
···
336
}),
337
});
338
339
-
attest(namespace.infer).type.toString.snap(`{
340
$type: "test.mixedNullable"
341
optional?: string | undefined
342
required: string
···
350
// ============================================================================
351
352
test("InferRef handles basic reference", () => {
353
-
const namespace = lx.namespace("test.ref", {
354
main: lx.object({
355
post: lx.ref("com.example.post"),
356
}),
357
});
358
359
-
attest(namespace.infer).type.toString.snap(`{
360
$type: "test.ref"
361
post?:
362
| { [x: string]: unknown; $type: "com.example.post" }
···
365
});
366
367
test("InferRef handles required reference", () => {
368
-
const namespace = lx.namespace("test.refRequired", {
369
main: lx.object({
370
author: lx.ref("com.example.user", { required: true }),
371
}),
372
});
373
374
-
attest(namespace.infer).type.toString.snap(`{
375
$type: "test.refRequired"
376
author?:
377
| { [x: string]: unknown; $type: "com.example.user" }
···
380
});
381
382
test("InferRef handles nullable reference", () => {
383
-
const namespace = lx.namespace("test.refNullable", {
384
main: lx.object({
385
parent: lx.ref("com.example.node", { nullable: true }),
386
}),
387
});
388
389
-
attest(namespace.infer).type.toString.snap(`{
390
$type: "test.refNullable"
391
parent?:
392
| { [x: string]: unknown; $type: "com.example.node" }
···
399
// ============================================================================
400
401
test("InferUnion handles basic union", () => {
402
-
const namespace = lx.namespace("test.union", {
403
main: lx.object({
404
content: lx.union(["com.example.text", "com.example.image"]),
405
}),
406
});
407
408
-
attest(namespace.infer).type.toString.snap(`{
409
$type: "test.union"
410
content?:
411
| { [x: string]: unknown; $type: "com.example.text" }
···
415
});
416
417
test("InferUnion handles required union", () => {
418
-
const namespace = lx.namespace("test.unionRequired", {
419
main: lx.object({
420
media: lx.union(["com.example.video", "com.example.audio"], {
421
required: true,
···
423
}),
424
});
425
426
-
attest(namespace.infer).type.toString.snap(`{
427
$type: "test.unionRequired"
428
media:
429
| { [x: string]: unknown; $type: "com.example.video" }
···
432
});
433
434
test("InferUnion handles union with many types", () => {
435
-
const namespace = lx.namespace("test.unionMultiple", {
436
main: lx.object({
437
attachment: lx.union([
438
"com.example.image",
···
443
}),
444
});
445
446
-
attest(namespace.infer).type.toString.snap(`{
447
$type: "test.unionMultiple"
448
attachment?:
449
| { [x: string]: unknown; $type: "com.example.image" }
···
462
// ============================================================================
463
464
test("InferParams handles basic params", () => {
465
-
const namespace = lx.namespace("test.params", {
466
main: lx.params({
467
limit: lx.integer(),
468
offset: lx.integer(),
469
}),
470
});
471
472
-
attest(namespace.infer).type.toString.snap(`{
473
$type: "test.params"
474
limit?: number | undefined
475
offset?: number | undefined
···
477
});
478
479
test("InferParams handles required params", () => {
480
-
const namespace = lx.namespace("test.paramsRequired", {
481
main: lx.params({
482
query: lx.string({ required: true }),
483
limit: lx.integer(),
484
}),
485
});
486
487
-
attest(namespace.infer).type.toString.snap(`{
488
$type: "test.paramsRequired"
489
limit?: number | undefined
490
query: string
···
496
// ============================================================================
497
498
test("InferRecord handles record with object schema", () => {
499
-
const namespace = lx.namespace("test.record", {
500
main: lx.record({
501
key: "tid",
502
record: lx.object({
···
507
}),
508
});
509
510
-
attest(namespace.infer).type.toString.snap(`{
511
$type: "test.record"
512
published?: boolean | undefined
513
content: string
···
520
// ============================================================================
521
522
test("InferObject handles nested objects", () => {
523
-
const namespace = lx.namespace("test.nested", {
524
main: lx.object({
525
user: lx.object({
526
name: lx.string({ required: true }),
···
529
}),
530
});
531
532
-
attest(namespace.infer).type.toString.snap(`{
533
$type: "test.nested"
534
user?: { name: string; email: string } | undefined
535
}`);
536
});
537
538
test("InferObject handles deeply nested objects", () => {
539
-
const namespace = lx.namespace("test.deepNested", {
540
main: lx.object({
541
data: lx.object({
542
user: lx.object({
···
548
}),
549
});
550
551
-
attest(namespace.infer).type.toString.snap(`{
552
$type: "test.deepNested"
553
data?:
554
| {
···
565
// ============================================================================
566
567
test("InferArray handles arrays of objects", () => {
568
-
const namespace = lx.namespace("test.arrayOfObjects", {
569
main: lx.object({
570
users: lx.array(
571
lx.object({
···
576
}),
577
});
578
579
-
attest(namespace.infer).type.toString.snap(`{
580
$type: "test.arrayOfObjects"
581
users?: { id: string; name: string }[] | undefined
582
}`);
···
587
matrix: lx.array(lx.array(lx.integer())),
588
});
589
590
-
const namespace = lx.namespace("test.nestedArrays", {
591
main: schema,
592
});
593
594
-
attest(namespace.infer).type.toString.snap(`{
595
$type: "test.nestedArrays"
596
matrix?: number[][] | undefined
597
}`);
598
});
599
600
test("InferArray handles arrays of refs", () => {
601
-
const namespace = lx.namespace("test.arrayOfRefs", {
602
main: lx.object({
603
followers: lx.array(lx.ref("com.example.user")),
604
}),
605
});
606
607
-
attest(namespace.infer).type.toString.snap(`{
608
$type: "test.arrayOfRefs"
609
followers?:
610
| { [x: string]: unknown; $type: "com.example.user" }[]
···
617
// ============================================================================
618
619
test("InferObject handles complex nested structure", () => {
620
-
const namespace = lx.namespace("test.complex", {
621
main: lx.object({
622
id: lx.string({ required: true }),
623
author: lx.object({
···
635
}),
636
});
637
638
-
attest(namespace.infer).type.toString.snap(`{
639
$type: "test.complex"
640
tags?: string[] | undefined
641
content?:
···
665
// ============================================================================
666
667
test("InferNS handles multiple defs in namespace", () => {
668
-
const namespace = lx.namespace("com.example.app", {
669
user: lx.object({
670
name: lx.string({ required: true }),
671
email: lx.string({ required: true }),
···
680
}),
681
});
682
683
-
attest(namespace.infer).type.toString.snap("never");
684
});
685
686
test("InferNS handles namespace with record and object defs", () => {
687
-
const namespace = lx.namespace("com.example.blog", {
688
main: lx.record({
689
key: "tid",
690
record: lx.object({
···
698
}),
699
});
700
701
-
attest(namespace.infer).type.toString.snap(`{
702
$type: "com.example.blog"
703
title: string
704
body: string
···
710
// ============================================================================
711
712
test("Local ref resolution: resolves refs to actual types", () => {
713
-
const ns = lx.namespace("test", {
714
user: lx.object({
715
name: lx.string({ required: true }),
716
email: lx.string({ required: true }),
···
731
});
732
733
test("Local ref resolution: refs in arrays", () => {
734
-
const ns = lx.namespace("test", {
735
user: lx.object({
736
name: lx.string({ required: true }),
737
}),
···
747
});
748
749
test("Local ref resolution: refs in unions", () => {
750
-
const ns = lx.namespace("test", {
751
text: lx.object({ content: lx.string({ required: true }) }),
752
image: lx.object({ url: lx.string({ required: true }) }),
753
main: lx.object({
···
765
});
766
767
test("Local ref resolution: nested refs", () => {
768
-
const ns = lx.namespace("test", {
769
profile: lx.object({
770
bio: lx.string({ required: true }),
771
}),
···
797
// ============================================================================
798
799
test("Edge case: circular reference detection", () => {
800
-
const ns = lx.namespace("test", {
801
main: lx.object({
802
value: lx.string({ required: true }),
803
parent: lx.ref("#main"),
···
820
});
821
822
test("Edge case: circular reference between multiple types", () => {
823
-
const ns = lx.namespace("test", {
824
user: lx.object({
825
name: lx.string({ required: true }),
826
posts: lx.array(lx.ref("#post")),
···
855
});
856
857
test("Edge case: missing reference detection", () => {
858
-
const ns = lx.namespace("test", {
859
main: lx.object({
860
author: lx.ref("#user", { required: true }),
861
}),
···
3
import { lx } from "../src/lib.ts";
4
5
test("InferNS produces expected type shape", () => {
6
+
const exampleLexicon = lx.lexicon("com.example.post", {
7
main: lx.record({
8
key: "tid",
9
record: lx.object({
···
26
});
27
28
test("InferObject handles required fields", () => {
29
+
const schema = lx.lexicon("test", {
30
main: lx.object({
31
required: lx.string({ required: true }),
32
optional: lx.string(),
···
41
});
42
43
test("InferObject handles nullable fields", () => {
44
+
const schema = lx.lexicon("test", {
45
main: lx.object({
46
nullable: lx.string({ nullable: true, required: true }),
47
}),
···
57
// ============================================================================
58
59
test("InferType handles string primitive", () => {
60
+
const lexicon = lx.lexicon("test.string", {
61
main: lx.object({
62
simpleString: lx.string(),
63
}),
64
});
65
66
+
attest(lexicon.infer).type.toString.snap(`{
67
$type: "test.string"
68
simpleString?: string | undefined
69
}`);
70
});
71
72
test("InferType handles integer primitive", () => {
73
+
const lexicon = lx.lexicon("test.integer", {
74
main: lx.object({
75
count: lx.integer(),
76
age: lx.integer({ minimum: 0, maximum: 120 }),
77
}),
78
});
79
80
+
attest(lexicon.infer).type.toString.snap(`{
81
$type: "test.integer"
82
count?: number | undefined
83
age?: number | undefined
···
85
});
86
87
test("InferType handles boolean primitive", () => {
88
+
const lexicon = lx.lexicon("test.boolean", {
89
main: lx.object({
90
isActive: lx.boolean(),
91
hasAccess: lx.boolean({ required: true }),
92
}),
93
});
94
95
+
attest(lexicon.infer).type.toString.snap(`{
96
$type: "test.boolean"
97
isActive?: boolean | undefined
98
hasAccess: boolean
···
100
});
101
102
test("InferType handles null primitive", () => {
103
+
const lexicon = lx.lexicon("test.null", {
104
main: lx.object({
105
nullValue: lx.null(),
106
}),
107
});
108
109
+
attest(lexicon.infer).type.toString.snap(`{
110
$type: "test.null"
111
nullValue?: null | undefined
112
}`);
113
});
114
115
test("InferType handles unknown primitive", () => {
116
+
const lexicon = lx.lexicon("test.unknown", {
117
main: lx.object({
118
metadata: lx.unknown(),
119
}),
120
});
121
122
+
attest(lexicon.infer).type.toString.snap(
123
'{ $type: "test.unknown"; metadata?: unknown }',
124
);
125
});
126
127
test("InferType handles bytes primitive", () => {
128
+
const lexicon = lx.lexicon("test.bytes", {
129
main: lx.object({
130
data: lx.bytes(),
131
}),
132
});
133
134
+
attest(lexicon.infer).type.toString.snap(`{
135
$type: "test.bytes"
136
data?: Uint8Array<ArrayBufferLike> | undefined
137
}`);
138
});
139
140
test("InferType handles blob primitive", () => {
141
+
const lexicon = lx.lexicon("test.blob", {
142
main: lx.object({
143
image: lx.blob({ accept: ["image/png", "image/jpeg"] }),
144
}),
145
});
146
147
+
attest(lexicon.infer).type.toString.snap(
148
'{ $type: "test.blob"; image?: Blob | undefined }',
149
);
150
});
···
154
// ============================================================================
155
156
test("InferToken handles basic token without enum", () => {
157
+
const lexicon = lx.lexicon("test.token", {
158
main: lx.object({
159
symbol: lx.token("A symbolic value"),
160
}),
161
});
162
163
+
attest(lexicon.infer).type.toString.snap(`{
164
$type: "test.token"
165
symbol?: string | undefined
166
}`);
···
171
// ============================================================================
172
173
test("InferArray handles string arrays", () => {
174
+
const lexicon = lx.lexicon("test.array.string", {
175
main: lx.object({
176
tags: lx.array(lx.string()),
177
}),
178
});
179
180
+
attest(lexicon.infer).type.toString.snap(`{
181
$type: "test.array.string"
182
tags?: string[] | undefined
183
}`);
184
});
185
186
test("InferArray handles integer arrays", () => {
187
+
const lexicon = lx.lexicon("test.array.integer", {
188
main: lx.object({
189
scores: lx.array(lx.integer(), { minLength: 1, maxLength: 10 }),
190
}),
191
});
192
193
+
attest(lexicon.infer).type.toString.snap(`{
194
$type: "test.array.integer"
195
scores?: number[] | undefined
196
}`);
197
});
198
199
test("InferArray handles boolean arrays", () => {
200
+
const lexicon = lx.lexicon("test.array.boolean", {
201
main: lx.object({
202
flags: lx.array(lx.boolean()),
203
}),
204
});
205
206
+
attest(lexicon.infer).type.toString.snap(`{
207
$type: "test.array.boolean"
208
flags?: boolean[] | undefined
209
}`);
210
});
211
212
test("InferArray handles unknown arrays", () => {
213
+
const lexicon = lx.lexicon("test.array.unknown", {
214
main: lx.object({
215
items: lx.array(lx.unknown()),
216
}),
217
});
218
219
+
attest(lexicon.infer).type.toString.snap(`{
220
$type: "test.array.unknown"
221
items?: unknown[] | undefined
222
}`);
···
227
// ============================================================================
228
229
test("InferObject handles mixed optional and required fields", () => {
230
+
const lexicon = lx.lexicon("test.mixed", {
231
main: lx.object({
232
id: lx.string({ required: true }),
233
name: lx.string({ required: true }),
···
236
}),
237
});
238
239
+
attest(lexicon.infer).type.toString.snap(`{
240
$type: "test.mixed"
241
age?: number | undefined
242
email?: string | undefined
···
246
});
247
248
test("InferObject handles all optional fields", () => {
249
+
const lexicon = lx.lexicon("test.allOptional", {
250
main: lx.object({
251
field1: lx.string(),
252
field2: lx.integer(),
···
254
}),
255
});
256
257
+
attest(lexicon.infer).type.toString.snap(`{
258
$type: "test.allOptional"
259
field1?: string | undefined
260
field2?: number | undefined
···
263
});
264
265
test("InferObject handles all required fields", () => {
266
+
const lexicon = lx.lexicon("test.allRequired", {
267
main: lx.object({
268
field1: lx.string({ required: true }),
269
field2: lx.integer({ required: true }),
···
271
}),
272
});
273
274
+
attest(lexicon.infer).type.toString.snap(`{
275
$type: "test.allRequired"
276
field1: string
277
field2: number
···
284
// ============================================================================
285
286
test("InferObject handles nullable optional field", () => {
287
+
const lexicon = lx.lexicon("test.nullableOptional", {
288
main: lx.object({
289
description: lx.string({ nullable: true }),
290
}),
291
});
292
293
+
attest(lexicon.infer).type.toString.snap(`{
294
$type: "test.nullableOptional"
295
description?: string | null | undefined
296
}`);
297
});
298
299
test("InferObject handles multiple nullable fields", () => {
300
+
const lexicon = lx.lexicon("test.multipleNullable", {
301
main: lx.object({
302
field1: lx.string({ nullable: true }),
303
field2: lx.integer({ nullable: true }),
···
305
}),
306
});
307
308
+
attest(lexicon.infer).type.toString.snap(`{
309
$type: "test.multipleNullable"
310
field1?: string | null | undefined
311
field2?: number | null | undefined
···
314
});
315
316
test("InferObject handles nullable and required field", () => {
317
+
const lexicon = lx.lexicon("test.nullableRequired", {
318
main: lx.object({
319
value: lx.string({ nullable: true, required: true }),
320
}),
321
});
322
323
+
attest(lexicon.infer).type.toString.snap(`{
324
$type: "test.nullableRequired"
325
value: string | null
326
}`);
327
});
328
329
test("InferObject handles mixed nullable, required, and optional", () => {
330
+
const lexicon = lx.lexicon("test.mixedNullable", {
331
main: lx.object({
332
requiredNullable: lx.string({ required: true, nullable: true }),
333
optionalNullable: lx.string({ nullable: true }),
···
336
}),
337
});
338
339
+
attest(lexicon.infer).type.toString.snap(`{
340
$type: "test.mixedNullable"
341
optional?: string | undefined
342
required: string
···
350
// ============================================================================
351
352
test("InferRef handles basic reference", () => {
353
+
const lexicon = lx.lexicon("test.ref", {
354
main: lx.object({
355
post: lx.ref("com.example.post"),
356
}),
357
});
358
359
+
attest(lexicon.infer).type.toString.snap(`{
360
$type: "test.ref"
361
post?:
362
| { [x: string]: unknown; $type: "com.example.post" }
···
365
});
366
367
test("InferRef handles required reference", () => {
368
+
const lexicon = lx.lexicon("test.refRequired", {
369
main: lx.object({
370
author: lx.ref("com.example.user", { required: true }),
371
}),
372
});
373
374
+
attest(lexicon.infer).type.toString.snap(`{
375
$type: "test.refRequired"
376
author?:
377
| { [x: string]: unknown; $type: "com.example.user" }
···
380
});
381
382
test("InferRef handles nullable reference", () => {
383
+
const lexicon = lx.lexicon("test.refNullable", {
384
main: lx.object({
385
parent: lx.ref("com.example.node", { nullable: true }),
386
}),
387
});
388
389
+
attest(lexicon.infer).type.toString.snap(`{
390
$type: "test.refNullable"
391
parent?:
392
| { [x: string]: unknown; $type: "com.example.node" }
···
399
// ============================================================================
400
401
test("InferUnion handles basic union", () => {
402
+
const lexicon = lx.lexicon("test.union", {
403
main: lx.object({
404
content: lx.union(["com.example.text", "com.example.image"]),
405
}),
406
});
407
408
+
attest(lexicon.infer).type.toString.snap(`{
409
$type: "test.union"
410
content?:
411
| { [x: string]: unknown; $type: "com.example.text" }
···
415
});
416
417
test("InferUnion handles required union", () => {
418
+
const lexicon = lx.lexicon("test.unionRequired", {
419
main: lx.object({
420
media: lx.union(["com.example.video", "com.example.audio"], {
421
required: true,
···
423
}),
424
});
425
426
+
attest(lexicon.infer).type.toString.snap(`{
427
$type: "test.unionRequired"
428
media:
429
| { [x: string]: unknown; $type: "com.example.video" }
···
432
});
433
434
test("InferUnion handles union with many types", () => {
435
+
const lexicon = lx.lexicon("test.unionMultiple", {
436
main: lx.object({
437
attachment: lx.union([
438
"com.example.image",
···
443
}),
444
});
445
446
+
attest(lexicon.infer).type.toString.snap(`{
447
$type: "test.unionMultiple"
448
attachment?:
449
| { [x: string]: unknown; $type: "com.example.image" }
···
462
// ============================================================================
463
464
test("InferParams handles basic params", () => {
465
+
const lexicon = lx.lexicon("test.params", {
466
main: lx.params({
467
limit: lx.integer(),
468
offset: lx.integer(),
469
}),
470
});
471
472
+
attest(lexicon.infer).type.toString.snap(`{
473
$type: "test.params"
474
limit?: number | undefined
475
offset?: number | undefined
···
477
});
478
479
test("InferParams handles required params", () => {
480
+
const lexicon = lx.lexicon("test.paramsRequired", {
481
main: lx.params({
482
query: lx.string({ required: true }),
483
limit: lx.integer(),
484
}),
485
});
486
487
+
attest(lexicon.infer).type.toString.snap(`{
488
$type: "test.paramsRequired"
489
limit?: number | undefined
490
query: string
···
496
// ============================================================================
497
498
test("InferRecord handles record with object schema", () => {
499
+
const lexicon = lx.lexicon("test.record", {
500
main: lx.record({
501
key: "tid",
502
record: lx.object({
···
507
}),
508
});
509
510
+
attest(lexicon.infer).type.toString.snap(`{
511
$type: "test.record"
512
published?: boolean | undefined
513
content: string
···
520
// ============================================================================
521
522
test("InferObject handles nested objects", () => {
523
+
const lexicon = lx.lexicon("test.nested", {
524
main: lx.object({
525
user: lx.object({
526
name: lx.string({ required: true }),
···
529
}),
530
});
531
532
+
attest(lexicon.infer).type.toString.snap(`{
533
$type: "test.nested"
534
user?: { name: string; email: string } | undefined
535
}`);
536
});
537
538
test("InferObject handles deeply nested objects", () => {
539
+
const lexicon = lx.lexicon("test.deepNested", {
540
main: lx.object({
541
data: lx.object({
542
user: lx.object({
···
548
}),
549
});
550
551
+
attest(lexicon.infer).type.toString.snap(`{
552
$type: "test.deepNested"
553
data?:
554
| {
···
565
// ============================================================================
566
567
test("InferArray handles arrays of objects", () => {
568
+
const lexicon = lx.lexicon("test.arrayOfObjects", {
569
main: lx.object({
570
users: lx.array(
571
lx.object({
···
576
}),
577
});
578
579
+
attest(lexicon.infer).type.toString.snap(`{
580
$type: "test.arrayOfObjects"
581
users?: { id: string; name: string }[] | undefined
582
}`);
···
587
matrix: lx.array(lx.array(lx.integer())),
588
});
589
590
+
const lexicon = lx.lexicon("test.nestedArrays", {
591
main: schema,
592
});
593
594
+
attest(lexicon.infer).type.toString.snap(`{
595
$type: "test.nestedArrays"
596
matrix?: number[][] | undefined
597
}`);
598
});
599
600
test("InferArray handles arrays of refs", () => {
601
+
const lexicon = lx.lexicon("test.arrayOfRefs", {
602
main: lx.object({
603
followers: lx.array(lx.ref("com.example.user")),
604
}),
605
});
606
607
+
attest(lexicon.infer).type.toString.snap(`{
608
$type: "test.arrayOfRefs"
609
followers?:
610
| { [x: string]: unknown; $type: "com.example.user" }[]
···
617
// ============================================================================
618
619
test("InferObject handles complex nested structure", () => {
620
+
const lexicon = lx.lexicon("test.complex", {
621
main: lx.object({
622
id: lx.string({ required: true }),
623
author: lx.object({
···
635
}),
636
});
637
638
+
attest(lexicon.infer).type.toString.snap(`{
639
$type: "test.complex"
640
tags?: string[] | undefined
641
content?:
···
665
// ============================================================================
666
667
test("InferNS handles multiple defs in namespace", () => {
668
+
const lexicon = lx.lexicon("com.example.app", {
669
user: lx.object({
670
name: lx.string({ required: true }),
671
email: lx.string({ required: true }),
···
680
}),
681
});
682
683
+
attest(lexicon.infer).type.toString.snap("never");
684
});
685
686
test("InferNS handles namespace with record and object defs", () => {
687
+
const lexicon = lx.lexicon("com.example.blog", {
688
main: lx.record({
689
key: "tid",
690
record: lx.object({
···
698
}),
699
});
700
701
+
attest(lexicon.infer).type.toString.snap(`{
702
$type: "com.example.blog"
703
title: string
704
body: string
···
710
// ============================================================================
711
712
test("Local ref resolution: resolves refs to actual types", () => {
713
+
const ns = lx.lexicon("test", {
714
user: lx.object({
715
name: lx.string({ required: true }),
716
email: lx.string({ required: true }),
···
731
});
732
733
test("Local ref resolution: refs in arrays", () => {
734
+
const ns = lx.lexicon("test", {
735
user: lx.object({
736
name: lx.string({ required: true }),
737
}),
···
747
});
748
749
test("Local ref resolution: refs in unions", () => {
750
+
const ns = lx.lexicon("test", {
751
text: lx.object({ content: lx.string({ required: true }) }),
752
image: lx.object({ url: lx.string({ required: true }) }),
753
main: lx.object({
···
765
});
766
767
test("Local ref resolution: nested refs", () => {
768
+
const ns = lx.lexicon("test", {
769
profile: lx.object({
770
bio: lx.string({ required: true }),
771
}),
···
797
// ============================================================================
798
799
test("Edge case: circular reference detection", () => {
800
+
const ns = lx.lexicon("test", {
801
main: lx.object({
802
value: lx.string({ required: true }),
803
parent: lx.ref("#main"),
···
820
});
821
822
test("Edge case: circular reference between multiple types", () => {
823
+
const ns = lx.lexicon("test", {
824
user: lx.object({
825
name: lx.string({ required: true }),
826
posts: lx.array(lx.ref("#post")),
···
855
});
856
857
test("Edge case: missing reference detection", () => {
858
+
const ns = lx.lexicon("test", {
859
main: lx.object({
860
author: lx.ref("#user", { required: true }),
861
}),
+6
-6
packages/site/src/components/Playground.tsx
···
47
const timeoutId = setTimeout(async () => {
48
try {
49
const nsMatch = code.match(
50
-
/const\s+ns\s*=\s*lx\.namespace\([^]*?\}\s*\);/,
51
);
52
if (!nsMatch) {
53
-
throw new Error("No namespace definition found");
54
}
55
56
const cleanedCode = nsMatch[0];
57
-
const wrappedCode = `${cleanedCode}\nreturn ns;`;
58
const fn = new Function("lx", wrappedCode);
59
const result = fn(lx);
60
let typeInfo = "// Hover over .infer in the editor to see the type";
···
180
181
const DEFAULT_CODE = `import { lx, type Infer } from "prototypey";
182
183
-
const ns = lx.namespace("app.bsky.actor.profile", {
184
main: lx.record({
185
key: "self",
186
record: lx.object({
···
190
}),
191
});
192
193
-
type ProfileInferred = Infer<typeof ns>;
194
195
-
const aProfile: ProfileInferred = {
196
$type: "app.bsky.actor.profile",
197
displayName: "Benny Harvey"
198
}`;
···
47
const timeoutId = setTimeout(async () => {
48
try {
49
const nsMatch = code.match(
50
+
/const\s+ns\s*=\s*lx\.lexicon\([^]*?\}\s*\);/,
51
);
52
if (!nsMatch) {
53
+
throw new Error("No lexicon definition found");
54
}
55
56
const cleanedCode = nsMatch[0];
57
+
const wrappedCode = `${cleanedCode}\nreturn lex;`;
58
const fn = new Function("lx", wrappedCode);
59
const result = fn(lx);
60
let typeInfo = "// Hover over .infer in the editor to see the type";
···
180
181
const DEFAULT_CODE = `import { lx, type Infer } from "prototypey";
182
183
+
const lex = lx.lexicon("app.bsky.actor.profile", {
184
main: lx.record({
185
key: "self",
186
record: lx.object({
···
190
}),
191
});
192
193
+
type Profile = Infer<typeof lex>;
194
195
+
const aProfile: Profile = {
196
$type: "app.bsky.actor.profile",
197
displayName: "Benny Harvey"
198
}`;
+1
-1
packages/site/tests/components/Playground.test.tsx
···
85
const inputEditor = editors[0] as HTMLTextAreaElement;
86
87
expect(inputEditor.value).toContain(
88
-
'lx.namespace("app.bsky.actor.profile"',
89
);
90
});
91
···
85
const inputEditor = editors[0] as HTMLTextAreaElement;
86
87
expect(inputEditor.value).toContain(
88
+
'lx.lexicon("app.bsky.actor.profile"',
89
);
90
});
91