tangled
alpha
login
or
join now
tylur.dev
/
prototypey
1
fork
atom
prototypey.org - atproto lexicon typescript toolkit - mirror https://github.com/tylersayshi/prototypey
1
fork
atom
overview
issues
pulls
pipelines
added tests for json emit
Tyler
5 months ago
46f917c9
e9acbecd
+660
-2
7 changed files
expand all
collapse all
unified
split
packages
cli
package.json
src
commands
gen-emit.ts
index.ts
tests
commands
gen-emit.test.ts
fixtures
simple-lexicon.ts
vitest.config.ts
pnpm-lock.yaml
+5
-2
packages/cli/package.json
···
21
21
"README.md"
22
22
],
23
23
"scripts": {
24
24
-
"build": "tsdown --entry src/index.ts --format esm --dts false"
24
24
+
"build": "tsdown --entry src/index.ts --format esm --dts false",
25
25
+
"test": "vitest run",
26
26
+
"tsc": "tsc"
25
27
},
26
28
"dependencies": {
27
29
"prototypey": "workspace:*",
···
31
33
"devDependencies": {
32
34
"@types/node": "24.0.4",
33
35
"tsdown": "0.12.7",
34
34
-
"typescript": "5.8.3"
36
36
+
"typescript": "5.8.3",
37
37
+
"vitest": "^3.2.4"
35
38
},
36
39
"engines": {
37
40
"node": ">=20.19.0"
+102
packages/cli/src/commands/gen-emit.ts
···
1
1
+
import { glob } from "tinyglobby";
2
2
+
import { mkdir, writeFile } from "node:fs/promises";
3
3
+
import { join } from "node:path";
4
4
+
import { pathToFileURL } from "node:url";
5
5
+
6
6
+
interface LexiconNamespace {
7
7
+
json: {
8
8
+
lexicon: number;
9
9
+
id: string;
10
10
+
defs: Record<string, unknown>;
11
11
+
};
12
12
+
}
13
13
+
14
14
+
export async function genEmit(
15
15
+
outdir: string,
16
16
+
sources: string | string[],
17
17
+
): Promise<void> {
18
18
+
try {
19
19
+
const sourcePatterns = Array.isArray(sources) ? sources : [sources];
20
20
+
21
21
+
// Find all source files matching the patterns
22
22
+
const sourceFiles = await glob(sourcePatterns, {
23
23
+
absolute: true,
24
24
+
onlyFiles: true,
25
25
+
});
26
26
+
27
27
+
if (sourceFiles.length === 0) {
28
28
+
console.log("No source files found matching patterns:", sourcePatterns);
29
29
+
return;
30
30
+
}
31
31
+
32
32
+
console.log(`Found ${sourceFiles.length} source file(s)`);
33
33
+
34
34
+
// Ensure output directory exists
35
35
+
await mkdir(outdir, { recursive: true });
36
36
+
37
37
+
// Process each source file
38
38
+
for (const sourcePath of sourceFiles) {
39
39
+
await processSourceFile(sourcePath, outdir);
40
40
+
}
41
41
+
42
42
+
console.log(`\nEmitted lexicon schemas to ${outdir}`);
43
43
+
} catch (error) {
44
44
+
console.error("Error emitting lexicon schemas:", error);
45
45
+
process.exit(1);
46
46
+
}
47
47
+
}
48
48
+
49
49
+
async function processSourceFile(
50
50
+
sourcePath: string,
51
51
+
outdir: string,
52
52
+
): Promise<void> {
53
53
+
try {
54
54
+
// Convert file path to file URL for dynamic import
55
55
+
const fileUrl = pathToFileURL(sourcePath).href;
56
56
+
57
57
+
// Dynamically import the module
58
58
+
const module = await import(fileUrl);
59
59
+
60
60
+
// Find all exported namespaces
61
61
+
const namespaces: LexiconNamespace[] = [];
62
62
+
for (const key of Object.keys(module)) {
63
63
+
const exported = module[key];
64
64
+
// Check if it's a namespace with a json property
65
65
+
if (
66
66
+
exported &&
67
67
+
typeof exported === "object" &&
68
68
+
"json" in exported &&
69
69
+
exported.json &&
70
70
+
typeof exported.json === "object" &&
71
71
+
"lexicon" in exported.json &&
72
72
+
"id" in exported.json &&
73
73
+
"defs" in exported.json
74
74
+
) {
75
75
+
namespaces.push(exported as LexiconNamespace);
76
76
+
}
77
77
+
}
78
78
+
79
79
+
if (namespaces.length === 0) {
80
80
+
console.warn(` ⚠ ${sourcePath}: No lexicon namespaces found`);
81
81
+
return;
82
82
+
}
83
83
+
84
84
+
// Emit JSON for each namespace
85
85
+
for (const namespace of namespaces) {
86
86
+
const { id } = namespace.json;
87
87
+
const outputPath = join(outdir, `${id}.json`);
88
88
+
89
89
+
// Write the JSON file
90
90
+
await writeFile(
91
91
+
outputPath,
92
92
+
JSON.stringify(namespace.json, null, "\t"),
93
93
+
"utf-8",
94
94
+
);
95
95
+
96
96
+
console.log(` ✓ ${id} -> ${id}.json`);
97
97
+
}
98
98
+
} catch (error) {
99
99
+
console.error(` ✗ Error processing ${sourcePath}:`, error);
100
100
+
throw error;
101
101
+
}
102
102
+
}
+7
packages/cli/src/index.ts
···
1
1
#!/usr/bin/env node
2
2
import sade from "sade";
3
3
import { genInferred } from "./commands/gen-inferred.ts";
4
4
+
import { genEmit } from "./commands/gen-emit.ts";
4
5
5
6
const prog = sade("prototypey");
6
7
···
13
14
.describe("Generate type-inferred code from lexicon schemas")
14
15
.example("gen-inferred ./generated/inferred ./lexicons/**/*.json")
15
16
.action(genInferred);
17
17
+
18
18
+
prog
19
19
+
.command("gen-emit <outdir> <sources...>")
20
20
+
.describe("Emit JSON lexicon schemas from authored TypeScript")
21
21
+
.example("gen-emit ./lexicons ./src/lexicons/**/*.ts")
22
22
+
.action(genEmit);
16
23
17
24
prog.parse(process.argv);
+525
packages/cli/tests/commands/gen-emit.test.ts
···
1
1
+
import { expect, test, describe, beforeEach, afterEach } from "vitest";
2
2
+
import { mkdir, writeFile, rm, readFile } from "node:fs/promises";
3
3
+
import { join } from "node:path";
4
4
+
import { genEmit } from "../../src/commands/gen-emit.ts";
5
5
+
import { tmpdir } from "node:os";
6
6
+
7
7
+
describe("genEmit", () => {
8
8
+
let testDir: string;
9
9
+
let outDir: string;
10
10
+
11
11
+
beforeEach(async () => {
12
12
+
// Create a temporary directory for test files
13
13
+
testDir = join(tmpdir(), `prototypey-test-${Date.now()}`);
14
14
+
outDir = join(testDir, "output");
15
15
+
await mkdir(testDir, { recursive: true });
16
16
+
await mkdir(outDir, { recursive: true });
17
17
+
});
18
18
+
19
19
+
afterEach(async () => {
20
20
+
// Clean up test directory
21
21
+
await rm(testDir, { recursive: true, force: true });
22
22
+
});
23
23
+
24
24
+
test("emits JSON from a simple lexicon file", async () => {
25
25
+
// Create a test lexicon file
26
26
+
const lexiconFile = join(testDir, "profile.ts");
27
27
+
await writeFile(
28
28
+
lexiconFile,
29
29
+
`
30
30
+
import { lx } from "prototypey";
31
31
+
32
32
+
export const profileNamespace = lx.namespace("app.bsky.actor.profile", {
33
33
+
main: lx.record({
34
34
+
key: "self",
35
35
+
record: lx.object({
36
36
+
displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }),
37
37
+
description: lx.string({ maxLength: 256, maxGraphemes: 256 }),
38
38
+
}),
39
39
+
}),
40
40
+
});
41
41
+
`,
42
42
+
);
43
43
+
44
44
+
// Run the emit command
45
45
+
await genEmit(outDir, lexiconFile);
46
46
+
47
47
+
// Read the emitted JSON file
48
48
+
const outputFile = join(outDir, "app.bsky.actor.profile.json");
49
49
+
const content = await readFile(outputFile, "utf-8");
50
50
+
const json = JSON.parse(content);
51
51
+
52
52
+
// Verify the structure
53
53
+
expect(json).toEqual({
54
54
+
lexicon: 1,
55
55
+
id: "app.bsky.actor.profile",
56
56
+
defs: {
57
57
+
main: {
58
58
+
type: "record",
59
59
+
key: "self",
60
60
+
record: {
61
61
+
type: "object",
62
62
+
properties: {
63
63
+
displayName: {
64
64
+
type: "string",
65
65
+
maxLength: 64,
66
66
+
maxGraphemes: 64,
67
67
+
},
68
68
+
description: {
69
69
+
type: "string",
70
70
+
maxLength: 256,
71
71
+
maxGraphemes: 256,
72
72
+
},
73
73
+
},
74
74
+
},
75
75
+
},
76
76
+
},
77
77
+
});
78
78
+
});
79
79
+
80
80
+
test("emits JSON from multiple lexicon exports in one file", async () => {
81
81
+
// Create a test file with multiple exports
82
82
+
const lexiconFile = join(testDir, "multiple.ts");
83
83
+
await writeFile(
84
84
+
lexiconFile,
85
85
+
`
86
86
+
import { lx } from "prototypey";
87
87
+
88
88
+
export const profile = lx.namespace("app.bsky.actor.profile", {
89
89
+
main: lx.record({
90
90
+
key: "self",
91
91
+
record: lx.object({
92
92
+
displayName: lx.string({ maxLength: 64 }),
93
93
+
}),
94
94
+
}),
95
95
+
});
96
96
+
97
97
+
export const post = lx.namespace("app.bsky.feed.post", {
98
98
+
main: lx.record({
99
99
+
key: "tid",
100
100
+
record: lx.object({
101
101
+
text: lx.string({ maxLength: 300 }),
102
102
+
}),
103
103
+
}),
104
104
+
});
105
105
+
`,
106
106
+
);
107
107
+
108
108
+
// Run the emit command
109
109
+
await genEmit(outDir, lexiconFile);
110
110
+
111
111
+
// Verify both files were created
112
112
+
const profileJson = JSON.parse(
113
113
+
await readFile(join(outDir, "app.bsky.actor.profile.json"), "utf-8"),
114
114
+
);
115
115
+
const postJson = JSON.parse(
116
116
+
await readFile(join(outDir, "app.bsky.feed.post.json"), "utf-8"),
117
117
+
);
118
118
+
119
119
+
expect(profileJson.id).toBe("app.bsky.actor.profile");
120
120
+
expect(postJson.id).toBe("app.bsky.feed.post");
121
121
+
});
122
122
+
123
123
+
test("handles glob patterns for multiple files", async () => {
124
124
+
// Create multiple test files
125
125
+
const lexicons = join(testDir, "lexicons");
126
126
+
await mkdir(lexicons, { recursive: true });
127
127
+
128
128
+
await writeFile(
129
129
+
join(lexicons, "profile.ts"),
130
130
+
`
131
131
+
import { lx } from "prototypey";
132
132
+
export const schema = lx.namespace("app.bsky.actor.profile", {
133
133
+
main: lx.record({ key: "self", record: lx.object({}) }),
134
134
+
});
135
135
+
`,
136
136
+
);
137
137
+
138
138
+
await writeFile(
139
139
+
join(lexicons, "post.ts"),
140
140
+
`
141
141
+
import { lx } from "prototypey";
142
142
+
export const schema = lx.namespace("app.bsky.feed.post", {
143
143
+
main: lx.record({ key: "tid", record: lx.object({}) }),
144
144
+
});
145
145
+
`,
146
146
+
);
147
147
+
148
148
+
// Run with glob pattern
149
149
+
await genEmit(outDir, `${lexicons}/*.ts`);
150
150
+
151
151
+
// Verify both files were created
152
152
+
const profileExists = await readFile(
153
153
+
join(outDir, "app.bsky.actor.profile.json"),
154
154
+
"utf-8",
155
155
+
);
156
156
+
const postExists = await readFile(
157
157
+
join(outDir, "app.bsky.feed.post.json"),
158
158
+
"utf-8",
159
159
+
);
160
160
+
161
161
+
expect(profileExists).toBeTruthy();
162
162
+
expect(postExists).toBeTruthy();
163
163
+
});
164
164
+
165
165
+
test("emits query endpoint with parameters and output", async () => {
166
166
+
const lexiconFile = join(testDir, "search.ts");
167
167
+
await writeFile(
168
168
+
lexiconFile,
169
169
+
`
170
170
+
import { lx } from "prototypey";
171
171
+
172
172
+
export const searchPosts = lx.namespace("app.bsky.feed.searchPosts", {
173
173
+
main: lx.query({
174
174
+
description: "Find posts matching search criteria",
175
175
+
parameters: lx.params({
176
176
+
q: lx.string({ required: true }),
177
177
+
limit: lx.integer({ minimum: 1, maximum: 100, default: 25 }),
178
178
+
cursor: lx.string(),
179
179
+
}),
180
180
+
output: {
181
181
+
encoding: "application/json",
182
182
+
schema: lx.object({
183
183
+
cursor: lx.string(),
184
184
+
posts: lx.array(lx.ref("app.bsky.feed.defs#postView"), { required: true }),
185
185
+
}),
186
186
+
},
187
187
+
}),
188
188
+
});
189
189
+
`,
190
190
+
);
191
191
+
192
192
+
await genEmit(outDir, lexiconFile);
193
193
+
194
194
+
const outputFile = join(outDir, "app.bsky.feed.searchPosts.json");
195
195
+
const content = await readFile(outputFile, "utf-8");
196
196
+
const json = JSON.parse(content);
197
197
+
198
198
+
expect(json).toEqual({
199
199
+
lexicon: 1,
200
200
+
id: "app.bsky.feed.searchPosts",
201
201
+
defs: {
202
202
+
main: {
203
203
+
type: "query",
204
204
+
description: "Find posts matching search criteria",
205
205
+
parameters: {
206
206
+
type: "params",
207
207
+
properties: {
208
208
+
q: { type: "string", required: true },
209
209
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 },
210
210
+
cursor: { type: "string" },
211
211
+
},
212
212
+
required: ["q"],
213
213
+
},
214
214
+
output: {
215
215
+
encoding: "application/json",
216
216
+
schema: {
217
217
+
type: "object",
218
218
+
properties: {
219
219
+
cursor: { type: "string" },
220
220
+
posts: {
221
221
+
type: "array",
222
222
+
items: { type: "ref", ref: "app.bsky.feed.defs#postView" },
223
223
+
required: true,
224
224
+
},
225
225
+
},
226
226
+
required: ["posts"],
227
227
+
},
228
228
+
},
229
229
+
},
230
230
+
},
231
231
+
});
232
232
+
});
233
233
+
234
234
+
test("emits procedure endpoint with input and output", async () => {
235
235
+
const lexiconFile = join(testDir, "create-post.ts");
236
236
+
await writeFile(
237
237
+
lexiconFile,
238
238
+
`
239
239
+
import { lx } from "prototypey";
240
240
+
241
241
+
export const createPost = lx.namespace("com.atproto.repo.createRecord", {
242
242
+
main: lx.procedure({
243
243
+
description: "Create a record",
244
244
+
input: {
245
245
+
encoding: "application/json",
246
246
+
schema: lx.object({
247
247
+
repo: lx.string({ required: true }),
248
248
+
collection: lx.string({ required: true }),
249
249
+
record: lx.unknown({ required: true }),
250
250
+
}),
251
251
+
},
252
252
+
output: {
253
253
+
encoding: "application/json",
254
254
+
schema: lx.object({
255
255
+
uri: lx.string({ required: true }),
256
256
+
cid: lx.string({ required: true }),
257
257
+
}),
258
258
+
},
259
259
+
}),
260
260
+
});
261
261
+
`,
262
262
+
);
263
263
+
264
264
+
await genEmit(outDir, lexiconFile);
265
265
+
266
266
+
const outputFile = join(outDir, "com.atproto.repo.createRecord.json");
267
267
+
const content = await readFile(outputFile, "utf-8");
268
268
+
const json = JSON.parse(content);
269
269
+
270
270
+
expect(json).toEqual({
271
271
+
lexicon: 1,
272
272
+
id: "com.atproto.repo.createRecord",
273
273
+
defs: {
274
274
+
main: {
275
275
+
type: "procedure",
276
276
+
description: "Create a record",
277
277
+
input: {
278
278
+
encoding: "application/json",
279
279
+
schema: {
280
280
+
type: "object",
281
281
+
properties: {
282
282
+
repo: { type: "string", required: true },
283
283
+
collection: { type: "string", required: true },
284
284
+
record: { type: "unknown", required: true },
285
285
+
},
286
286
+
required: ["repo", "collection", "record"],
287
287
+
},
288
288
+
},
289
289
+
output: {
290
290
+
encoding: "application/json",
291
291
+
schema: {
292
292
+
type: "object",
293
293
+
properties: {
294
294
+
uri: { type: "string", required: true },
295
295
+
cid: { type: "string", required: true },
296
296
+
},
297
297
+
required: ["uri", "cid"],
298
298
+
},
299
299
+
},
300
300
+
},
301
301
+
},
302
302
+
});
303
303
+
});
304
304
+
305
305
+
test("emits subscription endpoint with message union", async () => {
306
306
+
const lexiconFile = join(testDir, "subscription.ts");
307
307
+
await writeFile(
308
308
+
lexiconFile,
309
309
+
`
310
310
+
import { lx } from "prototypey";
311
311
+
312
312
+
export const subscribeRepos = lx.namespace("com.atproto.sync.subscribeRepos", {
313
313
+
main: lx.subscription({
314
314
+
description: "Repository event stream",
315
315
+
parameters: lx.params({
316
316
+
cursor: lx.integer(),
317
317
+
}),
318
318
+
message: {
319
319
+
schema: lx.union(["#commit", "#identity", "#account"]),
320
320
+
},
321
321
+
}),
322
322
+
commit: lx.object({
323
323
+
seq: lx.integer({ required: true }),
324
324
+
rebase: lx.boolean({ required: true }),
325
325
+
}),
326
326
+
identity: lx.object({
327
327
+
seq: lx.integer({ required: true }),
328
328
+
did: lx.string({ required: true, format: "did" }),
329
329
+
}),
330
330
+
account: lx.object({
331
331
+
seq: lx.integer({ required: true }),
332
332
+
active: lx.boolean({ required: true }),
333
333
+
}),
334
334
+
});
335
335
+
`,
336
336
+
);
337
337
+
338
338
+
await genEmit(outDir, lexiconFile);
339
339
+
340
340
+
const outputFile = join(outDir, "com.atproto.sync.subscribeRepos.json");
341
341
+
const content = await readFile(outputFile, "utf-8");
342
342
+
const json = JSON.parse(content);
343
343
+
344
344
+
expect(json).toEqual({
345
345
+
lexicon: 1,
346
346
+
id: "com.atproto.sync.subscribeRepos",
347
347
+
defs: {
348
348
+
main: {
349
349
+
type: "subscription",
350
350
+
description: "Repository event stream",
351
351
+
parameters: {
352
352
+
type: "params",
353
353
+
properties: {
354
354
+
cursor: { type: "integer" },
355
355
+
},
356
356
+
},
357
357
+
message: {
358
358
+
schema: {
359
359
+
type: "union",
360
360
+
refs: ["#commit", "#identity", "#account"],
361
361
+
},
362
362
+
},
363
363
+
},
364
364
+
commit: {
365
365
+
type: "object",
366
366
+
properties: {
367
367
+
seq: { type: "integer", required: true },
368
368
+
rebase: { type: "boolean", required: true },
369
369
+
},
370
370
+
required: ["seq", "rebase"],
371
371
+
},
372
372
+
identity: {
373
373
+
type: "object",
374
374
+
properties: {
375
375
+
seq: { type: "integer", required: true },
376
376
+
did: { type: "string", format: "did", required: true },
377
377
+
},
378
378
+
required: ["seq", "did"],
379
379
+
},
380
380
+
account: {
381
381
+
type: "object",
382
382
+
properties: {
383
383
+
seq: { type: "integer", required: true },
384
384
+
active: { type: "boolean", required: true },
385
385
+
},
386
386
+
required: ["seq", "active"],
387
387
+
},
388
388
+
},
389
389
+
});
390
390
+
});
391
391
+
392
392
+
test("emits complex namespace with tokens, refs, and unions", async () => {
393
393
+
const lexiconFile = join(testDir, "complex.ts");
394
394
+
await writeFile(
395
395
+
lexiconFile,
396
396
+
`
397
397
+
import { lx } from "prototypey";
398
398
+
399
399
+
export const feedDefs = lx.namespace("app.bsky.feed.defs", {
400
400
+
postView: lx.object({
401
401
+
uri: lx.string({ required: true, format: "at-uri" }),
402
402
+
cid: lx.string({ required: true, format: "cid" }),
403
403
+
author: lx.ref("app.bsky.actor.defs#profileViewBasic", { required: true }),
404
404
+
embed: lx.union([
405
405
+
"app.bsky.embed.images#view",
406
406
+
"app.bsky.embed.video#view",
407
407
+
]),
408
408
+
likeCount: lx.integer({ minimum: 0 }),
409
409
+
}),
410
410
+
requestLess: lx.token("Request less content like this"),
411
411
+
requestMore: lx.token("Request more content like this"),
412
412
+
});
413
413
+
`,
414
414
+
);
415
415
+
416
416
+
await genEmit(outDir, lexiconFile);
417
417
+
418
418
+
const outputFile = join(outDir, "app.bsky.feed.defs.json");
419
419
+
const content = await readFile(outputFile, "utf-8");
420
420
+
const json = JSON.parse(content);
421
421
+
422
422
+
expect(json).toEqual({
423
423
+
lexicon: 1,
424
424
+
id: "app.bsky.feed.defs",
425
425
+
defs: {
426
426
+
postView: {
427
427
+
type: "object",
428
428
+
properties: {
429
429
+
uri: { type: "string", format: "at-uri", required: true },
430
430
+
cid: { type: "string", format: "cid", required: true },
431
431
+
author: {
432
432
+
type: "ref",
433
433
+
ref: "app.bsky.actor.defs#profileViewBasic",
434
434
+
required: true,
435
435
+
},
436
436
+
embed: {
437
437
+
type: "union",
438
438
+
refs: ["app.bsky.embed.images#view", "app.bsky.embed.video#view"],
439
439
+
},
440
440
+
likeCount: { type: "integer", minimum: 0 },
441
441
+
},
442
442
+
required: ["uri", "cid", "author"],
443
443
+
},
444
444
+
requestLess: {
445
445
+
type: "token",
446
446
+
description: "Request less content like this",
447
447
+
},
448
448
+
requestMore: {
449
449
+
type: "token",
450
450
+
description: "Request more content like this",
451
451
+
},
452
452
+
},
453
453
+
});
454
454
+
});
455
455
+
456
456
+
test("emits lexicon with arrays, blobs, and string formats", async () => {
457
457
+
const lexiconFile = join(testDir, "primitives.ts");
458
458
+
await writeFile(
459
459
+
lexiconFile,
460
460
+
`
461
461
+
import { lx } from "prototypey";
462
462
+
463
463
+
export const imagePost = lx.namespace("app.example.imagePost", {
464
464
+
main: lx.record({
465
465
+
key: "tid",
466
466
+
record: lx.object({
467
467
+
text: lx.string({ maxLength: 300, maxGraphemes: 300, required: true }),
468
468
+
createdAt: lx.string({ format: "datetime", required: true }),
469
469
+
images: lx.array(lx.blob({ accept: ["image/png", "image/jpeg"], maxSize: 1000000 }), { maxLength: 4 }),
470
470
+
tags: lx.array(lx.string({ maxLength: 64 })),
471
471
+
langs: lx.array(lx.string()),
472
472
+
}),
473
473
+
}),
474
474
+
});
475
475
+
`,
476
476
+
);
477
477
+
478
478
+
await genEmit(outDir, lexiconFile);
479
479
+
480
480
+
const outputFile = join(outDir, "app.example.imagePost.json");
481
481
+
const content = await readFile(outputFile, "utf-8");
482
482
+
const json = JSON.parse(content);
483
483
+
484
484
+
expect(json).toEqual({
485
485
+
lexicon: 1,
486
486
+
id: "app.example.imagePost",
487
487
+
defs: {
488
488
+
main: {
489
489
+
type: "record",
490
490
+
key: "tid",
491
491
+
record: {
492
492
+
type: "object",
493
493
+
properties: {
494
494
+
text: {
495
495
+
type: "string",
496
496
+
maxLength: 300,
497
497
+
maxGraphemes: 300,
498
498
+
required: true,
499
499
+
},
500
500
+
createdAt: { type: "string", format: "datetime", required: true },
501
501
+
images: {
502
502
+
type: "array",
503
503
+
items: {
504
504
+
type: "blob",
505
505
+
accept: ["image/png", "image/jpeg"],
506
506
+
maxSize: 1000000,
507
507
+
},
508
508
+
maxLength: 4,
509
509
+
},
510
510
+
tags: {
511
511
+
type: "array",
512
512
+
items: { type: "string", maxLength: 64 },
513
513
+
},
514
514
+
langs: {
515
515
+
type: "array",
516
516
+
items: { type: "string" },
517
517
+
},
518
518
+
},
519
519
+
required: ["text", "createdAt"],
520
520
+
},
521
521
+
},
522
522
+
},
523
523
+
});
524
524
+
});
525
525
+
});
+11
packages/cli/tests/fixtures/simple-lexicon.ts
···
1
1
+
import { lx } from "prototypey";
2
2
+
3
3
+
export const profileNamespace = lx.namespace("app.bsky.actor.profile", {
4
4
+
main: lx.record({
5
5
+
key: "self",
6
6
+
record: lx.object({
7
7
+
displayName: lx.string({ maxLength: 64, maxGraphemes: 64 }),
8
8
+
description: lx.string({ maxLength: 256, maxGraphemes: 256 }),
9
9
+
}),
10
10
+
}),
11
11
+
});
+7
packages/cli/vitest.config.ts
···
1
1
+
import { defineConfig } from "vitest/config";
2
2
+
3
3
+
export default defineConfig({
4
4
+
test: {
5
5
+
include: ["tests/**/*.test.ts"],
6
6
+
},
7
7
+
});
+3
pnpm-lock.yaml
···
42
42
typescript:
43
43
specifier: 5.8.3
44
44
version: 5.8.3
45
45
+
vitest:
46
46
+
specifier: ^3.2.4
47
47
+
version: 3.2.4(@types/node@24.0.4)(jiti@2.6.1)
45
48
46
49
packages/prototypey:
47
50
devDependencies: