@datasworn-community/build-tools 0.2.1 → 0.2.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.
- package/package.json +6 -3
- package/schema-source/builders/IdBuilder.ts +849 -0
- package/schema-source/schema/common/Id.ts +3 -1
- package/schema-source/schema/delve/common.ts +3 -1
- package/schema-source/schema/generic/CollectableNode.ts +3 -1
- package/schema-source/schema/generic/CollectionNode.ts +3 -1
- package/schema-source/schema/generic/EmbedOnlyNode.ts +3 -1
- package/schema-source/schema/generic/NonCollectableNode.ts +3 -1
- package/schema-source/schema/generic/PrimaryTypeNode.ts +3 -1
- package/schema-source/schema/rules/TagRule.ts +3 -1
- package/schema-source/schema/utils/EmbeddedNode.ts +3 -1
- package/schema-source/schema/utils/string.ts +2 -2
- package/schema-source/scripts/augmentations.ts +70 -0
- package/schema-source/scripts/codegen/schemaToTsDeclaration.ts +290 -0
- package/schema-source/scripts/codegen/tsDocTags.ts +75 -0
- package/schema-source/scripts/codegen/writeTypescriptTypes.ts +35 -0
- package/schema-source/scripts/const.ts +40 -2
- package/schema-source/scripts/datasworn/sort.ts +80 -0
- package/schema-source/scripts/json-typedef/typedef.ts +1138 -0
- package/schema-source/scripts/json-typedef/utils.ts +349 -0
- package/schema-source/scripts/schema/buildSchema.ts +109 -0
- package/schema-source/scripts/utils/Log.ts +15 -0
- package/schema-source/scripts/utils/readWrite.ts +180 -0
- package/schema-source/scripts/validation/ajv.ts +29 -0
- package/schema-source/scripts/validation/formats.ts +6 -0
- package/schema-source/scripts/validation/keywords.ts +36 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Hint,
|
|
3
|
+
KindGuard,
|
|
4
|
+
type TAnySchema,
|
|
5
|
+
type TArray,
|
|
6
|
+
type TBoolean,
|
|
7
|
+
type TInteger,
|
|
8
|
+
type TLiteral,
|
|
9
|
+
type TNull,
|
|
10
|
+
type TNumber,
|
|
11
|
+
type TObject,
|
|
12
|
+
type TOptional,
|
|
13
|
+
type TRecord,
|
|
14
|
+
type TRef,
|
|
15
|
+
type TSchema,
|
|
16
|
+
type TString,
|
|
17
|
+
type TThis,
|
|
18
|
+
Type,
|
|
19
|
+
TypeGuard
|
|
20
|
+
} from '@sinclair/typebox'
|
|
21
|
+
import type { JTDSchemaType } from 'ajv/dist/core.js'
|
|
22
|
+
import type * as JTD from 'jtd'
|
|
23
|
+
import {
|
|
24
|
+
cloneDeep,
|
|
25
|
+
isInteger,
|
|
26
|
+
isString,
|
|
27
|
+
isUndefined,
|
|
28
|
+
mapValues,
|
|
29
|
+
merge,
|
|
30
|
+
omit,
|
|
31
|
+
omitBy,
|
|
32
|
+
pick,
|
|
33
|
+
set
|
|
34
|
+
} from 'lodash-es'
|
|
35
|
+
import * as Generic from '../../schema/Generic.js'
|
|
36
|
+
import type { TRoot } from '../../schema/root/Root.js'
|
|
37
|
+
import { Discriminator, JsonTypeDef, Mapping } from '../../schema/Symbols.js'
|
|
38
|
+
import * as Utils from '../../schema/Utils.js'
|
|
39
|
+
import { DefsKey, rootSchemaName } from '../const.js'
|
|
40
|
+
import Log from '../utils/Log.js'
|
|
41
|
+
import JtdType, {
|
|
42
|
+
type Metadata,
|
|
43
|
+
type TFields,
|
|
44
|
+
type TStruct
|
|
45
|
+
} from './typedef.js'
|
|
46
|
+
|
|
47
|
+
/** Extract metadata from a JSON schema for use in a JTD schema's `metadata` property */
|
|
48
|
+
export function extractMetadata<T extends TAnySchema>(jsonSchema: T) {
|
|
49
|
+
const metadataKeys = [
|
|
50
|
+
// general
|
|
51
|
+
'description',
|
|
52
|
+
'examples',
|
|
53
|
+
// string
|
|
54
|
+
'pattern',
|
|
55
|
+
'format',
|
|
56
|
+
// numeric
|
|
57
|
+
'exclusiveMaximum',
|
|
58
|
+
'exclusiveMinimum',
|
|
59
|
+
'maximum',
|
|
60
|
+
'minimum',
|
|
61
|
+
'multipleOf',
|
|
62
|
+
// array
|
|
63
|
+
'uniqueItems',
|
|
64
|
+
'minItems',
|
|
65
|
+
'maxItems'
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
let metadata = pick(cloneDeep(jsonSchema), ...metadataKeys) as Metadata
|
|
69
|
+
|
|
70
|
+
if (jsonSchema[JsonTypeDef]?.metadata)
|
|
71
|
+
metadata = merge(metadata, omit(jsonSchema[JsonTypeDef].metadata))
|
|
72
|
+
|
|
73
|
+
// @ts-expect-error
|
|
74
|
+
if (jsonSchema[Utils.EnumDescription]) {
|
|
75
|
+
// console.log(jsonSchema)
|
|
76
|
+
// @ts-expect-error
|
|
77
|
+
metadata.enumDescription = jsonSchema[Utils.EnumDescription]
|
|
78
|
+
// @ts-expect-error
|
|
79
|
+
metadata.description = jsonSchema[Utils.Description]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (Object.keys(metadata)?.length === 0) return undefined
|
|
83
|
+
|
|
84
|
+
return metadata
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function setIdRef<T extends { _id: string }, R extends string>(
|
|
88
|
+
schema: JTDSchemaType<T>,
|
|
89
|
+
ref: R
|
|
90
|
+
) {
|
|
91
|
+
type NewRef = { [P in R]: string }
|
|
92
|
+
type RefRecord =
|
|
93
|
+
typeof schema extends JTDSchemaType<T, infer U> ? U & NewRef : NewRef
|
|
94
|
+
return set(schema, 'properties._id.ref', ref) as unknown as JTDSchemaType<
|
|
95
|
+
T & { _id: R },
|
|
96
|
+
RefRecord
|
|
97
|
+
>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function toJtdEnum<
|
|
101
|
+
U extends string[] | number[],
|
|
102
|
+
T extends Utils.TUnionEnum<U>
|
|
103
|
+
>(schema: T) {
|
|
104
|
+
if (schema.enum.every(isString)) return JtdType.Enum(schema.enum)
|
|
105
|
+
if (schema.enum.every(isInteger)) return JtdType.Uint8()
|
|
106
|
+
|
|
107
|
+
throw new Error(`Unable to infer schema for enum: ${JSON.stringify(schema)}`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Identifiers of all JTD schemas that are referenced */
|
|
111
|
+
export const refTracker = new Set<string>()
|
|
112
|
+
|
|
113
|
+
export function toJtdRef(schema: TRef | TThis): JTD.SchemaFormRef {
|
|
114
|
+
const ref = schema.$ref.replace(`#/${DefsKey}/`, '')
|
|
115
|
+
|
|
116
|
+
refTracker.add(ref)
|
|
117
|
+
|
|
118
|
+
return JtdType.Ref(ref)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Transforms a Typebox array schema into JTD elements */
|
|
122
|
+
export function toJtdElements<U extends TSchema, T extends TArray<U>>(
|
|
123
|
+
schema: T
|
|
124
|
+
) {
|
|
125
|
+
const { items } = schema
|
|
126
|
+
return JtdType.Array(toJtdForm(items as unknown as TConvertible) as TSchema)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Transform a Typebox object schema into JTD properties */
|
|
130
|
+
export function toJtdProperties<T extends TObject>(schema: T) {
|
|
131
|
+
const fields = omitBy(
|
|
132
|
+
mapValues(schema.properties, (subschema) => {
|
|
133
|
+
if (Utils.TNullable(subschema))
|
|
134
|
+
return JtdType.Nullable(toJtdForm(subschema.anyOf[0] as any) as TSchema)
|
|
135
|
+
|
|
136
|
+
const base = toJtdForm(subschema as any) as TSchema
|
|
137
|
+
|
|
138
|
+
if (TypeGuard.IsOptional(subschema)) return Type.Optional(base)
|
|
139
|
+
|
|
140
|
+
return base
|
|
141
|
+
}),
|
|
142
|
+
isUndefined
|
|
143
|
+
) as TFields
|
|
144
|
+
|
|
145
|
+
const result = JtdType.Struct(fields)
|
|
146
|
+
|
|
147
|
+
return result
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Transform a Typebox record schema into JTD values */
|
|
151
|
+
export function toJtdValues<T extends TRecord<TString, U>, U extends TSchema>(
|
|
152
|
+
schema: T
|
|
153
|
+
) {
|
|
154
|
+
const [propertyPattern, value] = Object.entries(schema.patternProperties)[0]
|
|
155
|
+
const unwrap = toJtdForm(value as unknown as TConvertible) as TSchema
|
|
156
|
+
|
|
157
|
+
if (!unwrap)
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Couldn't unwrap Record value schema: ${JSON.stringify(schema)}`
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return JtdType.Record(unwrap, { propertyPattern })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function toJtdSingleEnum(schema: TLiteral<string>) {
|
|
166
|
+
if (typeof schema.const === 'number')
|
|
167
|
+
throw new Error(`Got a number literal from ${schema.$id}`)
|
|
168
|
+
|
|
169
|
+
return JtdType.Enum([schema.const])
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function toJtdDiscriminator<
|
|
173
|
+
M extends Utils.TDiscriminatorMap<Utils.TDiscriminableish>,
|
|
174
|
+
D extends string
|
|
175
|
+
>(schema: Utils.TDiscriminatedUnion<M, D>) {
|
|
176
|
+
const discriminator = schema[Discriminator] as string
|
|
177
|
+
|
|
178
|
+
// console.log(schema.$id)
|
|
179
|
+
// console.log(schema[Mapping])
|
|
180
|
+
|
|
181
|
+
const oldMapping = schema[Mapping] as Record<string, TObject>
|
|
182
|
+
const jtdMapping: Record<string, TStruct<TFields>> = {}
|
|
183
|
+
|
|
184
|
+
for (const k in oldMapping)
|
|
185
|
+
if (Object.hasOwn(oldMapping, k)) {
|
|
186
|
+
const subschema = oldMapping[k] as TObject
|
|
187
|
+
jtdMapping[k] = {
|
|
188
|
+
...omit(toJtdProperties(subschema), `properties.${discriminator}`),
|
|
189
|
+
metadata: extractMetadata(subschema as TAnySchema)
|
|
190
|
+
} as unknown as TStruct<TFields>
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// const mapping = Object.fromEntries(
|
|
194
|
+
// mapValues(oldMapping, (subschema) => [
|
|
195
|
+
// subschema.properties[discriminator].const,
|
|
196
|
+
// {
|
|
197
|
+
// ...omit(toJtdProperties(subschema), `properties.${discriminator}`),
|
|
198
|
+
// metadata: extractMetadata(subschema)
|
|
199
|
+
// }
|
|
200
|
+
// ])
|
|
201
|
+
// )
|
|
202
|
+
|
|
203
|
+
// console.log(discriminator, jtdMapping)
|
|
204
|
+
return JtdType.Union(jtdMapping, discriminator)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
type TConvertible =
|
|
208
|
+
| TLiteral<string>
|
|
209
|
+
| TString
|
|
210
|
+
| TBoolean
|
|
211
|
+
| TInteger
|
|
212
|
+
| TObject
|
|
213
|
+
| TNumber
|
|
214
|
+
| Utils.TUnionEnum
|
|
215
|
+
| TArray
|
|
216
|
+
|
|
217
|
+
function toJtdForm(
|
|
218
|
+
schema: TConvertible | Utils.TNullable<TConvertible> | TOptional<TConvertible>
|
|
219
|
+
): JTD.Schema
|
|
220
|
+
function toJtdForm(schema: TNull): null
|
|
221
|
+
function toJtdForm(schema: TSchema): JTD.Schema | null {
|
|
222
|
+
const metadata = extractMetadata(schema)
|
|
223
|
+
|
|
224
|
+
if (schema[JsonTypeDef]?.schema != null)
|
|
225
|
+
return merge(cloneDeep(schema[JsonTypeDef].schema), {
|
|
226
|
+
metadata
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
let result: JTD.Schema
|
|
230
|
+
|
|
231
|
+
switch (true) {
|
|
232
|
+
case schema[JsonTypeDef]?.skip:
|
|
233
|
+
case KindGuard.IsNull(schema):
|
|
234
|
+
return null
|
|
235
|
+
case Utils.TUnionEnum(schema):
|
|
236
|
+
result = toJtdEnum(schema)
|
|
237
|
+
break
|
|
238
|
+
case KindGuard.IsAny(schema):
|
|
239
|
+
result = JtdType.Any()
|
|
240
|
+
break
|
|
241
|
+
case KindGuard.IsLiteralString(schema):
|
|
242
|
+
result = toJtdSingleEnum(schema as TLiteral<string>)
|
|
243
|
+
break
|
|
244
|
+
case KindGuard.IsString(schema):
|
|
245
|
+
result = JtdType.String()
|
|
246
|
+
break
|
|
247
|
+
case KindGuard.IsLiteralBoolean(schema):
|
|
248
|
+
case KindGuard.IsBoolean(schema):
|
|
249
|
+
result = JtdType.Boolean()
|
|
250
|
+
break
|
|
251
|
+
case KindGuard.IsLiteralNumber(schema):
|
|
252
|
+
case KindGuard.IsInteger(schema):
|
|
253
|
+
result = JtdType.Int16()
|
|
254
|
+
break
|
|
255
|
+
case KindGuard.IsNumber(schema):
|
|
256
|
+
Log.warn(
|
|
257
|
+
'Received a number schema. Consider making it an integer instead.',
|
|
258
|
+
schema
|
|
259
|
+
)
|
|
260
|
+
result = JtdType.Float32()
|
|
261
|
+
break
|
|
262
|
+
case KindGuard.IsThis(schema):
|
|
263
|
+
case KindGuard.IsRef(schema):
|
|
264
|
+
result = toJtdRef(schema as TThis | TRef)
|
|
265
|
+
break
|
|
266
|
+
case KindGuard.IsRecord(schema):
|
|
267
|
+
case Generic.DictionaryBrand in schema:
|
|
268
|
+
result = toJtdValues(schema as TRecord) as JTD.Schema
|
|
269
|
+
break
|
|
270
|
+
case KindGuard.IsArray(schema):
|
|
271
|
+
result = toJtdElements(schema as TArray) as JTD.Schema
|
|
272
|
+
break
|
|
273
|
+
case KindGuard.IsObject(schema):
|
|
274
|
+
result = toJtdProperties(schema as TObject)
|
|
275
|
+
break
|
|
276
|
+
case Utils.TDiscriminatedUnion(schema):
|
|
277
|
+
result = toJtdDiscriminator(
|
|
278
|
+
schema as Utils.TDiscriminatedUnion
|
|
279
|
+
) as JTD.Schema
|
|
280
|
+
break
|
|
281
|
+
case KindGuard.IsUnion(schema) && schema[Hint] === 'Enum':
|
|
282
|
+
result = JtdType.Enum(
|
|
283
|
+
(schema.anyOf as TLiteral[]).map((item) => item.const as string),
|
|
284
|
+
{
|
|
285
|
+
enumDescription: Object.fromEntries(
|
|
286
|
+
(schema.anyOf as TLiteral[]).map((item) => [
|
|
287
|
+
item.const,
|
|
288
|
+
item.description ?? ''
|
|
289
|
+
])
|
|
290
|
+
) as Record<string, string>
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
break
|
|
294
|
+
default:
|
|
295
|
+
console.log(schema)
|
|
296
|
+
throw new Error('No JTD transform available for JSON schema')
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
result = merge(result, { metadata: extractMetadata(schema) })
|
|
300
|
+
|
|
301
|
+
// if (result[Kind] === 'TypeDef:Enum' && !result.metadata?.typescriptType) {
|
|
302
|
+
// result.metadata ||= {}
|
|
303
|
+
// result.metadata.typescriptType = result.enum
|
|
304
|
+
// .map((item: string | number) => (isInteger(item) ? item : `'${item}'`))
|
|
305
|
+
// .join(' | ')
|
|
306
|
+
// }
|
|
307
|
+
|
|
308
|
+
return result as any
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function toJtdRoot<T extends TRoot>(schemaRoot: T) {
|
|
312
|
+
const defs = {} as Record<string, JTD.Schema | undefined>
|
|
313
|
+
|
|
314
|
+
for (const k in schemaRoot[DefsKey]) {
|
|
315
|
+
if (k === rootSchemaName) continue
|
|
316
|
+
try {
|
|
317
|
+
defs[k] = toJtdForm(schemaRoot[DefsKey][k] as TConvertible)
|
|
318
|
+
} catch (err: any) {
|
|
319
|
+
console.log(`${k}:`, schemaRoot[DefsKey][k])
|
|
320
|
+
|
|
321
|
+
throw new Error(`Couldn't convert ${k}. ${err?.message ?? err}`)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const base = toJtdForm(schemaRoot[DefsKey][rootSchemaName] as TConvertible)
|
|
326
|
+
|
|
327
|
+
if (typeof base === 'undefined')
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Unable to infer JSON Typedef form for root schema "${rootSchemaName}".`
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
...(base as object),
|
|
334
|
+
definitions: omitBy(defs, (v, k) => {
|
|
335
|
+
if (v === null) {
|
|
336
|
+
Log.info(`Skipping JTD for "${k}"`)
|
|
337
|
+
return true
|
|
338
|
+
}
|
|
339
|
+
// if (!refTracker.has(k)) {
|
|
340
|
+
// Log.info(`Orphaned definition "${k}" -- skipping`)
|
|
341
|
+
// return true
|
|
342
|
+
// }
|
|
343
|
+
|
|
344
|
+
return false
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export { toJtdForm }
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regenerates the schemas and write them to file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import type { JsonSchema } from 'json-schema-library'
|
|
7
|
+
import { kebabCase } from 'lodash-es'
|
|
8
|
+
import * as Schema from '../../schema/index.js'
|
|
9
|
+
import type { TRoot } from '../../schema/root/Root.js'
|
|
10
|
+
import {
|
|
11
|
+
DefsKey,
|
|
12
|
+
DIR_HISTORY_CURRENT,
|
|
13
|
+
SCHEMA_NAME,
|
|
14
|
+
SCHEMA_PATH,
|
|
15
|
+
SOURCE_SCHEMA_NAME,
|
|
16
|
+
SOURCE_SCHEMA_PATH,
|
|
17
|
+
SOURCEDATA_SCHEMA_PATH
|
|
18
|
+
} from '../const.js'
|
|
19
|
+
import { sortSchemaKeys } from '../datasworn/sort.js'
|
|
20
|
+
import Log from '../utils/Log.js'
|
|
21
|
+
import { writeJSON } from '../utils/readWrite.js'
|
|
22
|
+
import AJV from '../validation/ajv.js'
|
|
23
|
+
|
|
24
|
+
interface SchemaOptions {
|
|
25
|
+
name: string
|
|
26
|
+
rootSchema: TRoot
|
|
27
|
+
paths: string[]
|
|
28
|
+
messages: {
|
|
29
|
+
writeStart: string
|
|
30
|
+
writeFinish: string
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const schemaOptions: SchemaOptions[] = [
|
|
35
|
+
{
|
|
36
|
+
name: SCHEMA_NAME,
|
|
37
|
+
rootSchema: Schema.DataswornSchema,
|
|
38
|
+
paths: [
|
|
39
|
+
SCHEMA_PATH,
|
|
40
|
+
path.join(DIR_HISTORY_CURRENT, `${kebabCase(SCHEMA_NAME)}.schema.json`)
|
|
41
|
+
],
|
|
42
|
+
messages: {
|
|
43
|
+
writeStart: '✏️ Writing schema for Datasworn',
|
|
44
|
+
writeFinish: '✅ Finished writing schema for Datasworn'
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: SOURCE_SCHEMA_NAME,
|
|
49
|
+
rootSchema: Schema.DataswornSourceSchema,
|
|
50
|
+
paths: [
|
|
51
|
+
SOURCEDATA_SCHEMA_PATH,
|
|
52
|
+
SOURCE_SCHEMA_PATH,
|
|
53
|
+
path.join(
|
|
54
|
+
DIR_HISTORY_CURRENT,
|
|
55
|
+
`${kebabCase(SOURCE_SCHEMA_NAME)}.schema.json`
|
|
56
|
+
)
|
|
57
|
+
],
|
|
58
|
+
messages: {
|
|
59
|
+
writeStart: '✏️ Writing schema for DataswornSource',
|
|
60
|
+
writeFinish: '✅ Finished writing schema for DataswornSource'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const metadataKeys: string[] = []
|
|
66
|
+
|
|
67
|
+
function replacer(k: string, v: unknown) {
|
|
68
|
+
if (metadataKeys.includes(k)) return undefined
|
|
69
|
+
|
|
70
|
+
if (k === '$id' && typeof v === 'string' && !v.startsWith('http'))
|
|
71
|
+
// omit $ids that aren't the root URI, they're redundant and only there for TypeBox
|
|
72
|
+
return undefined
|
|
73
|
+
if (
|
|
74
|
+
k === '$ref' &&
|
|
75
|
+
typeof v === 'string' &&
|
|
76
|
+
!v.startsWith('http') &&
|
|
77
|
+
!v.startsWith(`#/${DefsKey}/`)
|
|
78
|
+
)
|
|
79
|
+
// adjust references for use with standard json validation
|
|
80
|
+
return `#/${DefsKey}/${v}`
|
|
81
|
+
|
|
82
|
+
return v
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Pending operations to write the schema to disk */
|
|
86
|
+
const writeOps: Promise<unknown>[] = []
|
|
87
|
+
|
|
88
|
+
for (const { rootSchema, name, paths, messages } of schemaOptions) {
|
|
89
|
+
AJV.addSchema(rootSchema as JsonSchema, name)
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
Log.info(messages.writeStart)
|
|
93
|
+
|
|
94
|
+
// Recursively sort all keys in the schema
|
|
95
|
+
const sortedSchema = sortSchemaKeys(JSON.parse(JSON.stringify(rootSchema)))
|
|
96
|
+
|
|
97
|
+
writeOps.push(
|
|
98
|
+
writeJSON(paths, sortedSchema, {
|
|
99
|
+
replacer
|
|
100
|
+
}).then(() => Log.info(messages.writeFinish))
|
|
101
|
+
)
|
|
102
|
+
} catch (error) {
|
|
103
|
+
Log.error(error)
|
|
104
|
+
|
|
105
|
+
await writeJSON(paths, rootSchema, { replacer })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await Promise.all(writeOps)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal console-based logger, standing in for upstream's winston/logform
|
|
3
|
+
* setup so generation doesn't pull in a logging framework.
|
|
4
|
+
*/
|
|
5
|
+
const Log = {
|
|
6
|
+
// `log` (rather than `info`) matches ajv's `Logger` interface, which this
|
|
7
|
+
// object is passed to directly.
|
|
8
|
+
log: (...args: unknown[]) => console.info(...args),
|
|
9
|
+
info: (...args: unknown[]) => console.info(...args),
|
|
10
|
+
warn: (...args: unknown[]) => console.warn(...args),
|
|
11
|
+
error: (...args: unknown[]) => console.error(...args),
|
|
12
|
+
verbose: (...args: unknown[]) => console.debug(...args)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default Log
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { type BunFile } from 'bun'
|
|
4
|
+
import type { Simplify } from 'type-fest'
|
|
5
|
+
import yaml from 'yaml'
|
|
6
|
+
import type { DataswornSource } from '@datasworn-community/core'
|
|
7
|
+
|
|
8
|
+
const space = '\t'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Reads source data from a YAML or JSON file.
|
|
12
|
+
* @return The deserialized object.
|
|
13
|
+
*/
|
|
14
|
+
export async function readDataswornSourceData<
|
|
15
|
+
T extends DataswornSource.RulesPackage
|
|
16
|
+
>(
|
|
17
|
+
filePath: string | BunFile,
|
|
18
|
+
reviver?: (key: unknown, value: unknown) => unknown
|
|
19
|
+
): Promise<T> {
|
|
20
|
+
const file = typeof filePath === 'string' ? Bun.file(filePath) : filePath
|
|
21
|
+
|
|
22
|
+
const pathStr =
|
|
23
|
+
typeof filePath === 'string' ? filePath : (filePath.name as string)
|
|
24
|
+
switch (path.extname(pathStr)) {
|
|
25
|
+
case '.yaml':
|
|
26
|
+
case '.yml':
|
|
27
|
+
return await readYAML<T>(file, { reviver })
|
|
28
|
+
case '.json':
|
|
29
|
+
return await readJSON<T>(file, reviver)
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(`Unrecognized file extension in "${filePath}"`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function readYAML<T>(
|
|
35
|
+
filePath: string | BunFile,
|
|
36
|
+
options: YAMLOptions = {
|
|
37
|
+
// ensures that dates are serialized as strings rather than Date objects (which prevents AJV from validating them)
|
|
38
|
+
schema: 'core',
|
|
39
|
+
merge: true,
|
|
40
|
+
maxAliasCount: 1000,
|
|
41
|
+
logLevel: 'debug'
|
|
42
|
+
}
|
|
43
|
+
): Promise<T> {
|
|
44
|
+
const file = typeof filePath === 'string' ? Bun.file(filePath) : filePath
|
|
45
|
+
|
|
46
|
+
const str = await file.text()
|
|
47
|
+
return yaml.parse(str, options)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function ensureDir(dirPath: string) {
|
|
51
|
+
return await fs.mkdir(dirPath, { recursive: true })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function copyDir(from: string, to: string) {
|
|
55
|
+
return await fs.cp(from, to, { recursive: true })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function copyFile(from: string | BunFile, to: string | BunFile) {
|
|
59
|
+
const fromFile = typeof from === 'string' ? Bun.file(from) : from
|
|
60
|
+
const toFile = typeof to === 'string' ? Bun.file(to) : to
|
|
61
|
+
|
|
62
|
+
const op = await Bun.write(toFile, fromFile, { createPath: true })
|
|
63
|
+
|
|
64
|
+
return op
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function emptyDir(dirPath: string) {
|
|
68
|
+
try {
|
|
69
|
+
await fs.rmdir(dirPath)
|
|
70
|
+
} catch {
|
|
71
|
+
// Directory might not exist, ignore
|
|
72
|
+
}
|
|
73
|
+
return await ensureDir(dirPath)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function readJSON<T>(
|
|
77
|
+
filePath: string | BunFile,
|
|
78
|
+
reviver?: (this: any, key: string, value: any) => any
|
|
79
|
+
): Promise<T> {
|
|
80
|
+
const file =
|
|
81
|
+
typeof filePath === 'string'
|
|
82
|
+
? Bun.file(filePath.toString(), {
|
|
83
|
+
type: 'application/json'
|
|
84
|
+
})
|
|
85
|
+
: filePath
|
|
86
|
+
|
|
87
|
+
if (reviver == null) return await file.json()
|
|
88
|
+
|
|
89
|
+
return JSON.parse(await file.text(), reviver)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type WriteJsonOptions = {
|
|
93
|
+
skipCopyAwait?: boolean
|
|
94
|
+
replacer?: (this: any, key: string, value: any) => any
|
|
95
|
+
}
|
|
96
|
+
export async function writeJSON(
|
|
97
|
+
filePath: string | BunFile,
|
|
98
|
+
object: any,
|
|
99
|
+
options?: WriteJsonOptions
|
|
100
|
+
): Promise<string>
|
|
101
|
+
export async function writeJSON(
|
|
102
|
+
filePaths: (string | BunFile)[],
|
|
103
|
+
object: any,
|
|
104
|
+
options?: WriteJsonOptions
|
|
105
|
+
): Promise<any>
|
|
106
|
+
export async function writeJSON(
|
|
107
|
+
filePath: string | BunFile | (string | BunFile)[],
|
|
108
|
+
object: any,
|
|
109
|
+
{ skipCopyAwait = false, replacer }: WriteJsonOptions = {}
|
|
110
|
+
): Promise<any> {
|
|
111
|
+
if (typeof object === 'string') throw new Error()
|
|
112
|
+
|
|
113
|
+
const allPaths = Array.isArray(filePath) ? filePath : [filePath]
|
|
114
|
+
// dedupe identical destinations (callers may pass the same path twice when
|
|
115
|
+
// two conceptually distinct outputs happen to coincide); otherwise the file
|
|
116
|
+
// gets copied onto itself, which can race with its own write and truncate it
|
|
117
|
+
const seen = new Set<string>()
|
|
118
|
+
const pathParams = allPaths.filter((destination) => {
|
|
119
|
+
const key = typeof destination === 'string' ? destination : destination.name
|
|
120
|
+
if (key != null && seen.has(key)) return false
|
|
121
|
+
if (key != null) seen.add(key)
|
|
122
|
+
return true
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
if (pathParams.length === 0)
|
|
126
|
+
// nothing to do
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
const [writeDestination, ...copyDestinations]: BunFile[] = pathParams.map(
|
|
130
|
+
(destination) =>
|
|
131
|
+
typeof destination === 'string' ? Bun.file(destination) : destination
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const json = JSON.stringify(object, replacer, space)
|
|
135
|
+
|
|
136
|
+
// write to the first destination
|
|
137
|
+
await Bun.write(writeDestination, json, { createPath: true })
|
|
138
|
+
|
|
139
|
+
if (copyDestinations.length === 0)
|
|
140
|
+
// nothing left to do
|
|
141
|
+
return json
|
|
142
|
+
|
|
143
|
+
const copyOps: Promise<any>[] = []
|
|
144
|
+
|
|
145
|
+
for (const copyDestionation of copyDestinations)
|
|
146
|
+
copyOps.push(copyFile(writeDestination, copyDestionation))
|
|
147
|
+
|
|
148
|
+
if (skipCopyAwait) {
|
|
149
|
+
void Promise.all(copyOps)
|
|
150
|
+
} else {
|
|
151
|
+
await Promise.all(copyOps)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return json
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type YAMLOptions = Simplify<
|
|
158
|
+
yaml.ParseOptions &
|
|
159
|
+
yaml.DocumentOptions &
|
|
160
|
+
yaml.SchemaOptions &
|
|
161
|
+
yaml.ToJSOptions
|
|
162
|
+
>
|
|
163
|
+
|
|
164
|
+
export async function writeCode(filePath: string | BunFile, content: string) {
|
|
165
|
+
const file = typeof filePath === 'string' ? Bun.file(filePath) : filePath
|
|
166
|
+
|
|
167
|
+
await Bun.write(file, content, { createPath: true })
|
|
168
|
+
}
|
|
169
|
+
export async function updateJSON<T>(
|
|
170
|
+
path: string | BunFile,
|
|
171
|
+
update: Partial<T> | ((data: T) => T)
|
|
172
|
+
) {
|
|
173
|
+
let json = (await readJSON(path)) as T
|
|
174
|
+
|
|
175
|
+
// @ts-expect-error
|
|
176
|
+
if (typeof update !== 'function') Object.assign(json, update)
|
|
177
|
+
else json = update(json)
|
|
178
|
+
|
|
179
|
+
await writeJSON(path, json)
|
|
180
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import ajvPkg from 'ajv'
|
|
2
|
+
import ajvFormatPkg from 'ajv-formats'
|
|
3
|
+
import Log from '../utils/Log.js'
|
|
4
|
+
import { FORMATS } from './formats.js'
|
|
5
|
+
import { KEYWORDS } from './keywords.js'
|
|
6
|
+
|
|
7
|
+
// workaround for https://github.com/ajv-validator/ajv/issues/2132
|
|
8
|
+
const Ajv = ajvPkg.default
|
|
9
|
+
const addFormats = ajvFormatPkg.default
|
|
10
|
+
|
|
11
|
+
const AJV = new Ajv({
|
|
12
|
+
logger: Log,
|
|
13
|
+
strict: 'log',
|
|
14
|
+
strictSchema: 'log',
|
|
15
|
+
strictTypes: 'log',
|
|
16
|
+
useDefaults: 'empty',
|
|
17
|
+
validateFormats: true,
|
|
18
|
+
verbose: true
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
for (const format in FORMATS) AJV.addFormat(format, FORMATS[format])
|
|
22
|
+
|
|
23
|
+
for (const keyword in KEYWORDS)
|
|
24
|
+
AJV.addKeyword({ keyword, ...KEYWORDS[keyword] })
|
|
25
|
+
|
|
26
|
+
// Type assertion needed due to ajv-formats bundling its own ajv types
|
|
27
|
+
addFormats(AJV as unknown as Parameters<typeof addFormats>[0])
|
|
28
|
+
|
|
29
|
+
export default AJV
|