@atproto/lex-cli 0.10.2 → 0.10.3

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