@atproto/lex-cli 0.5.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/dist/codegen/client.d.ts.map +1 -1
- package/dist/codegen/client.js +18 -50
- package/dist/codegen/client.js.map +1 -1
- package/dist/codegen/common.d.ts.map +1 -1
- package/dist/codegen/common.js +174 -26
- package/dist/codegen/common.js.map +1 -1
- package/dist/codegen/lex-gen.d.ts +4 -5
- package/dist/codegen/lex-gen.d.ts.map +1 -1
- package/dist/codegen/lex-gen.js +161 -49
- package/dist/codegen/lex-gen.js.map +1 -1
- package/dist/codegen/server.d.ts.map +1 -1
- package/dist/codegen/server.js +9 -45
- package/dist/codegen/server.js.map +1 -1
- package/dist/codegen/util.d.ts +1 -1
- package/dist/codegen/util.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/mdgen/index.js +3 -3
- package/dist/mdgen/index.js.map +1 -1
- package/dist/util.d.ts +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +16 -16
- package/dist/util.js.map +1 -1
- package/package.json +6 -3
- package/src/codegen/client.ts +27 -65
- package/src/codegen/common.ts +182 -27
- package/src/codegen/lex-gen.ts +219 -78
- package/src/codegen/server.ts +17 -60
- package/src/codegen/util.ts +1 -1
- package/src/index.ts +7 -7
- package/src/mdgen/index.ts +1 -1
- package/src/util.ts +5 -5
package/src/codegen/common.ts
CHANGED
|
@@ -1,28 +1,133 @@
|
|
|
1
|
+
import { Options as PrettierOptions, format } from 'prettier'
|
|
1
2
|
import { Project, SourceFile, VariableDeclarationKind } from 'ts-morph'
|
|
2
3
|
import { LexiconDoc } from '@atproto/lexicon'
|
|
3
|
-
import prettier from 'prettier'
|
|
4
4
|
import { GeneratedFile } from '../types'
|
|
5
5
|
|
|
6
|
-
const PRETTIER_OPTS = {
|
|
6
|
+
const PRETTIER_OPTS: PrettierOptions = {
|
|
7
7
|
parser: 'typescript',
|
|
8
8
|
tabWidth: 2,
|
|
9
9
|
semi: false,
|
|
10
10
|
singleQuote: true,
|
|
11
|
-
trailingComma: 'all'
|
|
11
|
+
trailingComma: 'all',
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export const utilTs = (project) =>
|
|
15
15
|
gen(project, '/util.ts', async (file) => {
|
|
16
16
|
file.replaceWithText(`
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
import { ValidationResult } from '@atproto/lexicon'
|
|
18
|
+
|
|
19
|
+
export type OmitKey<T, K extends keyof T> = {
|
|
20
|
+
[K2 in keyof T as K2 extends K ? never : K2]: T[K2]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type $Typed<V, T extends string = string> = V & { $type: T }
|
|
24
|
+
export type Un$Typed<V extends { $type?: string }> = OmitKey<V, '$type'>
|
|
25
|
+
|
|
26
|
+
export type $Type<Id extends string, Hash extends string> = Hash extends 'main'
|
|
27
|
+
? Id
|
|
28
|
+
: \`\${Id}#\${Hash}\`
|
|
29
|
+
|
|
30
|
+
function isObject<V>(v: V): v is V & object {
|
|
31
|
+
return v != null && typeof v === 'object'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function is$type<Id extends string, Hash extends string>(
|
|
35
|
+
$type: unknown,
|
|
36
|
+
id: Id,
|
|
37
|
+
hash: Hash,
|
|
38
|
+
): $type is $Type<Id, Hash> {
|
|
39
|
+
return hash === 'main'
|
|
40
|
+
? $type === id
|
|
41
|
+
: // $type === \`\${id}#\${hash}\`
|
|
42
|
+
typeof $type === 'string' &&
|
|
43
|
+
$type.length === id.length + 1 + hash.length &&
|
|
44
|
+
$type.charCodeAt(id.length) === 35 /* '#' */ &&
|
|
45
|
+
$type.startsWith(id) &&
|
|
46
|
+
$type.endsWith(hash)
|
|
47
|
+
}
|
|
48
|
+
${
|
|
49
|
+
/**
|
|
50
|
+
* The construct below allows to properly distinguish open unions. Consider
|
|
51
|
+
* the following example:
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* type Foo = { $type?: $Type<'foo', 'main'>; foo: string }
|
|
55
|
+
* type Bar = { $type?: $Type<'bar', 'main'>; bar: string }
|
|
56
|
+
* type OpenFooBarUnion = $Typed<Foo> | $Typed<Bar> | { $type: string }
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* In the context of lexicons, when there is a open union as shown above, the
|
|
60
|
+
* if `$type` if either `foo` or `bar`, then the object IS of type `Foo` or
|
|
61
|
+
* `Bar`.
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* declare const obj1: OpenFooBarUnion
|
|
65
|
+
* if (is$typed(obj1, 'foo', 'main')) {
|
|
66
|
+
* obj1.$type // $Type<'foo', 'main'>
|
|
67
|
+
* obj1.foo // string
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* Similarly, if an object is of type `unknown`, then the `is$typed` function
|
|
72
|
+
* should only return assurance about the `$type` property, which is what it
|
|
73
|
+
* actually checks:
|
|
74
|
+
*
|
|
75
|
+
* ```ts
|
|
76
|
+
* declare const obj2: unknown
|
|
77
|
+
* if (is$typed(obj2, 'foo', 'main')) {
|
|
78
|
+
* obj2.$type // $Type<'foo', 'main'>
|
|
79
|
+
* // @ts-expect-error
|
|
80
|
+
* obj2.foo
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* The construct bellow is what makes these two scenarios possible.
|
|
85
|
+
*/
|
|
86
|
+
''
|
|
19
87
|
}
|
|
88
|
+
export type $TypedObject<V, Id extends string, Hash extends string> = V extends {
|
|
89
|
+
$type: $Type<Id, Hash>
|
|
90
|
+
}
|
|
91
|
+
? V
|
|
92
|
+
: V extends { $type?: string }
|
|
93
|
+
? V extends { $type?: infer T extends $Type<Id, Hash> }
|
|
94
|
+
? V & { $type: T }
|
|
95
|
+
: never
|
|
96
|
+
: V & { $type: $Type<Id, Hash> }
|
|
97
|
+
|
|
98
|
+
export function is$typed<V, Id extends string, Hash extends string>(
|
|
99
|
+
v: V,
|
|
100
|
+
id: Id,
|
|
101
|
+
hash: Hash,
|
|
102
|
+
): v is $TypedObject<V, Id, Hash> {
|
|
103
|
+
return isObject(v) && '$type' in v && is$type(v.$type, id, hash)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function maybe$typed<V, Id extends string, Hash extends string>(
|
|
107
|
+
v: V,
|
|
108
|
+
id: Id,
|
|
109
|
+
hash: Hash,
|
|
110
|
+
): v is V & object & { $type?: $Type<Id, Hash> } {
|
|
111
|
+
return (
|
|
112
|
+
isObject(v) &&
|
|
113
|
+
('$type' in v
|
|
114
|
+
? v.$type === undefined || is$type(v.$type, id, hash)
|
|
115
|
+
: true)
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type Validator<R = unknown> = (v: unknown) => ValidationResult<R>
|
|
120
|
+
export type ValidatorParam<V extends Validator> =
|
|
121
|
+
V extends Validator<infer R> ? R : never
|
|
20
122
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Utility function that allows to convert a "validate*" utility function into a
|
|
125
|
+
* type predicate.
|
|
126
|
+
*/
|
|
127
|
+
export function asPredicate<V extends Validator>(validate: V) {
|
|
128
|
+
return function <T>(v: T): v is T & ValidatorParam<V> {
|
|
129
|
+
return validate(v).success
|
|
130
|
+
}
|
|
26
131
|
}
|
|
27
132
|
`)
|
|
28
133
|
})
|
|
@@ -41,7 +146,23 @@ export const lexiconsTs = (project, lexicons: LexiconDoc[]) =>
|
|
|
41
146
|
.addImportDeclaration({
|
|
42
147
|
moduleSpecifier: '@atproto/lexicon',
|
|
43
148
|
})
|
|
44
|
-
.addNamedImports([
|
|
149
|
+
.addNamedImports([
|
|
150
|
+
{ name: 'LexiconDoc' },
|
|
151
|
+
{ name: 'Lexicons' },
|
|
152
|
+
{ name: 'ValidationError' },
|
|
153
|
+
{ name: 'ValidationResult' },
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
//= import {is$typed, maybe$typed, $Typed} from './util'
|
|
157
|
+
file
|
|
158
|
+
.addImportDeclaration({
|
|
159
|
+
moduleSpecifier: './util.js',
|
|
160
|
+
})
|
|
161
|
+
.addNamedImports([
|
|
162
|
+
{ name: '$Typed' },
|
|
163
|
+
{ name: 'is$typed' },
|
|
164
|
+
{ name: 'maybe$typed' },
|
|
165
|
+
])
|
|
45
166
|
|
|
46
167
|
//= export const schemaDict = {...} as const satisfies Record<string, LexiconDoc>
|
|
47
168
|
file.addVariableStatement({
|
|
@@ -66,14 +187,14 @@ export const lexiconsTs = (project, lexicons: LexiconDoc[]) =>
|
|
|
66
187
|
],
|
|
67
188
|
})
|
|
68
189
|
|
|
69
|
-
//= export const schemas = Object.values(schemaDict)
|
|
190
|
+
//= export const schemas = Object.values(schemaDict) satisfies LexiconDoc[]
|
|
70
191
|
file.addVariableStatement({
|
|
71
192
|
isExported: true,
|
|
72
193
|
declarationKind: VariableDeclarationKind.Const,
|
|
73
194
|
declarations: [
|
|
74
195
|
{
|
|
75
196
|
name: 'schemas',
|
|
76
|
-
initializer: 'Object.values(schemaDict)',
|
|
197
|
+
initializer: 'Object.values(schemaDict) satisfies LexiconDoc[]',
|
|
77
198
|
},
|
|
78
199
|
],
|
|
79
200
|
})
|
|
@@ -91,6 +212,44 @@ export const lexiconsTs = (project, lexicons: LexiconDoc[]) =>
|
|
|
91
212
|
],
|
|
92
213
|
})
|
|
93
214
|
|
|
215
|
+
file.addFunction({
|
|
216
|
+
isExported: true,
|
|
217
|
+
name: 'validate',
|
|
218
|
+
overloads: [
|
|
219
|
+
{
|
|
220
|
+
typeParameters: ['T extends { $type: string }'],
|
|
221
|
+
parameters: [
|
|
222
|
+
{ name: 'v', type: 'unknown' },
|
|
223
|
+
{ name: 'id', type: 'string' },
|
|
224
|
+
{ name: 'hash', type: 'string' },
|
|
225
|
+
{ name: 'requiredType', type: 'true' },
|
|
226
|
+
],
|
|
227
|
+
returnType: 'ValidationResult<T>',
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
typeParameters: ['T extends { $type?: string }'],
|
|
231
|
+
parameters: [
|
|
232
|
+
{ name: 'v', type: 'unknown' },
|
|
233
|
+
{ name: 'id', type: 'string' },
|
|
234
|
+
{ name: 'hash', type: 'string' },
|
|
235
|
+
{ name: 'requiredType', type: 'false', hasQuestionToken: true },
|
|
236
|
+
],
|
|
237
|
+
returnType: 'ValidationResult<T>',
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
parameters: [
|
|
241
|
+
{ name: 'v', type: 'unknown' },
|
|
242
|
+
{ name: 'id', type: 'string' },
|
|
243
|
+
{ name: 'hash', type: 'string' },
|
|
244
|
+
{ name: 'requiredType', type: 'boolean', hasQuestionToken: true },
|
|
245
|
+
],
|
|
246
|
+
statements: [
|
|
247
|
+
// If $type is present, make sure it is valid before validating the rest of the object
|
|
248
|
+
'return (requiredType ? is$typed : maybe$typed)(v, id, hash) ? lexicons.validate(`${id}#${hash}`, v) : { success: false, error: new ValidationError(`Must be an object with "${hash === \'main\' ? id : `${id}#${hash}`}" $type property`) }',
|
|
249
|
+
],
|
|
250
|
+
returnType: 'ValidationResult',
|
|
251
|
+
})
|
|
252
|
+
|
|
94
253
|
//= export const ids = {...}
|
|
95
254
|
file.addVariableStatement({
|
|
96
255
|
isExported: true,
|
|
@@ -98,14 +257,11 @@ export const lexiconsTs = (project, lexicons: LexiconDoc[]) =>
|
|
|
98
257
|
declarations: [
|
|
99
258
|
{
|
|
100
259
|
name: 'ids',
|
|
101
|
-
initializer:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
}, {}),
|
|
108
|
-
),
|
|
260
|
+
initializer: `{${lexicons
|
|
261
|
+
.map(
|
|
262
|
+
(lex) => `\n ${nsidToEnum(lex.id)}: ${JSON.stringify(lex.id)},`,
|
|
263
|
+
)
|
|
264
|
+
.join('')}\n} as const`,
|
|
109
265
|
},
|
|
110
266
|
],
|
|
111
267
|
})
|
|
@@ -118,12 +274,11 @@ export async function gen(
|
|
|
118
274
|
): Promise<GeneratedFile> {
|
|
119
275
|
const file = project.createSourceFile(path)
|
|
120
276
|
await gen(file)
|
|
121
|
-
file.
|
|
122
|
-
const src =
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
277
|
+
await file.save() // Save in the "in memory" file system
|
|
278
|
+
const src = `${banner()}${file.getFullText()}`
|
|
279
|
+
const content = await format(src, PRETTIER_OPTS)
|
|
280
|
+
|
|
281
|
+
return { path, content }
|
|
127
282
|
}
|
|
128
283
|
|
|
129
284
|
function banner() {
|