@datasworn-community/build-tools 0.2.2 → 0.2.4

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.
Files changed (47) hide show
  1. package/dist/cli/datasworn-build.js +12 -2
  2. package/dist/cli/datasworn-build.js.map +1 -1
  3. package/dist/config.d.ts +3 -1
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/content-package-builder.d.ts +54 -0
  7. package/dist/content-package-builder.d.ts.map +1 -0
  8. package/dist/content-package-builder.js +159 -0
  9. package/dist/content-package-builder.js.map +1 -0
  10. package/dist/id-references.d.ts +20 -0
  11. package/dist/id-references.d.ts.map +1 -0
  12. package/dist/id-references.js +96 -0
  13. package/dist/id-references.js.map +1 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/rules-package-builder.d.ts +14 -0
  19. package/dist/rules-package-builder.d.ts.map +1 -1
  20. package/dist/rules-package-builder.js +14 -1
  21. package/dist/rules-package-builder.js.map +1 -1
  22. package/package.json +6 -3
  23. package/schema-source/builders/IdBuilder.ts +849 -0
  24. package/schema-source/schema/common/Id.ts +3 -1
  25. package/schema-source/schema/delve/common.ts +3 -1
  26. package/schema-source/schema/generic/CollectableNode.ts +3 -1
  27. package/schema-source/schema/generic/CollectionNode.ts +3 -1
  28. package/schema-source/schema/generic/EmbedOnlyNode.ts +3 -1
  29. package/schema-source/schema/generic/NonCollectableNode.ts +3 -1
  30. package/schema-source/schema/generic/PrimaryTypeNode.ts +3 -1
  31. package/schema-source/schema/rules/TagRule.ts +3 -1
  32. package/schema-source/schema/utils/EmbeddedNode.ts +3 -1
  33. package/schema-source/schema/utils/string.ts +2 -2
  34. package/schema-source/scripts/augmentations.ts +70 -0
  35. package/schema-source/scripts/codegen/schemaToTsDeclaration.ts +290 -0
  36. package/schema-source/scripts/codegen/tsDocTags.ts +75 -0
  37. package/schema-source/scripts/codegen/writeTypescriptTypes.ts +35 -0
  38. package/schema-source/scripts/const.ts +40 -2
  39. package/schema-source/scripts/datasworn/sort.ts +80 -0
  40. package/schema-source/scripts/json-typedef/typedef.ts +1138 -0
  41. package/schema-source/scripts/json-typedef/utils.ts +349 -0
  42. package/schema-source/scripts/schema/buildSchema.ts +109 -0
  43. package/schema-source/scripts/utils/Log.ts +15 -0
  44. package/schema-source/scripts/utils/readWrite.ts +180 -0
  45. package/schema-source/scripts/validation/ajv.ts +29 -0
  46. package/schema-source/scripts/validation/formats.ts +6 -0
  47. 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
@@ -0,0 +1,6 @@
1
+ import type { Format } from 'ajv'
2
+
3
+ export const FORMATS: Record<string, Format> = {
4
+ markdown: true,
5
+ dice_notation: true
6
+ }