@amritk/generate-examples
Advanced tools
| {"version":3,"file":"build-schema.d.ts","sourceRoot":"","sources":["../../src/generators/build-schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAIjE;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,kBAAkB,eACjB,UAAU,gBACR,MAAM,0BAEnB,OAAO,CAAC,aAAa,EAAE,CAmBzB,CAAA"} |
| import { generateIndexBarrel } from '@amritk/helpers/generate-index-barrel'; | ||
| import { walkRefGraph } from '@amritk/helpers/walk-ref-graph'; | ||
| import { generateExampleFile } from './generate-files.js'; | ||
| /** | ||
| * Builds all TypeScript example files from a JSON Schema by traversing all | ||
| * `$ref` / `$dynamicRef` references recursively (via the shared | ||
| * `@amritk/helpers/walk-ref-graph` walker). | ||
| * | ||
| * Each generated file exports: | ||
| * - A TypeScript type definition | ||
| * - A `fast-check` arbitrary (`FooArbitrary`) that produces schema-valid values | ||
| * - A concrete example value (`fooExample`) | ||
| * | ||
| * An `index.ts` re-exports everything. The generated output imports `fast-check`, | ||
| * which consumers must install as a (dev) dependency. | ||
| * | ||
| * @param rootSchema - The root JSON Schema to build from | ||
| * @param rootTypeName - The name for the root type (e.g. "Document") | ||
| * @param typeSuffix - Suffix appended to every `$ref`-derived name (default `''`) | ||
| * @returns An array of generated TypeScript files | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const files = await buildExampleSchema(schema, 'Document') | ||
| * // files → [{ filename: 'document.ts', content: '...' }, { filename: 'index.ts', ... }] | ||
| * ``` | ||
| */ | ||
| export const buildExampleSchema = async (rootSchema, rootTypeName, typeSuffix = '') => { | ||
| const files = []; | ||
| walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => { | ||
| // `index` is reserved for the barrel below, so never let a definition of | ||
| // that name overwrite it. | ||
| if (node.filename === 'index') | ||
| return; | ||
| const content = generateExampleFile(node.schema, node.typeName, { | ||
| rootSchema: node.rootSchema, | ||
| typeSuffix, | ||
| ...(node.ref !== undefined ? { selfRef: node.ref } : {}), | ||
| }); | ||
| files.push({ filename: `${node.filename}.ts`, content }); | ||
| }); | ||
| files.push({ filename: 'index.ts', content: generateIndexBarrel(files) }); | ||
| return files; | ||
| }; |
| {"version":3,"file":"collect-example-imports.d.ts","sourceRoot":"","sources":["../../src/generators/collect-example-imports.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAEjE;;GAEG;AACH,KAAK,4BAA4B,GAAG;IAClC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACrC;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;IACzD;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AA0DD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,WAAY,UAAU,YAAY,4BAA4B,KAAG,MAAM,EA0BxG,CAAA"} |
| import { refToFilename } from '@amritk/helpers/ref-to-filename'; | ||
| import { refToName } from '@amritk/helpers/ref-to-name'; | ||
| import { resolveRef } from '@amritk/helpers/resolve-ref'; | ||
| import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasItems, hasOneOf, hasRef } from '@amritk/helpers/schema-guards'; | ||
| /** | ||
| * Generates an import statement for a single $ref, importing both the generated | ||
| * type and its arbitrary from the ref's generated file. | ||
| */ | ||
| const buildImport = (ref, suffix) => { | ||
| const filename = refToFilename(ref); | ||
| const typeName = refToName(ref, suffix); | ||
| return `import { type ${typeName}, ${typeName}Arbitrary } from './${filename}'`; | ||
| }; | ||
| /** | ||
| * Walks one level of the schema and yields all direct $ref strings that should | ||
| * become imports: properties, additionalProperties, items, and union branches. | ||
| */ | ||
| const collectDirectRefs = (schema) => { | ||
| if (typeof schema === 'boolean' || schema === null) | ||
| return []; | ||
| const refs = []; | ||
| if (hasRef(schema)) { | ||
| refs.push(schema.$ref); | ||
| return refs; | ||
| } | ||
| const propSchemas = 'properties' in schema && typeof schema.properties === 'object' && schema.properties !== null | ||
| ? Object.values(schema.properties) | ||
| : []; | ||
| for (const prop of propSchemas) { | ||
| if (hasRef(prop)) | ||
| refs.push(prop.$ref); | ||
| if (hasItems(prop) && hasRef(prop.items)) | ||
| refs.push(prop.items.$ref); | ||
| if (hasAdditionalProperties(prop) && hasRef(prop.additionalProperties)) { | ||
| refs.push(prop.additionalProperties.$ref); | ||
| } | ||
| } | ||
| if (hasItems(schema) && hasRef(schema.items)) { | ||
| refs.push(schema.items.$ref); | ||
| } | ||
| if (hasAdditionalProperties(schema) && hasRef(schema.additionalProperties)) { | ||
| refs.push(schema.additionalProperties.$ref); | ||
| } | ||
| for (const branch of [ | ||
| ...(hasOneOf(schema) ? schema.oneOf : []), | ||
| ...(hasAnyOf(schema) ? schema.anyOf : []), | ||
| ...(hasAllOf(schema) ? schema.allOf : []), | ||
| ]) { | ||
| if (hasRef(branch)) | ||
| refs.push(branch.$ref); | ||
| } | ||
| return refs; | ||
| }; | ||
| /** | ||
| * Collects import statements for all $ref dependencies of a schema. Each import | ||
| * brings in both the generated TypeScript type and the arbitrary for that ref. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const schema = { properties: { address: { $ref: '#/$defs/address' } } } | ||
| * collectExampleImports(schema) | ||
| * // ["import { type Address, AddressArbitrary } from './address'"] | ||
| * ``` | ||
| */ | ||
| export const collectExampleImports = (schema, options) => { | ||
| const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null; | ||
| const rootSchema = options?.rootSchema; | ||
| const typeSuffix = options?.typeSuffix ?? ''; | ||
| const refs = collectDirectRefs(schema); | ||
| const seen = new Set(); | ||
| const imports = []; | ||
| for (const ref of refs) { | ||
| const filename = refToFilename(ref); | ||
| if (seen.has(filename)) | ||
| continue; | ||
| if (selfFilename && filename === selfFilename) | ||
| continue; | ||
| // Skip refs that don't resolve in this schema (external / never generated) | ||
| if (rootSchema) { | ||
| const resolved = resolveRef(ref, rootSchema); | ||
| if (!resolved) | ||
| continue; | ||
| } | ||
| seen.add(filename); | ||
| imports.push(buildImport(ref, typeSuffix)); | ||
| } | ||
| return imports; | ||
| }; |
| {"version":3,"file":"derive-example.d.ts","sourceRoot":"","sources":["../../src/generators/derive-example.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAgCjE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,WAChB,UAAU,eACL,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,SAC9B,WAAW,CAAC,MAAM,CAAC,KACxB,OAuDF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,UAAW,OAAO,KAAG,MAW/C,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,WACvB,UAAU,YACR,MAAM,eACH,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACnC,MAGF,CAAA"} |
| import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'; | ||
| import { resolveRef } from '@amritk/helpers/resolve-ref'; | ||
| import { hasAnyOf, hasConst, hasDefault, hasEnum, hasExamples, hasFormat, hasItems, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasOneOf, hasProperties, hasRef, hasType, isSchemaObject, } from '@amritk/helpers/schema-guards'; | ||
| /** Lowercases the first character of a name. e.g. "User" → "user" */ | ||
| const lowerFirst = (name) => name.charAt(0).toLowerCase() + name.slice(1); | ||
| /** Derives the example const name from a type name. e.g. "User" → "userExample" */ | ||
| const exampleName = (typeName) => `${lowerFirst(typeName)}Example`; | ||
| /** Returns a representative string honouring `format` and length constraints. */ | ||
| const exampleString = (schema) => { | ||
| if (hasFormat(schema)) { | ||
| switch (schema.format) { | ||
| case 'email': | ||
| return 'user@example.com'; | ||
| case 'uuid': | ||
| return '00000000-0000-0000-0000-000000000000'; | ||
| case 'uri': | ||
| case 'url': | ||
| return 'https://example.com'; | ||
| case 'date-time': | ||
| return '1970-01-01T00:00:00.000Z'; | ||
| case 'date': | ||
| return '1970-01-01'; | ||
| } | ||
| } | ||
| let value = 'string'; | ||
| if (hasMinLength(schema) && value.length < schema.minLength) | ||
| value = value.padEnd(schema.minLength, 'x'); | ||
| if (hasMaxLength(schema) && value.length > schema.maxLength) | ||
| value = value.slice(0, schema.maxLength); | ||
| return value; | ||
| }; | ||
| /** | ||
| * Derives a single concrete, schema-valid value from a JSON Schema. | ||
| * | ||
| * Prefers explicit hints in this order: `const`, `examples[0]`, `default`, | ||
| * `enum[0]`; otherwise produces a canonical value for the declared type. | ||
| * `$ref`s are resolved and inlined by value; recursive refs short-circuit to | ||
| * `null` (tracked via `seen`). | ||
| * | ||
| * Note: values constrained only by `pattern` are not guaranteed to match the | ||
| * pattern — use the generated arbitrary when pattern fidelity matters. | ||
| */ | ||
| export const deriveExample = (schema, rootSchema, seen = new Set()) => { | ||
| if (!isSchemaObject(schema)) | ||
| return null; | ||
| if (hasConst(schema)) | ||
| return schema.const; | ||
| if (hasExamples(schema) && Array.isArray(schema.examples) && schema.examples.length > 0) | ||
| return schema.examples[0]; | ||
| if (hasDefault(schema)) | ||
| return schema.default; | ||
| if (hasEnum(schema) && schema.enum.length > 0) | ||
| return schema.enum[0]; | ||
| if (hasRef(schema)) { | ||
| const ref = schema.$ref; | ||
| if (seen.has(ref) || !rootSchema) | ||
| return null; | ||
| const resolved = resolveRef(ref, rootSchema); | ||
| if (!resolved) | ||
| return null; | ||
| return deriveExample(resolved, rootSchema, new Set([...seen, ref])); | ||
| } | ||
| const instanceOf = getMjstInstanceOf(schema); | ||
| if (instanceOf === 'Date') | ||
| return new Date(0); | ||
| const primitive = getMjstPrimitive(schema); | ||
| if (primitive === 'bigint') | ||
| return 0n; | ||
| if (hasOneOf(schema) && schema.oneOf[0] !== undefined) | ||
| return deriveExample(schema.oneOf[0], rootSchema, seen); | ||
| if (hasAnyOf(schema) && schema.anyOf[0] !== undefined) | ||
| return deriveExample(schema.anyOf[0], rootSchema, seen); | ||
| // `hasType` only matches a single string `type`; multi-type schemas fall | ||
| // through to `null`. | ||
| if (!hasType(schema)) | ||
| return null; | ||
| switch (schema.type) { | ||
| case 'string': | ||
| return exampleString(schema); | ||
| case 'number': | ||
| case 'integer': | ||
| return hasMinimum(schema) ? schema.minimum : 0; | ||
| case 'boolean': | ||
| return true; | ||
| case 'null': | ||
| return null; | ||
| case 'array': { | ||
| const item = hasItems(schema) ? deriveExample(schema.items, rootSchema, seen) : null; | ||
| const count = hasMinItems(schema) ? Math.max(schema.minItems, 1) : 1; | ||
| return Array.from({ length: count }, () => item); | ||
| } | ||
| case 'object': { | ||
| const out = {}; | ||
| if (hasProperties(schema)) { | ||
| for (const [key, propSchema] of Object.entries(schema.properties)) { | ||
| out[key] = deriveExample(propSchema, rootSchema, seen); | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
| default: | ||
| return null; | ||
| } | ||
| }; | ||
| /** | ||
| * Serializes a derived value into a TypeScript source expression. Handles the | ||
| * non-JSON values `deriveExample` can produce (`Date`, `bigint`) in addition to | ||
| * plain JSON. | ||
| */ | ||
| export const serializeValue = (value) => { | ||
| if (typeof value === 'bigint') | ||
| return `${value}n`; | ||
| if (value instanceof Date) | ||
| return `new Date(${JSON.stringify(value.toISOString())})`; | ||
| if (Array.isArray(value)) | ||
| return `[${value.map(serializeValue).join(', ')}]`; | ||
| if (value !== null && typeof value === 'object') { | ||
| const entries = Object.entries(value) | ||
| .filter(([, v]) => v !== undefined) | ||
| .map(([key, v]) => `${JSON.stringify(key)}: ${serializeValue(v)}`); | ||
| return `{ ${entries.join(', ')} }`; | ||
| } | ||
| return JSON.stringify(value); | ||
| }; | ||
| /** | ||
| * Generates an exported const holding a concrete, schema-valid example value. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * generateExampleConst({ type: 'object', properties: { name: { type: 'string' } } }, 'Info') | ||
| * // export const infoExample: Info = { "name": "string" } | ||
| * ``` | ||
| */ | ||
| export const generateExampleConst = (schema, typeName, rootSchema) => { | ||
| const value = deriveExample(schema, rootSchema); | ||
| return `export const ${exampleName(typeName)}: ${typeName} = ${serializeValue(value)}`; | ||
| }; |
| {"version":3,"file":"generate-arbitrary.d.ts","sourceRoot":"","sources":["../../src/generators/generate-arbitrary.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AA2JjE;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,WAAY,UAAU,YAAY,MAAM,sBAAgB,MAGrF,CAAA"} |
| import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'; | ||
| import { refToName } from '@amritk/helpers/ref-to-name'; | ||
| import { hasAnyOf, hasConst, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasFormat, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject, } from '@amritk/helpers/schema-guards'; | ||
| /** | ||
| * Derives the arbitrary const name from a type name. | ||
| * e.g. "User" → "UserArbitrary" | ||
| */ | ||
| const arbitraryName = (typeName) => `${typeName}Arbitrary`; | ||
| /** Builds a `fc.string({ ... })` expression honouring format and length constraints. */ | ||
| const stringExpr = (schema) => { | ||
| if (hasFormat(schema)) { | ||
| switch (schema.format) { | ||
| case 'email': | ||
| return 'fc.emailAddress()'; | ||
| case 'uuid': | ||
| return 'fc.uuid()'; | ||
| case 'uri': | ||
| case 'url': | ||
| return 'fc.webUrl()'; | ||
| case 'date-time': | ||
| return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString())'; | ||
| case 'date': | ||
| return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString().slice(0, 10))'; | ||
| } | ||
| } | ||
| if (hasPattern(schema)) | ||
| return `fc.stringMatching(/${schema.pattern}/)`; | ||
| const opts = []; | ||
| if (hasMinLength(schema)) | ||
| opts.push(`minLength: ${schema.minLength}`); | ||
| if (hasMaxLength(schema)) | ||
| opts.push(`maxLength: ${schema.maxLength}`); | ||
| return opts.length > 0 ? `fc.string({ ${opts.join(', ')} })` : 'fc.string()'; | ||
| }; | ||
| /** Builds a `fc.integer({ ... })` expression honouring range and multiple-of constraints. */ | ||
| const integerExpr = (schema) => { | ||
| const opts = []; | ||
| if (hasMinimum(schema)) | ||
| opts.push(`min: ${schema.minimum}`); | ||
| else if (hasExclusiveMinimum(schema)) | ||
| opts.push(`min: ${Number(schema.exclusiveMinimum) + 1}`); | ||
| if (hasMaximum(schema)) | ||
| opts.push(`max: ${schema.maximum}`); | ||
| else if (hasExclusiveMaximum(schema)) | ||
| opts.push(`max: ${Number(schema.exclusiveMaximum) - 1}`); | ||
| const base = opts.length > 0 ? `fc.integer({ ${opts.join(', ')} })` : 'fc.integer()'; | ||
| return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base; | ||
| }; | ||
| /** Builds a `fc.double({ ... })` expression honouring range and multiple-of constraints. */ | ||
| const numberExpr = (schema) => { | ||
| const opts = ['noNaN: true', 'noDefaultInfinity: true']; | ||
| if (hasMinimum(schema)) | ||
| opts.push(`min: ${schema.minimum}`); | ||
| else if (hasExclusiveMinimum(schema)) | ||
| opts.push(`min: ${schema.exclusiveMinimum}`, 'minExcluded: true'); | ||
| if (hasMaximum(schema)) | ||
| opts.push(`max: ${schema.maximum}`); | ||
| else if (hasExclusiveMaximum(schema)) | ||
| opts.push(`max: ${schema.exclusiveMaximum}`, 'maxExcluded: true'); | ||
| const base = `fc.double({ ${opts.join(', ')} })`; | ||
| return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base; | ||
| }; | ||
| /** Builds a `fc.array(...)` / `fc.uniqueArray(...)` expression for an array schema. */ | ||
| const arrayExpr = (schema, suffix) => { | ||
| const items = hasItems(schema) && isSchemaObject(schema.items) ? arbitraryExpr(schema.items, suffix) : 'fc.anything()'; | ||
| const opts = []; | ||
| if (hasMinItems(schema)) | ||
| opts.push(`minLength: ${schema.minItems}`); | ||
| if (hasMaxItems(schema)) | ||
| opts.push(`maxLength: ${schema.maxItems}`); | ||
| const fn = hasUniqueItems(schema) && schema.uniqueItems === true ? 'fc.uniqueArray' : 'fc.array'; | ||
| return opts.length > 0 ? `${fn}(${items}, { ${opts.join(', ')} })` : `${fn}(${items})`; | ||
| }; | ||
| /** Builds a `fc.record(...)` expression for an object schema. */ | ||
| const objectExpr = (schema, suffix) => { | ||
| if (!hasProperties(schema)) | ||
| return 'fc.object()'; | ||
| const required = new Set(hasRequired(schema) ? schema.required : []); | ||
| const keys = Object.keys(schema.properties); | ||
| const entries = Object.entries(schema.properties).map(([key, propSchema]) => `${JSON.stringify(key)}: ${arbitraryExpr(propSchema, suffix)}`); | ||
| if (entries.length === 0) | ||
| return 'fc.record({})'; | ||
| const model = `{ ${entries.join(', ')} }`; | ||
| // fc.record treats all keys as required by default. Only emit requiredKeys | ||
| // when at least one property is optional. | ||
| if (keys.every((key) => required.has(key))) | ||
| return `fc.record(${model})`; | ||
| const requiredKeys = [...required].map((key) => JSON.stringify(key)).join(', '); | ||
| return `fc.record(${model}, { requiredKeys: [${requiredKeys}] })`; | ||
| }; | ||
| /** Builds a `fc.oneof(...)` expression from a list of branch schemas. */ | ||
| const oneofExpr = (branches, suffix) => { | ||
| const exprs = branches.map((branch) => arbitraryExpr(branch, suffix)); | ||
| return `fc.oneof(${exprs.join(', ')})`; | ||
| }; | ||
| /** Builds the fast-check expression for a single (non-union) JSON Schema type. */ | ||
| const scalarExpr = (type, schema, suffix) => { | ||
| switch (type) { | ||
| case 'string': | ||
| return stringExpr(schema); | ||
| case 'integer': | ||
| return integerExpr(schema); | ||
| case 'number': | ||
| return numberExpr(schema); | ||
| case 'boolean': | ||
| return 'fc.boolean()'; | ||
| case 'null': | ||
| return 'fc.constant(null)'; | ||
| case 'array': | ||
| return arrayExpr(schema, suffix); | ||
| case 'object': | ||
| return objectExpr(schema, suffix); | ||
| default: | ||
| return 'fc.anything()'; | ||
| } | ||
| }; | ||
| /** | ||
| * Recursively builds the fast-check arbitrary expression for a schema node. | ||
| * `$ref`s resolve to the referenced file's exported arbitrary; everything else | ||
| * maps to the appropriate `fc.*` combinator. | ||
| */ | ||
| const arbitraryExpr = (schema, suffix) => { | ||
| if (!isSchemaObject(schema)) | ||
| return 'fc.anything()'; | ||
| if (hasRef(schema)) | ||
| return arbitraryName(refToName(schema.$ref, suffix)); | ||
| if (hasConst(schema)) | ||
| return `fc.constant(${JSON.stringify(schema.const)})`; | ||
| if (hasEnum(schema)) { | ||
| const values = schema.enum.map((value) => JSON.stringify(value)).join(', '); | ||
| return `fc.constantFrom(${values})`; | ||
| } | ||
| const instanceOf = getMjstInstanceOf(schema); | ||
| if (instanceOf === 'Date') | ||
| return 'fc.date({ noInvalidDate: true })'; | ||
| if (instanceOf) | ||
| return 'fc.anything()'; | ||
| const primitive = getMjstPrimitive(schema); | ||
| if (primitive === 'bigint') | ||
| return 'fc.bigInt()'; | ||
| if (primitive) | ||
| return 'fc.anything()'; | ||
| if (hasOneOf(schema)) | ||
| return oneofExpr(schema.oneOf, suffix); | ||
| if (hasAnyOf(schema)) | ||
| return oneofExpr(schema.anyOf, suffix); | ||
| // `hasType` only matches a single string `type`; multi-type schemas | ||
| // (`type: ['string', 'null']`) fall through to the permissive fallback. | ||
| if (hasType(schema)) | ||
| return scalarExpr(schema.type, schema, suffix); | ||
| return 'fc.anything()'; | ||
| }; | ||
| /** | ||
| * Generates a `fast-check` arbitrary that produces schema-valid values. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * generateArbitrary({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, 'Info') | ||
| * // export const InfoArbitrary: fc.Arbitrary<Info> = fc.record({ "name": fc.string() }) | ||
| * ``` | ||
| */ | ||
| export const generateArbitrary = (schema, typeName, suffix = '') => { | ||
| const expr = arbitraryExpr(schema, suffix); | ||
| return `export const ${arbitraryName(typeName)}: fc.Arbitrary<${typeName}> = ${expr}`; | ||
| }; |
| {"version":3,"file":"generate-files.d.ts","sourceRoot":"","sources":["../../src/generators/generate-files.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAMjE;;GAEG;AACH,KAAK,0BAA0B,GAAG;IAChC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7C;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,mBAAmB,WACtB,UAAU,YACR,MAAM,YACN,0BAA0B,KACnC,MAsBF,CAAA"} |
| import { generateTypeDefinition } from '@amritk/helpers/generate-type-definition'; | ||
| import { collectExampleImports } from './collect-example-imports.js'; | ||
| import { generateExampleConst } from './derive-example.js'; | ||
| import { generateArbitrary } from './generate-arbitrary.js'; | ||
| /** | ||
| * Generates a complete TypeScript example file from a JSON Schema. | ||
| * | ||
| * The file contains: | ||
| * - An import of `fast-check` and imports for any `$ref` types and arbitraries | ||
| * - The exported TypeScript type definition | ||
| * - An exported `fast-check` arbitrary (`FooArbitrary`) | ||
| * - An exported concrete example value (`fooExample`) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const schema = { type: 'object', properties: { title: { type: 'string' } }, required: ['title'] } | ||
| * generateExampleFile(schema, 'Info') | ||
| * // import * as fc from 'fast-check' | ||
| * // export type Info = { title: string } | ||
| * // export const InfoArbitrary: fc.Arbitrary<Info> = fc.record({ "title": fc.string() }) | ||
| * // export const infoExample: Info = { "title": "string" } | ||
| * ``` | ||
| */ | ||
| export const generateExampleFile = (schema, typeName, options) => { | ||
| const typeSuffix = options?.typeSuffix ?? ''; | ||
| const refImports = collectExampleImports(schema, { | ||
| selfRef: options?.selfRef, | ||
| rootSchema: options?.rootSchema, | ||
| typeSuffix, | ||
| }); | ||
| const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix }); | ||
| const arbitrary = generateArbitrary(schema, typeName, typeSuffix); | ||
| const example = generateExampleConst(schema, typeName, options?.rootSchema); | ||
| let result = `import * as fc from 'fast-check'\n`; | ||
| for (const imp of refImports) { | ||
| result += imp + '\n'; | ||
| } | ||
| result += '\n'; | ||
| result += typeDefinition + '\n\n' + arbitrary + '\n\n' + example + '\n'; | ||
| return result; | ||
| }; |
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AACjG,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA"} |
@@ -11,3 +11,4 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| * Builds all TypeScript example files from a JSON Schema by traversing all | ||
| * $ref references recursively, mirroring the generate-parsers pipeline. | ||
| * `$ref` / `$dynamicRef` references recursively (via the shared | ||
| * `@amritk/helpers/walk-ref-graph` walker). | ||
| * | ||
@@ -34,1 +35,2 @@ * Each generated file exports: | ||
| export declare const buildExampleSchema: (rootSchema: JSONSchema, rootTypeName: string, typeSuffix?: string) => Promise<GeneratedFile[]>; | ||
| //# sourceMappingURL=build-schema.d.ts.map |
@@ -35,1 +35,2 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export {}; | ||
| //# sourceMappingURL=collect-example-imports.d.ts.map |
@@ -30,1 +30,2 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export declare const generateExampleConst: (schema: JSONSchema, typeName: string, rootSchema?: Record<string, unknown>) => string; | ||
| //# sourceMappingURL=derive-example.d.ts.map |
@@ -12,1 +12,2 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export declare const generateArbitrary: (schema: JSONSchema, typeName: string, suffix?: string) => string; | ||
| //# sourceMappingURL=generate-arbitrary.d.ts.map |
@@ -43,1 +43,2 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export {}; | ||
| //# sourceMappingURL=generate-files.d.ts.map |
+5
-4
@@ -1,4 +0,5 @@ | ||
| export type { GeneratedFile } from './generators/build-schema'; | ||
| export { buildExampleSchema } from './generators/build-schema'; | ||
| export { deriveExample, generateExampleConst, serializeValue } from './generators/derive-example'; | ||
| export { generateArbitrary } from './generators/generate-arbitrary'; | ||
| export type { GeneratedFile } from './generators/build-schema.js'; | ||
| export { buildExampleSchema } from './generators/build-schema.js'; | ||
| export { deriveExample, generateExampleConst, serializeValue } from './generators/derive-example.js'; | ||
| export { generateArbitrary } from './generators/generate-arbitrary.js'; | ||
| //# sourceMappingURL=index.d.ts.map |
+3
-1353
@@ -1,1353 +0,3 @@ | ||
| // ../helpers/dist/build-dynamic-ref-map.js | ||
| var isSchemaObject = (schema) => { | ||
| return typeof schema === "object" && schema !== null && typeof schema !== "boolean"; | ||
| }; | ||
| var buildDynamicRefMap = (rootSchema) => { | ||
| const map = {}; | ||
| if (!isSchemaObject(rootSchema) || !("$defs" in rootSchema)) { | ||
| return map; | ||
| } | ||
| const defs = rootSchema.$defs; | ||
| for (const [key, value] of Object.entries(defs)) { | ||
| if (typeof value === "object" && value !== null && "$dynamicAnchor" in value && typeof value["$dynamicAnchor"] === "string") { | ||
| const anchor = value["$dynamicAnchor"]; | ||
| map[`#${anchor}`] = `#/$defs/${key}`; | ||
| } | ||
| } | ||
| return map; | ||
| }; | ||
| // ../helpers/dist/extract-refs.js | ||
| var isResolvableRef = (ref) => { | ||
| if (ref === "#") | ||
| return false; | ||
| if (ref.startsWith("#")) | ||
| return true; | ||
| if (ref.startsWith("http://") || ref.startsWith("https://")) | ||
| return true; | ||
| return false; | ||
| }; | ||
| var extractRefs = (schema) => { | ||
| const refs = new Set; | ||
| const traverse = (obj) => { | ||
| if (typeof obj !== "object" || obj === null) { | ||
| return; | ||
| } | ||
| if (Array.isArray(obj)) { | ||
| for (const item of obj) { | ||
| traverse(item); | ||
| } | ||
| return; | ||
| } | ||
| const record = obj; | ||
| if ("$ref" in record && typeof record["$ref"] === "string" && isResolvableRef(record["$ref"])) { | ||
| refs.add(record["$ref"]); | ||
| } | ||
| for (const key in record) { | ||
| traverse(record[key]); | ||
| } | ||
| }; | ||
| traverse(schema); | ||
| return refs; | ||
| }; | ||
| // ../helpers/dist/ref-to-filename.js | ||
| var toKebabCase = (value) => value.replace(/OAuth/g, "Oauth").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase(); | ||
| var uriRefToFilename = (uri) => { | ||
| const hashIndex = uri.indexOf("#"); | ||
| const baseUri = hashIndex === -1 ? uri : uri.slice(0, hashIndex); | ||
| const fragment = hashIndex === -1 ? "" : uri.slice(hashIndex + 1); | ||
| const withoutProtocol = baseUri.replace(/^https?:\/\/[^/]+\//, ""); | ||
| const withoutExt = withoutProtocol.replace(/\.json$/, ""); | ||
| const rawSegments = withoutExt.split("/"); | ||
| const SKIP_KEYS = new Set(["definitions", "$defs"]); | ||
| const segments = []; | ||
| for (let i = 0;i < rawSegments.length; i++) { | ||
| const s = rawSegments[i]; | ||
| if (SKIP_KEYS.has(s)) | ||
| continue; | ||
| const prevRaw = rawSegments[i - 1]; | ||
| if (/^\d+\.\d+/.test(s) && prevRaw !== undefined && SKIP_KEYS.has(prevRaw)) | ||
| continue; | ||
| segments.push(s); | ||
| } | ||
| const baseName = segments.map((s) => toKebabCase(s).replace(/\./g, "-")).join("-"); | ||
| if (!fragment) | ||
| return baseName; | ||
| const fragSegments = fragment.split("/").filter((s) => s && !SKIP_KEYS.has(s) && s !== "properties"); | ||
| const fragLast = fragSegments[fragSegments.length - 1]; | ||
| if (!fragLast) | ||
| return baseName; | ||
| return `${baseName}-${toKebabCase(fragLast)}`; | ||
| }; | ||
| var refToFilename = (ref) => { | ||
| if (ref.startsWith("http://") || ref.startsWith("https://")) { | ||
| return uriRefToFilename(ref); | ||
| } | ||
| const segments = ref.split("/"); | ||
| let filename = segments[segments.length - 1]; | ||
| if (/[A-Z]/.test(filename)) { | ||
| filename = toKebabCase(filename); | ||
| } | ||
| return filename; | ||
| }; | ||
| // ../helpers/dist/ref-to-name.js | ||
| var toKebabCase2 = (value) => value.replace(/OAuth/g, "Oauth").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase(); | ||
| var uriRefToFilename2 = (uri) => { | ||
| const hashIndex = uri.indexOf("#"); | ||
| const baseUri = hashIndex === -1 ? uri : uri.slice(0, hashIndex); | ||
| const fragment = hashIndex === -1 ? "" : uri.slice(hashIndex + 1); | ||
| const withoutProtocol = baseUri.replace(/^https?:\/\/[^/]+\//, ""); | ||
| const withoutExt = withoutProtocol.replace(/\.json$/, ""); | ||
| const rawSegments = withoutExt.split("/"); | ||
| const SKIP_KEYS = new Set(["definitions", "$defs"]); | ||
| const segments = []; | ||
| for (let i = 0;i < rawSegments.length; i++) { | ||
| const s = rawSegments[i]; | ||
| if (SKIP_KEYS.has(s)) | ||
| continue; | ||
| const prevRaw = rawSegments[i - 1]; | ||
| if (/^\d+\.\d+/.test(s) && prevRaw !== undefined && SKIP_KEYS.has(prevRaw)) | ||
| continue; | ||
| segments.push(s); | ||
| } | ||
| const baseName = segments.map((s) => toKebabCase2(s).replace(/\./g, "-")).join("-"); | ||
| if (!fragment) | ||
| return baseName; | ||
| const fragSegments = fragment.split("/").filter((s) => s && !SKIP_KEYS.has(s) && s !== "properties"); | ||
| const fragLast = fragSegments[fragSegments.length - 1]; | ||
| if (!fragLast) | ||
| return baseName; | ||
| return `${baseName}-${toKebabCase2(fragLast)}`; | ||
| }; | ||
| var refToFilename2 = (ref) => { | ||
| if (ref.startsWith("http://") || ref.startsWith("https://")) { | ||
| return uriRefToFilename2(ref); | ||
| } | ||
| const segments = ref.split("/"); | ||
| let filename = segments[segments.length - 1]; | ||
| if (/[A-Z]/.test(filename)) { | ||
| filename = toKebabCase2(filename); | ||
| } | ||
| return filename; | ||
| }; | ||
| var kebabToPascal = (kebab, suffix) => { | ||
| const words = kebab.split("-"); | ||
| let pascalCase = ""; | ||
| for (const word of words) { | ||
| pascalCase += word.charAt(0).toUpperCase() + word.slice(1); | ||
| } | ||
| return pascalCase + suffix; | ||
| }; | ||
| var refToName = (ref, suffix = "") => kebabToPascal(refToFilename2(ref), suffix); | ||
| // ../helpers/dist/resolve-dynamic-refs.js | ||
| var resolveDynamicRefs = (schema, dynamicRefMap) => { | ||
| if (typeof schema !== "object" || schema === null) { | ||
| return schema; | ||
| } | ||
| if (Object.keys(dynamicRefMap).length === 0) { | ||
| return schema; | ||
| } | ||
| const clone = JSON.parse(JSON.stringify(schema)); | ||
| const walk = (obj) => { | ||
| if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { | ||
| return; | ||
| } | ||
| const record = obj; | ||
| if ("$dynamicRef" in record && typeof record["$dynamicRef"] === "string") { | ||
| const resolved = dynamicRefMap[record["$dynamicRef"]]; | ||
| if (resolved) { | ||
| record["$ref"] = resolved; | ||
| delete record["$dynamicRef"]; | ||
| } | ||
| } | ||
| for (const key in record) { | ||
| walk(record[key]); | ||
| } | ||
| }; | ||
| walk(clone); | ||
| return clone; | ||
| }; | ||
| // ../helpers/dist/resolve-ref.js | ||
| var navigatePointer = (pointer, schema) => { | ||
| const parts = pointer.split("/").filter(Boolean); | ||
| let current = schema; | ||
| for (const part of parts) { | ||
| const decodedPart = part.replace(/~1/g, "/").replace(/~0/g, "~"); | ||
| if (current && typeof current === "object" && decodedPart in current) { | ||
| const next = current[decodedPart]; | ||
| if (typeof next === "object" && next !== null) { | ||
| current = next; | ||
| } else { | ||
| return; | ||
| } | ||
| } else { | ||
| return; | ||
| } | ||
| } | ||
| return current; | ||
| }; | ||
| var resolveRef = (ref, rootSchema) => { | ||
| if (ref.startsWith("#")) { | ||
| return navigatePointer(ref.slice(1), rootSchema); | ||
| } | ||
| const hashIndex = ref.indexOf("#"); | ||
| const baseUri = hashIndex === -1 ? ref : ref.slice(0, hashIndex); | ||
| const rawFragment = hashIndex === -1 ? "" : ref.slice(hashIndex + 1); | ||
| const fragment = rawFragment === "" || rawFragment === "/" ? "" : rawFragment; | ||
| const defs = rootSchema["$defs"]; | ||
| if (typeof defs !== "object" || defs === null) | ||
| return; | ||
| const defsRecord = defs; | ||
| const base = defsRecord[baseUri]; | ||
| if (typeof base !== "object" || base === null) | ||
| return; | ||
| if (!fragment) | ||
| return base; | ||
| const normalizedFragment = fragment.replace(/^\/definitions\//, "/$defs/"); | ||
| return navigatePointer(normalizedFragment, base); | ||
| }; | ||
| // ../helpers/dist/upgrade-draft07-schema.js | ||
| var toKebabCase3 = (value) => value.replace(/OAuth/g, "Oauth").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase(); | ||
| var uriRefToFilename3 = (uri) => { | ||
| const hashIndex = uri.indexOf("#"); | ||
| const baseUri = hashIndex === -1 ? uri : uri.slice(0, hashIndex); | ||
| const fragment = hashIndex === -1 ? "" : uri.slice(hashIndex + 1); | ||
| const withoutProtocol = baseUri.replace(/^https?:\/\/[^/]+\//, ""); | ||
| const withoutExt = withoutProtocol.replace(/\.json$/, ""); | ||
| const rawSegments = withoutExt.split("/"); | ||
| const SKIP_KEYS = new Set(["definitions", "$defs"]); | ||
| const segments = []; | ||
| for (let i = 0;i < rawSegments.length; i++) { | ||
| const s = rawSegments[i]; | ||
| if (SKIP_KEYS.has(s)) | ||
| continue; | ||
| const prevRaw = rawSegments[i - 1]; | ||
| if (/^\d+\.\d+/.test(s) && prevRaw !== undefined && SKIP_KEYS.has(prevRaw)) | ||
| continue; | ||
| segments.push(s); | ||
| } | ||
| const baseName = segments.map((s) => toKebabCase3(s).replace(/\./g, "-")).join("-"); | ||
| if (!fragment) | ||
| return baseName; | ||
| const fragSegments = fragment.split("/").filter((s) => s && !SKIP_KEYS.has(s) && s !== "properties"); | ||
| const fragLast = fragSegments[fragSegments.length - 1]; | ||
| if (!fragLast) | ||
| return baseName; | ||
| return `${baseName}-${toKebabCase3(fragLast)}`; | ||
| }; | ||
| var refToFilename3 = (ref) => { | ||
| if (ref.startsWith("http://") || ref.startsWith("https://")) { | ||
| return uriRefToFilename3(ref); | ||
| } | ||
| const segments = ref.split("/"); | ||
| let filename = segments[segments.length - 1]; | ||
| if (/[A-Z]/.test(filename)) { | ||
| filename = toKebabCase3(filename); | ||
| } | ||
| return filename; | ||
| }; | ||
| var isDraft07Schema = (schema) => typeof schema["$schema"] === "string" && schema["$schema"].includes("draft-07"); | ||
| var rewriteRefs = (obj, refMap, selfRef) => { | ||
| if (typeof obj !== "object" || obj === null) | ||
| return obj; | ||
| if (Array.isArray(obj)) | ||
| return obj.map((item) => rewriteRefs(item, refMap, selfRef)); | ||
| const record = obj; | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(record)) { | ||
| if (key === "$ref" && typeof value === "string") { | ||
| if (refMap.has(value)) { | ||
| result[key] = refMap.get(value); | ||
| } else if (value === "#" && selfRef) { | ||
| result[key] = selfRef; | ||
| } else { | ||
| result[key] = value; | ||
| } | ||
| } else { | ||
| result[key] = rewriteRefs(value, refMap, selfRef); | ||
| } | ||
| } | ||
| return result; | ||
| }; | ||
| var hoistNestedDefs = (defs) => { | ||
| const hoisted = {}; | ||
| for (const [parentName, parentSchema] of Object.entries(defs)) { | ||
| if (typeof parentSchema !== "object" || parentSchema === null) { | ||
| hoisted[parentName] = parentSchema; | ||
| continue; | ||
| } | ||
| const parentObj = parentSchema; | ||
| const nestedDefs = parentObj["$defs"]; | ||
| if (!nestedDefs || typeof nestedDefs !== "object") { | ||
| hoisted[parentName] = parentSchema; | ||
| continue; | ||
| } | ||
| const parentPrefix = parentName.startsWith("http://") || parentName.startsWith("https://") ? refToFilename3(parentName) : parentName; | ||
| const localToHoisted = new Map; | ||
| for (const localName of Object.keys(nestedDefs)) { | ||
| const hoistedName = `${parentPrefix}-${toKebabCase3(localName)}`; | ||
| localToHoisted.set(`#/$defs/${localName}`, `#/$defs/${hoistedName}`); | ||
| } | ||
| const selfRef = `#/$defs/${parentPrefix}`; | ||
| const rewrittenParent = rewriteRefs(parentObj, localToHoisted, selfRef); | ||
| hoisted[parentName] = rewrittenParent; | ||
| for (const [localName, localSchema] of Object.entries(nestedDefs)) { | ||
| const hoistedName = `${parentPrefix}-${toKebabCase3(localName)}`; | ||
| hoisted[hoistedName] = rewriteRefs(localSchema, localToHoisted, selfRef); | ||
| } | ||
| } | ||
| return hoisted; | ||
| }; | ||
| var upgradeDraft07Schema = (schema) => { | ||
| if (!isDraft07Schema(schema)) | ||
| return schema; | ||
| const { definitions, $schema: _, ...rest } = schema; | ||
| const rawDefs = definitions ?? {}; | ||
| const renamedDefs = {}; | ||
| for (const [key, value] of Object.entries(rawDefs)) { | ||
| renamedDefs[key] = renameNestedDefs(value); | ||
| } | ||
| const hoistedDefs = hoistNestedDefs(renamedDefs); | ||
| for (const key of Object.keys(hoistedDefs)) { | ||
| if (key.startsWith("http://") || key.startsWith("https://")) { | ||
| const shortName = refToFilename3(key); | ||
| if (shortName && !(shortName in hoistedDefs)) { | ||
| hoistedDefs[shortName] = hoistedDefs[key]; | ||
| } | ||
| } | ||
| } | ||
| return { | ||
| ...rest, | ||
| $defs: hoistedDefs | ||
| }; | ||
| }; | ||
| var renameNestedDefs = (obj) => { | ||
| if (typeof obj !== "object" || obj === null) | ||
| return obj; | ||
| if (Array.isArray(obj)) | ||
| return obj.map(renameNestedDefs); | ||
| const record = obj; | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(record)) { | ||
| if (key === "$ref" && typeof value === "string" && value.startsWith("#/definitions/")) { | ||
| result[key] = value.replace("#/definitions/", "#/$defs/"); | ||
| } else { | ||
| const outKey = key === "definitions" ? "$defs" : key; | ||
| result[outKey] = renameNestedDefs(value); | ||
| } | ||
| } | ||
| return result; | ||
| }; | ||
| // ../helpers/dist/generate-type-definition.js | ||
| var isSchemaObject2 = (schema) => { | ||
| return typeof schema === "object" && schema !== null && typeof schema !== "boolean"; | ||
| }; | ||
| var isObjectSchema = (schema) => { | ||
| return isSchemaObject2(schema) && (("type" in schema) && schema.type === "object" || ("properties" in schema)); | ||
| }; | ||
| var MJST_EXTENSION_KEY = "x-mjst"; | ||
| var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/; | ||
| var SUPPORTED_PRIMITIVES = new Set(["bigint"]); | ||
| var SAFE_BRAND = /^[\w$ -]+$/; | ||
| var readExtensionString = (schema, field) => { | ||
| if (!isSchemaObject2(schema)) | ||
| return; | ||
| const extension = schema[MJST_EXTENSION_KEY]; | ||
| if (typeof extension !== "object" || extension === null) | ||
| return; | ||
| const value = extension[field]; | ||
| return typeof value === "string" ? value : undefined; | ||
| }; | ||
| var getMjstInstanceOf = (schema) => { | ||
| const instanceOf = readExtensionString(schema, "instanceOf"); | ||
| return instanceOf !== undefined && IDENTIFIER.test(instanceOf) ? instanceOf : undefined; | ||
| }; | ||
| var getMjstPrimitive = (schema) => { | ||
| const primitive = readExtensionString(schema, "primitive"); | ||
| return primitive !== undefined && SUPPORTED_PRIMITIVES.has(primitive) ? primitive : undefined; | ||
| }; | ||
| var getMjstBrand = (schema) => { | ||
| const brand = readExtensionString(schema, "brand"); | ||
| return brand !== undefined && SAFE_BRAND.test(brand) ? brand : undefined; | ||
| }; | ||
| var toKebabCase4 = (value) => value.replace(/OAuth/g, "Oauth").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase(); | ||
| var uriRefToFilename4 = (uri) => { | ||
| const hashIndex = uri.indexOf("#"); | ||
| const baseUri = hashIndex === -1 ? uri : uri.slice(0, hashIndex); | ||
| const fragment = hashIndex === -1 ? "" : uri.slice(hashIndex + 1); | ||
| const withoutProtocol = baseUri.replace(/^https?:\/\/[^/]+\//, ""); | ||
| const withoutExt = withoutProtocol.replace(/\.json$/, ""); | ||
| const rawSegments = withoutExt.split("/"); | ||
| const SKIP_KEYS = new Set(["definitions", "$defs"]); | ||
| const segments = []; | ||
| for (let i = 0;i < rawSegments.length; i++) { | ||
| const s = rawSegments[i]; | ||
| if (SKIP_KEYS.has(s)) | ||
| continue; | ||
| const prevRaw = rawSegments[i - 1]; | ||
| if (/^\d+\.\d+/.test(s) && prevRaw !== undefined && SKIP_KEYS.has(prevRaw)) | ||
| continue; | ||
| segments.push(s); | ||
| } | ||
| const baseName = segments.map((s) => toKebabCase4(s).replace(/\./g, "-")).join("-"); | ||
| if (!fragment) | ||
| return baseName; | ||
| const fragSegments = fragment.split("/").filter((s) => s && !SKIP_KEYS.has(s) && s !== "properties"); | ||
| const fragLast = fragSegments[fragSegments.length - 1]; | ||
| if (!fragLast) | ||
| return baseName; | ||
| return `${baseName}-${toKebabCase4(fragLast)}`; | ||
| }; | ||
| var refToFilename4 = (ref) => { | ||
| if (ref.startsWith("http://") || ref.startsWith("https://")) { | ||
| return uriRefToFilename4(ref); | ||
| } | ||
| const segments = ref.split("/"); | ||
| let filename = segments[segments.length - 1]; | ||
| if (/[A-Z]/.test(filename)) { | ||
| filename = toKebabCase4(filename); | ||
| } | ||
| return filename; | ||
| }; | ||
| var kebabToPascal2 = (kebab, suffix) => { | ||
| const words = kebab.split("-"); | ||
| let pascalCase = ""; | ||
| for (const word of words) { | ||
| pascalCase += word.charAt(0).toUpperCase() + word.slice(1); | ||
| } | ||
| return pascalCase + suffix; | ||
| }; | ||
| var refToName2 = (ref, suffix = "") => kebabToPascal2(refToFilename4(ref), suffix); | ||
| var JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; | ||
| var safeKey = (key) => { | ||
| if (JS_IDENTIFIER.test(key)) { | ||
| return key; | ||
| } | ||
| return `'${key}'`; | ||
| }; | ||
| var getConditionalObjectSchema = (schema) => { | ||
| if (!isSchemaObject2(schema)) { | ||
| return null; | ||
| } | ||
| if (!("if" in schema) || !("then" in schema)) { | ||
| return null; | ||
| } | ||
| const ifSchema = schema.if; | ||
| const thenSchema = schema.then; | ||
| if (!isSchemaObject2(ifSchema) || !isSchemaObject2(thenSchema)) { | ||
| return null; | ||
| } | ||
| const ifProperties = ifSchema.properties; | ||
| const thenProperties = thenSchema.properties; | ||
| const hasIfProperties = ifProperties && typeof ifProperties === "object"; | ||
| const hasThenProperties = thenProperties && typeof thenProperties === "object"; | ||
| if (!hasIfProperties && !hasThenProperties) { | ||
| return null; | ||
| } | ||
| const properties = { | ||
| ...hasIfProperties ? ifProperties : {}, | ||
| ...hasThenProperties ? thenProperties : {} | ||
| }; | ||
| const required = new Set; | ||
| if (Array.isArray(ifSchema.required)) { | ||
| for (const key of ifSchema.required) { | ||
| required.add(key); | ||
| } | ||
| } | ||
| if (hasIfProperties) { | ||
| for (const key in ifProperties) { | ||
| required.add(key); | ||
| } | ||
| } | ||
| if (Array.isArray(thenSchema.required)) { | ||
| for (const key of thenSchema.required) { | ||
| required.add(key); | ||
| } | ||
| } | ||
| if (hasThenProperties) { | ||
| for (const key in thenProperties) { | ||
| required.add(key); | ||
| } | ||
| } | ||
| const thenRef = typeof thenSchema.$ref === "string" ? thenSchema.$ref : null; | ||
| return { | ||
| schema: { | ||
| type: "object", | ||
| properties, | ||
| ...required.size > 0 ? { required: Array.from(required) } : {} | ||
| }, | ||
| thenRef | ||
| }; | ||
| }; | ||
| var isObjectLikeSchema = (schema) => { | ||
| if (!isSchemaObject2(schema)) { | ||
| return false; | ||
| } | ||
| if (isObjectSchema(schema)) { | ||
| return true; | ||
| } | ||
| return "patternProperties" in schema || "additionalProperties" in schema || "if" in schema && "then" in schema; | ||
| }; | ||
| var getBooleanSubSchemaType = (schema) => { | ||
| return schema ? "unknown" : "never"; | ||
| }; | ||
| var buildJsDocBlock = (title, description, commentUrl) => { | ||
| let block = `/** | ||
| `; | ||
| block += `* ${title} | ||
| `; | ||
| block += `* | ||
| `; | ||
| block += `* ${description} | ||
| `; | ||
| if (commentUrl?.startsWith("http")) { | ||
| block += `* | ||
| `; | ||
| block += `* @see {@link ${commentUrl}} | ||
| `; | ||
| } | ||
| block += `*/ | ||
| `; | ||
| return block; | ||
| }; | ||
| var getTypeScriptType = (schema, options = {}) => { | ||
| const base = getUnbrandedType(schema, options); | ||
| const brand = getMjstBrand(schema); | ||
| return brand ? `(${base} & { readonly __brand: '${brand}' })` : base; | ||
| }; | ||
| var recordType = (keyType, valueType, options) => options.readonly ? `Readonly<Record<${keyType}, ${valueType}>>` : `Record<${keyType}, ${valueType}>`; | ||
| var getUnbrandedType = (schema, options = {}) => { | ||
| if (typeof schema === "boolean") { | ||
| return getBooleanSubSchemaType(schema); | ||
| } | ||
| if (typeof schema !== "object" || schema === null) { | ||
| return "unknown"; | ||
| } | ||
| const instanceOf = getMjstInstanceOf(schema); | ||
| if (instanceOf) { | ||
| return instanceOf; | ||
| } | ||
| const primitive = getMjstPrimitive(schema); | ||
| if (primitive) { | ||
| return primitive; | ||
| } | ||
| if (schema.$ref) { | ||
| if (!schema.$ref.startsWith("#")) { | ||
| return "unknown"; | ||
| } | ||
| return refToName2(schema.$ref, options.typeSuffix); | ||
| } | ||
| if (schema.$dynamicRef) { | ||
| if (schema.$dynamicRef === "#meta") { | ||
| return `Schema${options.typeSuffix ?? ""}`; | ||
| } | ||
| return refToName2(schema.$dynamicRef, options.typeSuffix); | ||
| } | ||
| if (schema.const !== undefined) { | ||
| return JSON.stringify(schema.const); | ||
| } | ||
| if (schema.enum && schema.enum.length > 0) { | ||
| if (schema.enum.length === 1) { | ||
| return JSON.stringify(schema.enum[0]); | ||
| } | ||
| let enumUnion = JSON.stringify(schema.enum[0]); | ||
| for (let i = 1;i < schema.enum.length; i++) { | ||
| enumUnion += " | " + JSON.stringify(schema.enum[i]); | ||
| } | ||
| return enumUnion; | ||
| } | ||
| if (schema.enum && schema.enum.length > 1) { | ||
| let multiEnumUnion = JSON.stringify(schema.enum[0]); | ||
| for (let i = 1;i < schema.enum.length; i++) { | ||
| multiEnumUnion += " | " + JSON.stringify(schema.enum[i]); | ||
| } | ||
| return multiEnumUnion; | ||
| } | ||
| if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length > 0) { | ||
| let oneOfUnion = getTypeScriptType(schema.oneOf[0], options); | ||
| for (let i = 1;i < schema.oneOf.length; i++) { | ||
| oneOfUnion += " | " + getTypeScriptType(schema.oneOf[i], options); | ||
| } | ||
| return oneOfUnion; | ||
| } | ||
| if (schema.anyOf && Array.isArray(schema.anyOf) && schema.anyOf.length > 0) { | ||
| let anyOfUnion = getTypeScriptType(schema.anyOf[0], options); | ||
| for (let i = 1;i < schema.anyOf.length; i++) { | ||
| anyOfUnion += " | " + getTypeScriptType(schema.anyOf[i], options); | ||
| } | ||
| return anyOfUnion; | ||
| } | ||
| if (schema.allOf && Array.isArray(schema.allOf) && schema.allOf.length > 0) { | ||
| let intersectionTypes = getTypeScriptType(schema.allOf[0], options); | ||
| for (let i = 1;i < schema.allOf.length; i++) { | ||
| intersectionTypes += " & " + getTypeScriptType(schema.allOf[i], options); | ||
| } | ||
| return intersectionTypes; | ||
| } | ||
| const conditionalResult = getConditionalObjectSchema(schema); | ||
| if (conditionalResult) { | ||
| const baseType = getTypeScriptType(conditionalResult.schema, options); | ||
| if (conditionalResult.thenRef) { | ||
| return `(${baseType}) & ${refToName2(conditionalResult.thenRef, options.typeSuffix)}`; | ||
| } | ||
| return baseType; | ||
| } | ||
| if (!schema.type) { | ||
| if (schema.additionalProperties !== undefined) { | ||
| if (typeof schema.additionalProperties === "boolean") { | ||
| return recordType("string", getBooleanSubSchemaType(schema.additionalProperties), options); | ||
| } | ||
| return recordType("string", getTypeScriptType(schema.additionalProperties, options), options); | ||
| } | ||
| if (schema.patternProperties && typeof schema.patternProperties === "object") { | ||
| const firstEntry = Object.entries(schema.patternProperties)[0]; | ||
| if (firstEntry) { | ||
| const [pattern, value] = firstEntry; | ||
| if (value !== undefined) { | ||
| const valueType = typeof value === "boolean" ? getBooleanSubSchemaType(value) : getTypeScriptType(value, options); | ||
| if (pattern === "^x-") { | ||
| return recordType("`x-${string}`", valueType, options); | ||
| } | ||
| return recordType("string", valueType, options); | ||
| } | ||
| } | ||
| } | ||
| if (schema.default !== undefined) { | ||
| if (typeof schema.default === "string") { | ||
| return "string"; | ||
| } | ||
| if (typeof schema.default === "number") { | ||
| return "number"; | ||
| } | ||
| if (typeof schema.default === "boolean") { | ||
| return "boolean"; | ||
| } | ||
| } | ||
| return "unknown"; | ||
| } | ||
| if (Array.isArray(schema.type)) { | ||
| const mapType = (t) => { | ||
| switch (t) { | ||
| case "string": | ||
| return "string"; | ||
| case "number": | ||
| case "integer": | ||
| return "number"; | ||
| case "boolean": | ||
| return "boolean"; | ||
| case "null": | ||
| return "null"; | ||
| case "array": | ||
| return "unknown[]"; | ||
| case "object": | ||
| return "Record<string, unknown>"; | ||
| default: | ||
| return "unknown"; | ||
| } | ||
| }; | ||
| let typeUnion = mapType(schema.type[0]); | ||
| for (let i = 1;i < schema.type.length; i++) { | ||
| typeUnion += " | " + mapType(schema.type[i]); | ||
| } | ||
| return typeUnion; | ||
| } | ||
| switch (schema.type) { | ||
| case "string": | ||
| return "string"; | ||
| case "number": | ||
| case "integer": | ||
| return "number"; | ||
| case "boolean": | ||
| return "boolean"; | ||
| case "array": | ||
| if (schema.items) { | ||
| const itemType = getTypeScriptType(schema.items, options); | ||
| const wrappedItemType = itemType.includes(" | ") ? `(${itemType})` : itemType; | ||
| return options.readonly ? `readonly ${wrappedItemType}[]` : `${wrappedItemType}[]`; | ||
| } | ||
| return options.readonly ? "readonly unknown[]" : "unknown[]"; | ||
| case "object": | ||
| if (schema.properties) { | ||
| const readonlyPrefix = options.readonly ? "readonly " : ""; | ||
| const hasDescriptions = Object.values(schema.properties).some((p) => isSchemaObject2(p) && (typeof p.description === "string" || typeof p.$comment === "string")); | ||
| if (hasDescriptions) { | ||
| let properties2 = ""; | ||
| let first2 = true; | ||
| for (const key in schema.properties) { | ||
| const propSchema = schema.properties[key]; | ||
| const isRequired = schema.required?.includes(key) ?? false; | ||
| const optional = isRequired ? "" : "?"; | ||
| const propType = getTypeScriptType(propSchema, options); | ||
| const inlineDescription = isSchemaObject2(propSchema) && typeof propSchema.description === "string" ? propSchema.description : isSchemaObject2(propSchema) && typeof propSchema.$comment === "string" ? propSchema.$comment : undefined; | ||
| if (!first2) | ||
| properties2 += ` | ||
| `; | ||
| first2 = false; | ||
| if (inlineDescription) { | ||
| properties2 += " /** " + inlineDescription + ` */ | ||
| ` + readonlyPrefix + safeKey(key) + optional + ": " + propType + ";"; | ||
| } else { | ||
| properties2 += " " + readonlyPrefix + safeKey(key) + optional + ": " + propType + ";"; | ||
| } | ||
| } | ||
| return `{ | ||
| ` + properties2 + ` | ||
| }`; | ||
| } | ||
| let properties = ""; | ||
| let first = true; | ||
| for (const key in schema.properties) { | ||
| const propSchema = schema.properties[key]; | ||
| const isRequired = schema.required?.includes(key) ?? false; | ||
| const optional = isRequired ? "" : "?"; | ||
| const propType = getTypeScriptType(propSchema, options); | ||
| if (!first) | ||
| properties += "; "; | ||
| properties += readonlyPrefix + safeKey(key) + optional + ": " + propType; | ||
| first = false; | ||
| } | ||
| return "{ " + properties + " }"; | ||
| } | ||
| if (schema.additionalProperties && typeof schema.additionalProperties === "object") { | ||
| const additionalPropType = getTypeScriptType(schema.additionalProperties, options); | ||
| return recordType("string", additionalPropType, options); | ||
| } | ||
| if (schema.patternProperties && typeof schema.patternProperties === "object") { | ||
| const firstEntry = Object.entries(schema.patternProperties)[0]; | ||
| if (firstEntry) { | ||
| const [pattern, patternVal] = firstEntry; | ||
| if (patternVal) { | ||
| const valueType = getTypeScriptType(patternVal, options); | ||
| if (pattern === "^x-") { | ||
| return recordType("`x-${string}`", valueType, options); | ||
| } | ||
| return recordType("string", valueType, options); | ||
| } | ||
| } | ||
| } | ||
| return "object"; | ||
| default: | ||
| return "unknown"; | ||
| } | ||
| }; | ||
| var generateTypeDefinition = (schema, typeName, options = {}) => { | ||
| const readonlyPrefix = options.readonly ? "readonly " : ""; | ||
| if (!isObjectLikeSchema(schema)) { | ||
| const tsType = getTypeScriptType(schema, options); | ||
| let result = ""; | ||
| const topLevelComment = isSchemaObject2(schema) && typeof schema.description === "string" && schema.description || isSchemaObject2(schema) && typeof schema.$comment === "string" && schema.$comment || undefined; | ||
| if (topLevelComment) { | ||
| result += buildJsDocBlock(typeName, topLevelComment); | ||
| } | ||
| result += `export type ${typeName} = ${tsType};`; | ||
| return result; | ||
| } | ||
| if (isObjectLikeSchema(schema)) { | ||
| const conditionalResult = getConditionalObjectSchema(schema); | ||
| const normalizedSchema = conditionalResult?.schema ?? schema; | ||
| const conditionalThenRef = conditionalResult?.thenRef ?? null; | ||
| let jsDocTitle; | ||
| let jsDocDescription; | ||
| const topLevelComment = isSchemaObject2(schema) && typeof schema.description === "string" && schema.description || isSchemaObject2(schema) && typeof schema.$comment === "string" && schema.$comment || undefined; | ||
| if (topLevelComment) { | ||
| jsDocTitle = typeName; | ||
| jsDocDescription = topLevelComment; | ||
| } | ||
| const hasProperties2 = normalizedSchema.properties && Object.keys(normalizedSchema.properties).length > 0; | ||
| const hasAdditionalProperties2 = normalizedSchema.additionalProperties && typeof normalizedSchema.additionalProperties === "object"; | ||
| const hasPatternProperties = normalizedSchema.patternProperties && typeof normalizedSchema.patternProperties === "object" && Object.keys(normalizedSchema.patternProperties).length > 0; | ||
| if (!hasProperties2 && hasPatternProperties && normalizedSchema.patternProperties) { | ||
| const firstEntry = Object.entries(normalizedSchema.patternProperties)[0]; | ||
| const firstPattern = firstEntry?.[0]; | ||
| const firstPatternProperty = firstEntry?.[1]; | ||
| if (firstPatternProperty === undefined) { | ||
| return `export type ${typeName} = Record<string, unknown>;`; | ||
| } | ||
| const patternPropType = typeof firstPatternProperty === "boolean" ? getBooleanSubSchemaType(firstPatternProperty) : getTypeScriptType(firstPatternProperty, options); | ||
| const keyType = firstPattern === "^x-" ? "`x-${string}`" : "string"; | ||
| let result2 = ""; | ||
| if (jsDocTitle && jsDocDescription) { | ||
| result2 += buildJsDocBlock(jsDocTitle, jsDocDescription); | ||
| } | ||
| result2 += `export type ${typeName} = ${recordType(keyType, patternPropType, options)};`; | ||
| return result2; | ||
| } | ||
| if (!hasProperties2 && hasAdditionalProperties2 && normalizedSchema.additionalProperties) { | ||
| const additionalPropType = getTypeScriptType(normalizedSchema.additionalProperties, options); | ||
| let result2 = ""; | ||
| if (jsDocTitle && jsDocDescription) { | ||
| result2 += buildJsDocBlock(jsDocTitle, jsDocDescription); | ||
| } | ||
| result2 += `export type ${typeName} = { | ||
| ${readonlyPrefix}[key: string]: ${additionalPropType}; | ||
| };`; | ||
| return result2; | ||
| } | ||
| const schemaProps = normalizedSchema.properties ?? {}; | ||
| let properties = ""; | ||
| let isFirstProp = true; | ||
| for (const key in schemaProps) { | ||
| const propSchema = schemaProps[key]; | ||
| const isRequired = normalizedSchema.required?.includes(key) ?? false; | ||
| const optional = isRequired ? "" : "?"; | ||
| const propType = getTypeScriptType(propSchema, options); | ||
| const quotedKey = readonlyPrefix + safeKey(key); | ||
| if (!isFirstProp) | ||
| properties += ` | ||
| `; | ||
| isFirstProp = false; | ||
| const inlineDescription = isSchemaObject2(propSchema) && typeof propSchema.description === "string" ? propSchema.description : isSchemaObject2(propSchema) && typeof propSchema.$comment === "string" ? propSchema.$comment : undefined; | ||
| if (inlineDescription) { | ||
| properties += " /** " + inlineDescription + ` */ | ||
| ` + quotedKey + optional + ": " + propType + ";"; | ||
| } else { | ||
| properties += " " + quotedKey + optional + ": " + propType + ";"; | ||
| } | ||
| } | ||
| const allOfIntersections = []; | ||
| if (isSchemaObject2(schema) && Array.isArray(schema.allOf)) { | ||
| for (const entry of schema.allOf) { | ||
| if (isSchemaObject2(entry) && entry.$ref) { | ||
| allOfIntersections.push(refToName2(entry.$ref, options.typeSuffix)); | ||
| } | ||
| } | ||
| } | ||
| if (isSchemaObject2(schema) && typeof schema.$ref === "string" && schema.$ref.startsWith("#")) { | ||
| allOfIntersections.push(refToName2(schema.$ref, options.typeSuffix)); | ||
| } | ||
| let result = ""; | ||
| if (jsDocTitle && jsDocDescription) { | ||
| result += buildJsDocBlock(jsDocTitle, jsDocDescription); | ||
| } | ||
| let typeBody = `{ | ||
| ` + properties + ` | ||
| }`; | ||
| if (conditionalThenRef) { | ||
| typeBody += " & " + refToName2(conditionalThenRef, options.typeSuffix); | ||
| } | ||
| for (const intersectionType of allOfIntersections) { | ||
| typeBody += " & " + intersectionType; | ||
| } | ||
| result += "export type " + typeName + " = " + typeBody + ";"; | ||
| return result; | ||
| } | ||
| return "export type " + typeName + " = unknown;"; | ||
| }; | ||
| // ../helpers/dist/schema-guards.js | ||
| var hasRef = (value) => { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value) && "$ref" in value && typeof value.$ref === "string"; | ||
| }; | ||
| var isSchemaObject3 = (schema) => { | ||
| return typeof schema === "object" && schema !== null && typeof schema !== "boolean"; | ||
| }; | ||
| var hasType = (schema) => { | ||
| return isSchemaObject3(schema) && "type" in schema && typeof schema.type === "string"; | ||
| }; | ||
| var hasProperties = (schema) => { | ||
| return isSchemaObject3(schema) && "properties" in schema && typeof schema.properties === "object" && schema.properties !== null; | ||
| }; | ||
| var hasEnum = (schema) => { | ||
| return isSchemaObject3(schema) && "enum" in schema && Array.isArray(schema.enum); | ||
| }; | ||
| var hasConst = (schema) => { | ||
| return isSchemaObject3(schema) && "const" in schema; | ||
| }; | ||
| var hasPattern = (schema) => { | ||
| return isSchemaObject3(schema) && "pattern" in schema && typeof schema.pattern === "string"; | ||
| }; | ||
| var hasFormat = (schema) => { | ||
| return isSchemaObject3(schema) && "format" in schema && typeof schema.format === "string"; | ||
| }; | ||
| var hasDefault = (schema) => { | ||
| return isSchemaObject3(schema) && "default" in schema; | ||
| }; | ||
| var hasExamples = (schema) => { | ||
| return isSchemaObject3(schema) && "examples" in schema && Array.isArray(schema.examples); | ||
| }; | ||
| var hasOneOf = (schema) => { | ||
| return isSchemaObject3(schema) && "oneOf" in schema && Array.isArray(schema.oneOf); | ||
| }; | ||
| var hasAnyOf = (schema) => { | ||
| return isSchemaObject3(schema) && "anyOf" in schema && Array.isArray(schema.anyOf); | ||
| }; | ||
| var hasAllOf = (schema) => { | ||
| return isSchemaObject3(schema) && "allOf" in schema && Array.isArray(schema.allOf); | ||
| }; | ||
| var hasRequired = (schema) => { | ||
| return isSchemaObject3(schema) && "required" in schema && Array.isArray(schema.required); | ||
| }; | ||
| var hasItems = (schema) => { | ||
| return isSchemaObject3(schema) && "items" in schema && typeof schema.items === "object" && schema.items !== null && typeof schema.items !== "boolean"; | ||
| }; | ||
| var hasAdditionalProperties = (schema) => { | ||
| return isSchemaObject3(schema) && "additionalProperties" in schema; | ||
| }; | ||
| var hasMinLength = (schema) => { | ||
| return isSchemaObject3(schema) && "minLength" in schema && typeof schema.minLength === "number"; | ||
| }; | ||
| var hasMaxLength = (schema) => { | ||
| return isSchemaObject3(schema) && "maxLength" in schema && typeof schema.maxLength === "number"; | ||
| }; | ||
| var hasMinimum = (schema) => { | ||
| return isSchemaObject3(schema) && "minimum" in schema && typeof schema.minimum === "number"; | ||
| }; | ||
| var hasMaximum = (schema) => { | ||
| return isSchemaObject3(schema) && "maximum" in schema && typeof schema.maximum === "number"; | ||
| }; | ||
| var hasExclusiveMinimum = (schema) => { | ||
| return isSchemaObject3(schema) && "exclusiveMinimum" in schema && typeof schema.exclusiveMinimum === "number"; | ||
| }; | ||
| var hasExclusiveMaximum = (schema) => { | ||
| return isSchemaObject3(schema) && "exclusiveMaximum" in schema && typeof schema.exclusiveMaximum === "number"; | ||
| }; | ||
| var hasMultipleOf = (schema) => { | ||
| return isSchemaObject3(schema) && "multipleOf" in schema && typeof schema.multipleOf === "number"; | ||
| }; | ||
| var hasMinItems = (schema) => { | ||
| return isSchemaObject3(schema) && "minItems" in schema && typeof schema.minItems === "number"; | ||
| }; | ||
| var hasMaxItems = (schema) => { | ||
| return isSchemaObject3(schema) && "maxItems" in schema && typeof schema.maxItems === "number"; | ||
| }; | ||
| var hasUniqueItems = (schema) => { | ||
| return isSchemaObject3(schema) && "uniqueItems" in schema && typeof schema.uniqueItems === "boolean"; | ||
| }; | ||
| // src/generators/collect-example-imports.ts | ||
| var buildImport = (ref, suffix) => { | ||
| const filename = refToFilename(ref); | ||
| const typeName = refToName(ref, suffix); | ||
| return `import { type ${typeName}, ${typeName}Arbitrary } from './${filename}'`; | ||
| }; | ||
| var collectDirectRefs = (schema) => { | ||
| if (typeof schema === "boolean" || schema === null) | ||
| return []; | ||
| const refs = []; | ||
| if (hasRef(schema)) { | ||
| refs.push(schema.$ref); | ||
| return refs; | ||
| } | ||
| const propSchemas = "properties" in schema && typeof schema.properties === "object" && schema.properties !== null ? Object.values(schema.properties) : []; | ||
| for (const prop of propSchemas) { | ||
| if (hasRef(prop)) | ||
| refs.push(prop.$ref); | ||
| if (hasItems(prop) && hasRef(prop.items)) | ||
| refs.push(prop.items.$ref); | ||
| if (hasAdditionalProperties(prop) && hasRef(prop.additionalProperties)) { | ||
| refs.push(prop.additionalProperties.$ref); | ||
| } | ||
| } | ||
| if (hasItems(schema) && hasRef(schema.items)) { | ||
| refs.push(schema.items.$ref); | ||
| } | ||
| if (hasAdditionalProperties(schema) && hasRef(schema.additionalProperties)) { | ||
| refs.push(schema.additionalProperties.$ref); | ||
| } | ||
| for (const branch of [ | ||
| ...hasOneOf(schema) ? schema.oneOf : [], | ||
| ...hasAnyOf(schema) ? schema.anyOf : [], | ||
| ...hasAllOf(schema) ? schema.allOf : [] | ||
| ]) { | ||
| if (hasRef(branch)) | ||
| refs.push(branch.$ref); | ||
| } | ||
| return refs; | ||
| }; | ||
| var collectExampleImports = (schema, options) => { | ||
| const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null; | ||
| const rootSchema = options?.rootSchema; | ||
| const typeSuffix = options?.typeSuffix ?? ""; | ||
| const refs = collectDirectRefs(schema); | ||
| const seen = new Set; | ||
| const imports = []; | ||
| for (const ref of refs) { | ||
| const filename = refToFilename(ref); | ||
| if (seen.has(filename)) | ||
| continue; | ||
| if (selfFilename && filename === selfFilename) | ||
| continue; | ||
| if (rootSchema) { | ||
| const resolved = resolveRef(ref, rootSchema); | ||
| if (!resolved) | ||
| continue; | ||
| } | ||
| seen.add(filename); | ||
| imports.push(buildImport(ref, typeSuffix)); | ||
| } | ||
| return imports; | ||
| }; | ||
| // ../helpers/dist/mjst-extension.js | ||
| var isSchemaObject4 = (schema) => { | ||
| return typeof schema === "object" && schema !== null && typeof schema !== "boolean"; | ||
| }; | ||
| var MJST_EXTENSION_KEY2 = "x-mjst"; | ||
| var IDENTIFIER2 = /^[A-Za-z_$][A-Za-z0-9_$]*$/; | ||
| var SUPPORTED_PRIMITIVES2 = new Set(["bigint"]); | ||
| var readExtensionString2 = (schema, field) => { | ||
| if (!isSchemaObject4(schema)) | ||
| return; | ||
| const extension = schema[MJST_EXTENSION_KEY2]; | ||
| if (typeof extension !== "object" || extension === null) | ||
| return; | ||
| const value = extension[field]; | ||
| return typeof value === "string" ? value : undefined; | ||
| }; | ||
| var getMjstInstanceOf2 = (schema) => { | ||
| const instanceOf = readExtensionString2(schema, "instanceOf"); | ||
| return instanceOf !== undefined && IDENTIFIER2.test(instanceOf) ? instanceOf : undefined; | ||
| }; | ||
| var getMjstPrimitive2 = (schema) => { | ||
| const primitive = readExtensionString2(schema, "primitive"); | ||
| return primitive !== undefined && SUPPORTED_PRIMITIVES2.has(primitive) ? primitive : undefined; | ||
| }; | ||
| // src/generators/derive-example.ts | ||
| var lowerFirst = (name) => name.charAt(0).toLowerCase() + name.slice(1); | ||
| var exampleName = (typeName) => `${lowerFirst(typeName)}Example`; | ||
| var exampleString = (schema) => { | ||
| if (hasFormat(schema)) { | ||
| switch (schema.format) { | ||
| case "email": | ||
| return "user@example.com"; | ||
| case "uuid": | ||
| return "00000000-0000-0000-0000-000000000000"; | ||
| case "uri": | ||
| case "url": | ||
| return "https://example.com"; | ||
| case "date-time": | ||
| return "1970-01-01T00:00:00.000Z"; | ||
| case "date": | ||
| return "1970-01-01"; | ||
| } | ||
| } | ||
| let value = "string"; | ||
| if (hasMinLength(schema) && value.length < schema.minLength) | ||
| value = value.padEnd(schema.minLength, "x"); | ||
| if (hasMaxLength(schema) && value.length > schema.maxLength) | ||
| value = value.slice(0, schema.maxLength); | ||
| return value; | ||
| }; | ||
| var deriveExample = (schema, rootSchema, seen = new Set) => { | ||
| if (!isSchemaObject3(schema)) | ||
| return null; | ||
| if (hasConst(schema)) | ||
| return schema.const; | ||
| if (hasExamples(schema) && Array.isArray(schema.examples) && schema.examples.length > 0) | ||
| return schema.examples[0]; | ||
| if (hasDefault(schema)) | ||
| return schema.default; | ||
| if (hasEnum(schema) && schema.enum.length > 0) | ||
| return schema.enum[0]; | ||
| if (hasRef(schema)) { | ||
| const ref = schema.$ref; | ||
| if (seen.has(ref) || !rootSchema) | ||
| return null; | ||
| const resolved = resolveRef(ref, rootSchema); | ||
| if (!resolved) | ||
| return null; | ||
| return deriveExample(resolved, rootSchema, new Set([...seen, ref])); | ||
| } | ||
| const instanceOf = getMjstInstanceOf2(schema); | ||
| if (instanceOf === "Date") | ||
| return new Date(0); | ||
| const primitive = getMjstPrimitive2(schema); | ||
| if (primitive === "bigint") | ||
| return 0n; | ||
| if (hasOneOf(schema) && schema.oneOf[0] !== undefined) | ||
| return deriveExample(schema.oneOf[0], rootSchema, seen); | ||
| if (hasAnyOf(schema) && schema.anyOf[0] !== undefined) | ||
| return deriveExample(schema.anyOf[0], rootSchema, seen); | ||
| if (!hasType(schema)) | ||
| return null; | ||
| switch (schema.type) { | ||
| case "string": | ||
| return exampleString(schema); | ||
| case "number": | ||
| case "integer": | ||
| return hasMinimum(schema) ? schema.minimum : 0; | ||
| case "boolean": | ||
| return true; | ||
| case "null": | ||
| return null; | ||
| case "array": { | ||
| const item = hasItems(schema) ? deriveExample(schema.items, rootSchema, seen) : null; | ||
| const count = hasMinItems(schema) ? Math.max(schema.minItems, 1) : 1; | ||
| return Array.from({ length: count }, () => item); | ||
| } | ||
| case "object": { | ||
| const out = {}; | ||
| if (hasProperties(schema)) { | ||
| for (const [key, propSchema] of Object.entries(schema.properties)) { | ||
| out[key] = deriveExample(propSchema, rootSchema, seen); | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
| default: | ||
| return null; | ||
| } | ||
| }; | ||
| var serializeValue = (value) => { | ||
| if (typeof value === "bigint") | ||
| return `${value}n`; | ||
| if (value instanceof Date) | ||
| return `new Date(${JSON.stringify(value.toISOString())})`; | ||
| if (Array.isArray(value)) | ||
| return `[${value.map(serializeValue).join(", ")}]`; | ||
| if (value !== null && typeof value === "object") { | ||
| const entries = Object.entries(value).filter(([, v]) => v !== undefined).map(([key, v]) => `${JSON.stringify(key)}: ${serializeValue(v)}`); | ||
| return `{ ${entries.join(", ")} }`; | ||
| } | ||
| return JSON.stringify(value); | ||
| }; | ||
| var generateExampleConst = (schema, typeName, rootSchema) => { | ||
| const value = deriveExample(schema, rootSchema); | ||
| return `export const ${exampleName(typeName)}: ${typeName} = ${serializeValue(value)}`; | ||
| }; | ||
| // src/generators/generate-arbitrary.ts | ||
| var arbitraryName = (typeName) => `${typeName}Arbitrary`; | ||
| var stringExpr = (schema) => { | ||
| if (hasFormat(schema)) { | ||
| switch (schema.format) { | ||
| case "email": | ||
| return "fc.emailAddress()"; | ||
| case "uuid": | ||
| return "fc.uuid()"; | ||
| case "uri": | ||
| case "url": | ||
| return "fc.webUrl()"; | ||
| case "date-time": | ||
| return "fc.date({ noInvalidDate: true }).map((d) => d.toISOString())"; | ||
| case "date": | ||
| return "fc.date({ noInvalidDate: true }).map((d) => d.toISOString().slice(0, 10))"; | ||
| } | ||
| } | ||
| if (hasPattern(schema)) | ||
| return `fc.stringMatching(/${schema.pattern}/)`; | ||
| const opts = []; | ||
| if (hasMinLength(schema)) | ||
| opts.push(`minLength: ${schema.minLength}`); | ||
| if (hasMaxLength(schema)) | ||
| opts.push(`maxLength: ${schema.maxLength}`); | ||
| return opts.length > 0 ? `fc.string({ ${opts.join(", ")} })` : "fc.string()"; | ||
| }; | ||
| var integerExpr = (schema) => { | ||
| const opts = []; | ||
| if (hasMinimum(schema)) | ||
| opts.push(`min: ${schema.minimum}`); | ||
| else if (hasExclusiveMinimum(schema)) | ||
| opts.push(`min: ${Number(schema.exclusiveMinimum) + 1}`); | ||
| if (hasMaximum(schema)) | ||
| opts.push(`max: ${schema.maximum}`); | ||
| else if (hasExclusiveMaximum(schema)) | ||
| opts.push(`max: ${Number(schema.exclusiveMaximum) - 1}`); | ||
| const base = opts.length > 0 ? `fc.integer({ ${opts.join(", ")} })` : "fc.integer()"; | ||
| return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base; | ||
| }; | ||
| var numberExpr = (schema) => { | ||
| const opts = ["noNaN: true", "noDefaultInfinity: true"]; | ||
| if (hasMinimum(schema)) | ||
| opts.push(`min: ${schema.minimum}`); | ||
| else if (hasExclusiveMinimum(schema)) | ||
| opts.push(`min: ${schema.exclusiveMinimum}`, "minExcluded: true"); | ||
| if (hasMaximum(schema)) | ||
| opts.push(`max: ${schema.maximum}`); | ||
| else if (hasExclusiveMaximum(schema)) | ||
| opts.push(`max: ${schema.exclusiveMaximum}`, "maxExcluded: true"); | ||
| const base = `fc.double({ ${opts.join(", ")} })`; | ||
| return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base; | ||
| }; | ||
| var arrayExpr = (schema, suffix) => { | ||
| const items = hasItems(schema) && isSchemaObject3(schema.items) ? arbitraryExpr(schema.items, suffix) : "fc.anything()"; | ||
| const opts = []; | ||
| if (hasMinItems(schema)) | ||
| opts.push(`minLength: ${schema.minItems}`); | ||
| if (hasMaxItems(schema)) | ||
| opts.push(`maxLength: ${schema.maxItems}`); | ||
| const fn = hasUniqueItems(schema) && schema.uniqueItems === true ? "fc.uniqueArray" : "fc.array"; | ||
| return opts.length > 0 ? `${fn}(${items}, { ${opts.join(", ")} })` : `${fn}(${items})`; | ||
| }; | ||
| var objectExpr = (schema, suffix) => { | ||
| if (!hasProperties(schema)) | ||
| return "fc.object()"; | ||
| const required = new Set(hasRequired(schema) ? schema.required : []); | ||
| const keys = Object.keys(schema.properties); | ||
| const entries = Object.entries(schema.properties).map(([key, propSchema]) => `${JSON.stringify(key)}: ${arbitraryExpr(propSchema, suffix)}`); | ||
| if (entries.length === 0) | ||
| return "fc.record({})"; | ||
| const model = `{ ${entries.join(", ")} }`; | ||
| if (keys.every((key) => required.has(key))) | ||
| return `fc.record(${model})`; | ||
| const requiredKeys = [...required].map((key) => JSON.stringify(key)).join(", "); | ||
| return `fc.record(${model}, { requiredKeys: [${requiredKeys}] })`; | ||
| }; | ||
| var oneofExpr = (branches, suffix) => { | ||
| const exprs = branches.map((branch) => arbitraryExpr(branch, suffix)); | ||
| return `fc.oneof(${exprs.join(", ")})`; | ||
| }; | ||
| var scalarExpr = (type, schema, suffix) => { | ||
| switch (type) { | ||
| case "string": | ||
| return stringExpr(schema); | ||
| case "integer": | ||
| return integerExpr(schema); | ||
| case "number": | ||
| return numberExpr(schema); | ||
| case "boolean": | ||
| return "fc.boolean()"; | ||
| case "null": | ||
| return "fc.constant(null)"; | ||
| case "array": | ||
| return arrayExpr(schema, suffix); | ||
| case "object": | ||
| return objectExpr(schema, suffix); | ||
| default: | ||
| return "fc.anything()"; | ||
| } | ||
| }; | ||
| var arbitraryExpr = (schema, suffix) => { | ||
| if (!isSchemaObject3(schema)) | ||
| return "fc.anything()"; | ||
| if (hasRef(schema)) | ||
| return arbitraryName(refToName(schema.$ref, suffix)); | ||
| if (hasConst(schema)) | ||
| return `fc.constant(${JSON.stringify(schema.const)})`; | ||
| if (hasEnum(schema)) { | ||
| const values = schema.enum.map((value) => JSON.stringify(value)).join(", "); | ||
| return `fc.constantFrom(${values})`; | ||
| } | ||
| const instanceOf = getMjstInstanceOf2(schema); | ||
| if (instanceOf === "Date") | ||
| return "fc.date({ noInvalidDate: true })"; | ||
| if (instanceOf) | ||
| return "fc.anything()"; | ||
| const primitive = getMjstPrimitive2(schema); | ||
| if (primitive === "bigint") | ||
| return "fc.bigInt()"; | ||
| if (primitive) | ||
| return "fc.anything()"; | ||
| if (hasOneOf(schema)) | ||
| return oneofExpr(schema.oneOf, suffix); | ||
| if (hasAnyOf(schema)) | ||
| return oneofExpr(schema.anyOf, suffix); | ||
| if (hasType(schema)) | ||
| return scalarExpr(schema.type, schema, suffix); | ||
| return "fc.anything()"; | ||
| }; | ||
| var generateArbitrary = (schema, typeName, suffix = "") => { | ||
| const expr = arbitraryExpr(schema, suffix); | ||
| return `export const ${arbitraryName(typeName)}: fc.Arbitrary<${typeName}> = ${expr}`; | ||
| }; | ||
| // src/generators/generate-files.ts | ||
| var generateExampleFile = (schema, typeName, options) => { | ||
| const typeSuffix = options?.typeSuffix ?? ""; | ||
| const refImports = collectExampleImports(schema, { | ||
| selfRef: options?.selfRef, | ||
| rootSchema: options?.rootSchema, | ||
| typeSuffix | ||
| }); | ||
| const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix }); | ||
| const arbitrary = generateArbitrary(schema, typeName, typeSuffix); | ||
| const example = generateExampleConst(schema, typeName, options?.rootSchema); | ||
| let result = `import * as fc from 'fast-check' | ||
| `; | ||
| for (const imp of refImports) { | ||
| result += imp + ` | ||
| `; | ||
| } | ||
| result += ` | ||
| `; | ||
| result += typeDefinition + ` | ||
| ` + arbitrary + ` | ||
| ` + example + ` | ||
| `; | ||
| return result; | ||
| }; | ||
| // src/generators/build-schema.ts | ||
| var buildExampleSchema = async (rootSchema, rootTypeName, typeSuffix = "") => { | ||
| rootSchema = upgradeDraft07Schema(rootSchema); | ||
| const files = []; | ||
| const processedRefs = new Set; | ||
| const processedFilenames = new Set; | ||
| const refsToProcess = []; | ||
| const dynamicRefMap = buildDynamicRefMap(rootSchema); | ||
| const processedRootSchema = resolveDynamicRefs(rootSchema, dynamicRefMap); | ||
| const rootContent = generateExampleFile(processedRootSchema, rootTypeName, { | ||
| rootSchema, | ||
| typeSuffix | ||
| }); | ||
| const rootFilename = rootTypeName.toLowerCase(); | ||
| if (rootFilename !== "index") { | ||
| processedFilenames.add(rootFilename); | ||
| files.push({ filename: `${rootFilename}.ts`, content: rootContent }); | ||
| } | ||
| const rootRefs = extractRefs(rootSchema); | ||
| refsToProcess.push(...rootRefs); | ||
| while (refsToProcess.length > 0) { | ||
| const ref = refsToProcess.shift(); | ||
| if (!ref || processedRefs.has(ref)) | ||
| continue; | ||
| processedRefs.add(ref); | ||
| const resolvedSchema = resolveRef(ref, rootSchema); | ||
| if (!resolvedSchema) { | ||
| console.warn(`Warning: Could not resolve ref: ${ref}`); | ||
| continue; | ||
| } | ||
| const typeName = refToName(ref, typeSuffix); | ||
| const filename = refToFilename(ref); | ||
| const processedSchema = resolveDynamicRefs(resolvedSchema, dynamicRefMap); | ||
| const content = generateExampleFile(processedSchema, typeName, { | ||
| selfRef: ref, | ||
| rootSchema, | ||
| typeSuffix | ||
| }); | ||
| if (filename !== "index" && !processedFilenames.has(filename)) { | ||
| processedFilenames.add(filename); | ||
| files.push({ filename: `${filename}.ts`, content }); | ||
| } | ||
| for (const nestedRef of extractRefs(resolvedSchema)) { | ||
| if (!processedRefs.has(nestedRef)) | ||
| refsToProcess.push(nestedRef); | ||
| } | ||
| } | ||
| const TYPE_EXPORT_RE = /^export type (\w+)/gm; | ||
| const CONST_EXPORT_RE = /^export const (\w+)/gm; | ||
| const sortedFiles = [...files].sort((a, b) => a.filename.localeCompare(b.filename)); | ||
| let indexContent = ""; | ||
| for (const file of sortedFiles) { | ||
| const moduleName = file.filename.replace(/\.ts$/, ""); | ||
| const typeNames = []; | ||
| const constNames = []; | ||
| for (const match of file.content.matchAll(TYPE_EXPORT_RE)) | ||
| typeNames.push(match[1]); | ||
| for (const match of file.content.matchAll(CONST_EXPORT_RE)) | ||
| constNames.push(match[1]); | ||
| if (typeNames.length === 0 && constNames.length === 0) | ||
| continue; | ||
| const typeExports = typeNames.map((n) => `type ${n}`); | ||
| indexContent += `export { ${[...typeExports, ...constNames].join(", ")} } from './${moduleName}'; | ||
| `; | ||
| } | ||
| files.push({ filename: "index.ts", content: indexContent }); | ||
| return files; | ||
| }; | ||
| export { | ||
| serializeValue, | ||
| generateExampleConst, | ||
| generateArbitrary, | ||
| deriveExample, | ||
| buildExampleSchema | ||
| }; | ||
| export { buildExampleSchema } from './generators/build-schema.js'; | ||
| export { deriveExample, generateExampleConst, serializeValue } from './generators/derive-example.js'; | ||
| export { generateArbitrary } from './generators/generate-arbitrary.js'; |
+6
-6
| { | ||
| "name": "@amritk/generate-examples", | ||
| "version": "0.1.1", | ||
| "version": "0.2.0", | ||
| "description": "Generate fast-check arbitraries and example values from JSON Schemas.", | ||
@@ -36,5 +36,5 @@ "module": "./dist/index.js", | ||
| "scripts": { | ||
| "build": "bun run build:code && bun run build:types", | ||
| "build:code": "bun build ./src/index.ts --outdir=dist --target=node", | ||
| "build:types": "tsc -p ." | ||
| "build": "tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f", | ||
| "types:check": "tsgo -p . --noEmit", | ||
| "test": "NODE_ENV=production vitest run --root ../.. generate-examples" | ||
| }, | ||
@@ -46,3 +46,3 @@ "imports": { | ||
| ".": { | ||
| "bun": "./src/index.ts", | ||
| "development": "./src/index.ts", | ||
| "default": "./dist/index.js", | ||
@@ -54,3 +54,3 @@ "types": "./dist/index.d.ts" | ||
| "json-schema-typed": "^8.0.1", | ||
| "@amritk/helpers": "0.6.2" | ||
| "@amritk/helpers": "0.7.0" | ||
| }, | ||
@@ -57,0 +57,0 @@ "peerDependencies": { |
@@ -1,8 +0,3 @@ | ||
| import { buildDynamicRefMap } from '@amritk/helpers/build-dynamic-ref-map' | ||
| import { extractRefs } from '@amritk/helpers/extract-refs' | ||
| import { refToFilename } from '@amritk/helpers/ref-to-filename' | ||
| import { refToName } from '@amritk/helpers/ref-to-name' | ||
| import { resolveDynamicRefs } from '@amritk/helpers/resolve-dynamic-refs' | ||
| import { resolveRef } from '@amritk/helpers/resolve-ref' | ||
| import { upgradeDraft07Schema } from '@amritk/helpers/upgrade-draft07-schema' | ||
| import { generateIndexBarrel } from '@amritk/helpers/generate-index-barrel' | ||
| import { walkRefGraph } from '@amritk/helpers/walk-ref-graph' | ||
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12' | ||
@@ -22,3 +17,4 @@ | ||
| * Builds all TypeScript example files from a JSON Schema by traversing all | ||
| * $ref references recursively, mirroring the generate-parsers pipeline. | ||
| * `$ref` / `$dynamicRef` references recursively (via the shared | ||
| * `@amritk/helpers/walk-ref-graph` walker). | ||
| * | ||
@@ -49,81 +45,20 @@ * Each generated file exports: | ||
| ): Promise<GeneratedFile[]> => { | ||
| rootSchema = upgradeDraft07Schema(rootSchema as Record<string, unknown>) as JSONSchema | ||
| const files: GeneratedFile[] = [] | ||
| const processedRefs = new Set<string>() | ||
| const processedFilenames = new Set<string>() | ||
| const refsToProcess: string[] = [] | ||
| const dynamicRefMap = buildDynamicRefMap(rootSchema) | ||
| walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => { | ||
| // `index` is reserved for the barrel below, so never let a definition of | ||
| // that name overwrite it. | ||
| if (node.filename === 'index') return | ||
| // Root schema | ||
| const processedRootSchema = resolveDynamicRefs(rootSchema, dynamicRefMap) | ||
| const rootContent = generateExampleFile(processedRootSchema, rootTypeName, { | ||
| rootSchema: rootSchema as Record<string, unknown>, | ||
| typeSuffix, | ||
| }) | ||
| const rootFilename = rootTypeName.toLowerCase() | ||
| if (rootFilename !== 'index') { | ||
| processedFilenames.add(rootFilename) | ||
| files.push({ filename: `${rootFilename}.ts`, content: rootContent }) | ||
| } | ||
| const rootRefs = extractRefs(rootSchema) | ||
| refsToProcess.push(...rootRefs) | ||
| while (refsToProcess.length > 0) { | ||
| const ref = refsToProcess.shift() | ||
| if (!ref || processedRefs.has(ref)) continue | ||
| processedRefs.add(ref) | ||
| const resolvedSchema = resolveRef(ref, rootSchema as Record<string, unknown>) | ||
| if (!resolvedSchema) { | ||
| console.warn(`Warning: Could not resolve ref: ${ref}`) | ||
| continue | ||
| } | ||
| const typeName = refToName(ref, typeSuffix) | ||
| const filename = refToFilename(ref) | ||
| const processedSchema = resolveDynamicRefs(resolvedSchema as JSONSchema, dynamicRefMap) | ||
| const content = generateExampleFile(processedSchema, typeName, { | ||
| selfRef: ref, | ||
| rootSchema: rootSchema as Record<string, unknown>, | ||
| const content = generateExampleFile(node.schema, node.typeName, { | ||
| rootSchema: node.rootSchema, | ||
| typeSuffix, | ||
| ...(node.ref !== undefined ? { selfRef: node.ref } : {}), | ||
| }) | ||
| files.push({ filename: `${node.filename}.ts`, content }) | ||
| }) | ||
| if (filename !== 'index' && !processedFilenames.has(filename)) { | ||
| processedFilenames.add(filename) | ||
| files.push({ filename: `${filename}.ts`, content }) | ||
| } | ||
| files.push({ filename: 'index.ts', content: generateIndexBarrel(files) }) | ||
| for (const nestedRef of extractRefs(resolvedSchema as JSONSchema)) { | ||
| if (!processedRefs.has(nestedRef)) refsToProcess.push(nestedRef) | ||
| } | ||
| } | ||
| // Generate index.ts barrel | ||
| const TYPE_EXPORT_RE = /^export type (\w+)/gm | ||
| const CONST_EXPORT_RE = /^export const (\w+)/gm | ||
| const sortedFiles = [...files].sort((a, b) => a.filename.localeCompare(b.filename)) | ||
| let indexContent = '' | ||
| for (const file of sortedFiles) { | ||
| const moduleName = file.filename.replace(/\.ts$/, '') | ||
| const typeNames: string[] = [] | ||
| const constNames: string[] = [] | ||
| for (const match of file.content.matchAll(TYPE_EXPORT_RE)) typeNames.push(match[1] as string) | ||
| for (const match of file.content.matchAll(CONST_EXPORT_RE)) constNames.push(match[1] as string) | ||
| if (typeNames.length === 0 && constNames.length === 0) continue | ||
| const typeExports = typeNames.map((n) => `type ${n}`) | ||
| indexContent += `export { ${[...typeExports, ...constNames].join(', ')} } from './${moduleName}';\n` | ||
| } | ||
| files.push({ filename: 'index.ts', content: indexContent }) | ||
| return files | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
29
61.11%66996
-30.14%1371
-39.84%1
Infinity%+ Added
- Removed
Updated