@amritk/helpers
Advanced tools
| {"version":3,"file":"build-dynamic-ref-map.d.ts","sourceRoot":"","sources":["../src/build-dynamic-ref-map.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAIjE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,kBAAkB,eAAgB,UAAU,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAsBhF,CAAA"} |
| {"version":3,"file":"derive-root-type-name.d.ts","sourceRoot":"","sources":["../src/derive-root-type-name.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,kBAAkB,WAAY,OAAO,KAAG,MAOpD,CAAA"} |
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| /** | ||
| * Collects `#/$defs/<key>` refs for every root definition that carries a | ||
| * `$dynamicAnchor`. | ||
| * | ||
| * These definitions are reachable only through `$dynamicRef`, which the ref | ||
| * walker does not follow directly (a `$dynamicRef` is rewritten to a concrete | ||
| * `$ref` by `resolveDynamicRefs` *after* the schema is extracted, so plain | ||
| * `extractRefs` never sees it). Seeding them explicitly guarantees a file is | ||
| * generated for each dynamic-anchor target — without this, a generator would | ||
| * emit code that imports a type whose file was never produced. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // $defs.schema has $dynamicAnchor: "meta" | ||
| * extractDynamicAnchorDefs(rootSchema) // ['#/$defs/schema'] | ||
| * ``` | ||
| */ | ||
| export declare const extractDynamicAnchorDefs: (schema: JSONSchema) => string[]; | ||
| //# sourceMappingURL=extract-dynamic-anchor-defs.d.ts.map |
| {"version":3,"file":"extract-dynamic-anchor-defs.d.ts","sourceRoot":"","sources":["../src/extract-dynamic-anchor-defs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAEjE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,wBAAwB,WAAY,UAAU,KAAG,MAAM,EAanE,CAAA"} |
| /** | ||
| * Collects `#/$defs/<key>` refs for every root definition that carries a | ||
| * `$dynamicAnchor`. | ||
| * | ||
| * These definitions are reachable only through `$dynamicRef`, which the ref | ||
| * walker does not follow directly (a `$dynamicRef` is rewritten to a concrete | ||
| * `$ref` by `resolveDynamicRefs` *after* the schema is extracted, so plain | ||
| * `extractRefs` never sees it). Seeding them explicitly guarantees a file is | ||
| * generated for each dynamic-anchor target — without this, a generator would | ||
| * emit code that imports a type whose file was never produced. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // $defs.schema has $dynamicAnchor: "meta" | ||
| * extractDynamicAnchorDefs(rootSchema) // ['#/$defs/schema'] | ||
| * ``` | ||
| */ | ||
| export const extractDynamicAnchorDefs = (schema) => { | ||
| const refs = []; | ||
| if (typeof schema !== 'object' || schema === null) | ||
| return refs; | ||
| if (!('$defs' in schema) || typeof schema['$defs'] !== 'object' || schema['$defs'] === null) | ||
| return refs; | ||
| for (const [key, value] of Object.entries(schema['$defs'])) { | ||
| if (typeof value === 'object' && value !== null && '$dynamicAnchor' in value) { | ||
| refs.push(`#/$defs/${key}`); | ||
| } | ||
| } | ||
| return refs; | ||
| }; |
| {"version":3,"file":"extract-refs.d.ts","sourceRoot":"","sources":["../src/extract-refs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAsBjE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,WAAW,WAAY,UAAU,KAAG,GAAG,CAAC,MAAM,CA6B1D,CAAA"} |
| /** A generated file: its name (with extension) and TypeScript source. */ | ||
| export type IndexBarrelFile = { | ||
| filename: string; | ||
| content: string; | ||
| }; | ||
| /** Options controlling how the barrel re-exports each module. */ | ||
| export type GenerateIndexBarrelOptions = { | ||
| /** | ||
| * When true, every re-export is type-only (`export type { ... }`). Used by the | ||
| * types-only parser output, where no runtime values exist to re-export. | ||
| * Defaults to `false`. | ||
| */ | ||
| readonly typesOnly?: boolean; | ||
| }; | ||
| /** | ||
| * Builds the `index.ts` barrel that re-exports every generated module. This is | ||
| * the shared version of the near-identical barrel each generator used to build | ||
| * inline: it scans each file's source for `export type` / `export const` | ||
| * declarations and emits one re-export line per module, sorted by filename. | ||
| * | ||
| * Files under `_helpers/` are internal runtime helpers (embedded-mode output) | ||
| * and are never re-exported. Modules that expose nothing are skipped. | ||
| * | ||
| * @param files - The generated files to barrel (the `index.ts` itself excluded). | ||
| * @param options - See {@link GenerateIndexBarrelOptions}. | ||
| * @returns The `index.ts` file content. | ||
| */ | ||
| export declare const generateIndexBarrel: (files: IndexBarrelFile[], options?: GenerateIndexBarrelOptions) => string; | ||
| //# sourceMappingURL=generate-index-barrel.d.ts.map |
| {"version":3,"file":"generate-index-barrel.d.ts","sourceRoot":"","sources":["../src/generate-index-barrel.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,iEAAiE;AACjE,MAAM,MAAM,0BAA0B,GAAG;IACvC;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAC7B,CAAA;AAOD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,UAAW,eAAe,EAAE,YAAW,0BAA0B,KAAQ,MA2BxG,CAAA"} |
| // Generated files declare their public surface with these two forms, so we can | ||
| // recover the export names from the source text without parsing it. | ||
| const TYPE_EXPORT_RE = /^export type (\w+)/gm; | ||
| const CONST_EXPORT_RE = /^export const (\w+)/gm; | ||
| /** | ||
| * Builds the `index.ts` barrel that re-exports every generated module. This is | ||
| * the shared version of the near-identical barrel each generator used to build | ||
| * inline: it scans each file's source for `export type` / `export const` | ||
| * declarations and emits one re-export line per module, sorted by filename. | ||
| * | ||
| * Files under `_helpers/` are internal runtime helpers (embedded-mode output) | ||
| * and are never re-exported. Modules that expose nothing are skipped. | ||
| * | ||
| * @param files - The generated files to barrel (the `index.ts` itself excluded). | ||
| * @param options - See {@link GenerateIndexBarrelOptions}. | ||
| * @returns The `index.ts` file content. | ||
| */ | ||
| export const generateIndexBarrel = (files, options = {}) => { | ||
| const typesOnly = options.typesOnly ?? false; | ||
| const sortedFiles = files | ||
| .filter((file) => !file.filename.startsWith('_helpers/')) | ||
| .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; | ||
| if (typesOnly) { | ||
| indexContent += `export type { ${typeNames.join(', ')} } from './${moduleName}';\n`; | ||
| } | ||
| else { | ||
| const typeExports = typeNames.map((name) => `type ${name}`); | ||
| indexContent += `export { ${[...typeExports, ...constNames].join(', ')} } from './${moduleName}';\n`; | ||
| } | ||
| } | ||
| return indexContent; | ||
| }; |
| {"version":3,"file":"generate-type-definition.d.ts","sourceRoot":"","sources":["../src/generate-type-definition.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAYjE,iDAAiD;AACjD,MAAM,MAAM,WAAW,GAAG;IACxB,kGAAkG;IAClG,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAC3B;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAmZD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,WAAY,UAAU,YAAY,MAAM,YAAW,WAAW,KAAQ,MAsJxG,CAAA"} |
| {"version":3,"file":"has-ref.d.ts","sourceRoot":"","sources":["../src/has-ref.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,eAAO,MAAM,MAAM,UAAW,OAAO,KAAG,KAAK,IAAI;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAQzF,CAAA"} |
| {"version":3,"file":"is-object.d.ts","sourceRoot":"","sources":["../src/is-object.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,QAAQ,UAAW,OAAO,KAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CACV,CAAA"} |
| {"version":3,"file":"mjst-extension.d.ts","sourceRoot":"","sources":["../src/mjst-extension.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAIjE;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,WAAW,CAAA;AAE1C;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AAwBD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,WAAY,UAAU,KAAG,MAAM,GAAG,SAG/D,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,WAAY,UAAU,KAAG,MAAM,GAAG,SAG9D,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,WAAY,UAAU,KAAG,MAAM,GAAG,SAG1D,CAAA"} |
| {"version":3,"file":"parse-documentation.d.ts","sourceRoot":"","sources":["../src/parse-documentation.ts"],"names":[],"mappings":"AAAA,KAAK,qBAAqB,GAAG;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,OAAO,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAA;CAClD,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,0BACN,MAAM,cACjB,MAAM,uBACG,MAAM,KAC1B,mBAAmB,GAAG,IAuJxB,CAAA"} |
| {"version":3,"file":"ref-to-filename.d.ts","sourceRoot":"","sources":["../src/ref-to-filename.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,WAAW,UAAW,MAAM,KAAG,MAM1B,CAAA;AA0DlB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,aAAa,QAAS,MAAM,KAAG,MAiB3C,CAAA"} |
| {"version":3,"file":"ref-to-name.d.ts","sourceRoot":"","sources":["../src/ref-to-name.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,SAAS,QAAS,MAAM,sBAAgB,MAAmD,CAAA"} |
| {"version":3,"file":"resolve-dynamic-refs.d.ts","sourceRoot":"","sources":["../src/resolve-dynamic-refs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAEjE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,WAAY,UAAU,iBAAiB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,UAkC9F,CAAA"} |
| {"version":3,"file":"resolve-ref.d.ts","sourceRoot":"","sources":["../src/resolve-ref.ts"],"names":[],"mappings":"AA0BA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,UAAU,QAAS,MAAM,cAAc,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SA8BvG,CAAA"} |
| {"version":3,"file":"safe-accessor.d.ts","sourceRoot":"","sources":["../src/safe-accessor.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,aAAc,MAAM,OAAO,MAAM,KAAG,MAW5D,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,QAAS,MAAM,KAAG,MAKrC,CAAA"} |
| {"version":3,"file":"schema-guards.d.ts","sourceRoot":"","sources":["../src/schema-guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAEjE,KAAK,YAAY,GAAG,OAAO,CAAC,UAAU,EAAE,KAAK,GAAG,OAAO,CAAC,CAAA;AAExD,iDAAiD;AACjD,eAAO,MAAM,cAAc,WAAY,UAAU,KAAG,MAAM,IAAI,YAE7D,CAAA;AAED,wDAAwD;AACxD,eAAO,MAAM,OAAO,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAEnF,CAAA;AAED,wDAAwD;AACxD,eAAO,MAAM,cAAc,WAAY,UAAU,KAAG,MAAM,IAAI,UAAU,CAAC,MAExE,CAAA;AAED,mDAAmD;AACnD,eAAO,MAAM,aAAa,WAChB,UAAU,KACjB,MAAM,IAAI,YAAY,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;CAOnE,CAAA;AAED,6CAA6C;AAC7C,eAAO,MAAM,OAAO,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;CAE/F,CAAA;AAED,8CAA8C;AAC9C,eAAO,MAAM,QAAQ,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,KAAK,EAAE,OAAO,CAAA;CAEtF,CAAA;AAED,gDAAgD;AAChD,eAAO,MAAM,UAAU,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAEzF,CAAA;AAED,+CAA+C;AAC/C,eAAO,MAAM,SAAS,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAEvF,CAAA;AAED,gDAAgD;AAChD,eAAO,MAAM,UAAU,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAE1F,CAAA;AAED,iDAAiD;AACjD,eAAO,MAAM,WAAW,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAA;CAEvG,CAAA;AAED,8CAA8C;AAC9C,eAAO,MAAM,QAAQ,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,KAAK,EAAE,SAAS,UAAU,EAAE,CAAA;CAEpG,CAAA;AAED,8CAA8C;AAC9C,eAAO,MAAM,QAAQ,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,KAAK,EAAE,SAAS,UAAU,EAAE,CAAA;CAEpG,CAAA;AAED,8CAA8C;AAC9C,eAAO,MAAM,QAAQ,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,KAAK,EAAE,SAAS,UAAU,EAAE,CAAA;CAEpG,CAAA;AAED,iDAAiD;AACjD,eAAO,MAAM,WAAW,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAEtG,CAAA;AAED,0EAA0E;AAC1E,eAAO,MAAM,QAAQ,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,KAAK,EAAE,YAAY,CAAA;CAQ3F,CAAA;AAED,6DAA6D;AAC7D,eAAO,MAAM,uBAAuB,WAC1B,UAAU,KACjB,MAAM,IAAI,YAAY,GAAG;IAAE,oBAAoB,EAAE,UAAU,GAAG,OAAO,CAAA;CAEvE,CAAA;AAED,kDAAkD;AAClD,eAAO,MAAM,YAAY,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAE7F,CAAA;AAED,kDAAkD;AAClD,eAAO,MAAM,YAAY,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAE7F,CAAA;AAED,gDAAgD;AAChD,eAAO,MAAM,UAAU,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAEzF,CAAA;AAED,gDAAgD;AAChD,eAAO,MAAM,UAAU,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAEzF,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,gBAAgB,EAAE,MAAM,CAAA;CAE3G,CAAA;AAED,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,gBAAgB,EAAE,MAAM,CAAA;CAE3G,CAAA;AAED,mDAAmD;AACnD,eAAO,MAAM,aAAa,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAE/F,CAAA;AAED,iDAAiD;AACjD,eAAO,MAAM,WAAW,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAE3F,CAAA;AAED,iDAAiD;AACjD,eAAO,MAAM,WAAW,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAE3F,CAAA;AAED,oDAAoD;AACpD,eAAO,MAAM,cAAc,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,WAAW,EAAE,OAAO,CAAA;CAElG,CAAA;AAED,sDAAsD;AACtD,eAAO,MAAM,gBAAgB,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,aAAa,EAAE,MAAM,CAAA;CAErG,CAAA;AAED,sDAAsD;AACtD,eAAO,MAAM,gBAAgB,WAAY,UAAU,KAAG,MAAM,IAAI,YAAY,GAAG;IAAE,aAAa,EAAE,MAAM,CAAA;CAErG,CAAA;AAED,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA"} |
| {"version":3,"file":"upgrade-draft07-schema.d.ts","sourceRoot":"","sources":["../src/upgrade-draft07-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH;;GAEG;AACH,eAAO,MAAM,eAAe,WAAY,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,OACe,CAAA;AAqFjF;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB,WAAY,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAiC5F,CAAA"} |
| {"version":3,"file":"validate-array.d.ts","sourceRoot":"","sources":["../src/validate-array.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,eAAO,MAAM,aAAa,UAAW,OAAO,UAAU,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,UAahF,CAAA"} |
| {"version":3,"file":"validate-record.d.ts","sourceRoot":"","sources":["../src/validate-record.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,eAAO,MAAM,cAAc,UAAW,OAAO,UAAU,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,4BAYjF,CAAA"} |
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| /** | ||
| * One node of the `$ref` graph, handed to the `visit` callback. Everything a | ||
| * generator needs to emit a single output file is pre-computed here so the | ||
| * traversal, naming, and `$dynamicRef` rewriting live in one place instead of | ||
| * being copy-pasted into every generator. | ||
| */ | ||
| export type RefNode = { | ||
| /** The `$ref` string this node was reached through, or `undefined` for the root schema. */ | ||
| ref: string | undefined; | ||
| /** PascalCase type name — the root type name verbatim, or `refToName(ref, typeSuffix)`. */ | ||
| typeName: string; | ||
| /** kebab-case filename without extension — the lowercased root type name, or `refToFilename(ref)`. */ | ||
| filename: string; | ||
| /** The subschema to generate, with any `$dynamicRef` already rewritten to `$ref`. */ | ||
| schema: JSONSchema; | ||
| /** The upgraded root document, for callers that resolve imports against it. */ | ||
| rootSchema: Record<string, unknown>; | ||
| /** True for the root schema node, which is always visited first. */ | ||
| isRoot: boolean; | ||
| }; | ||
| /** Options controlling how `$ref`-derived names are produced. */ | ||
| export type WalkRefGraphOptions = { | ||
| /** | ||
| * Suffix appended to every `$ref`-derived type name (e.g. `'Object'` → | ||
| * `ContactObject`). The root type name is used verbatim and is not affected. | ||
| * Defaults to `''`. | ||
| */ | ||
| readonly typeSuffix?: string; | ||
| }; | ||
| /** | ||
| * Walks a JSON Schema and its entire `$ref` / `$dynamicRef` graph, invoking | ||
| * `visit` once per distinct output file: first the root, then every reachable | ||
| * definition (breadth-first). This is the single, shared traversal the parser, | ||
| * validator, and example generators were each re-implementing. | ||
| * | ||
| * For every node the walker has already upgraded draft-07 inputs, resolved the | ||
| * ref, rewritten `$dynamicRef` to `$ref`, and derived the type/file names — so | ||
| * callers only have to turn `node.schema` into file content. Definitions | ||
| * reachable only via `$dynamicAnchor` are seeded too, so nothing the generated | ||
| * code imports goes ungenerated. A ref that fails to resolve is reported via | ||
| * `console.warn` and skipped, matching the generators' prior behavior. | ||
| * | ||
| * Resolution work is memoized per root document (see {@link RootCache}), so | ||
| * running several generators over the same loaded schema does the expensive | ||
| * walking once. | ||
| * | ||
| * @param rootSchema - The root JSON Schema to walk. | ||
| * @param rootTypeName - The name for the root type (e.g. `'Document'`). | ||
| * @param options - Naming options ({@link WalkRefGraphOptions}). | ||
| * @param visit - Called once per output file with a fully prepared {@link RefNode}. | ||
| */ | ||
| export declare const walkRefGraph: (rootSchema: JSONSchema, rootTypeName: string, options: WalkRefGraphOptions, visit: (node: RefNode) => void) => void; | ||
| //# sourceMappingURL=walk-ref-graph.d.ts.map |
| {"version":3,"file":"walk-ref-graph.d.ts","sourceRoot":"","sources":["../src/walk-ref-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAWjE;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG;IACpB,2FAA2F;IAC3F,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;IACvB,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,CAAA;IAChB,sGAAsG;IACtG,QAAQ,EAAE,MAAM,CAAA;IAChB,qFAAqF;IACrF,MAAM,EAAE,UAAU,CAAA;IAClB,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACnC,oEAAoE;IACpE,MAAM,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,iEAAiE;AACjE,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AA2DD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,YAAY,eACX,UAAU,gBACR,MAAM,WACX,mBAAmB,SACrB,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,KAC7B,IAyDF,CAAA"} |
| import { buildDynamicRefMap } from './build-dynamic-ref-map.js'; | ||
| import { extractDynamicAnchorDefs } from './extract-dynamic-anchor-defs.js'; | ||
| import { extractRefs } from './extract-refs.js'; | ||
| import { refToFilename } from './ref-to-filename.js'; | ||
| import { refToName } from './ref-to-name.js'; | ||
| import { resolveDynamicRefs } from './resolve-dynamic-refs.js'; | ||
| import { resolveRef } from './resolve-ref.js'; | ||
| import { upgradeDraft07Schema } from './upgrade-draft07-schema.js'; | ||
| const rootCaches = new WeakMap(); | ||
| const getRootCache = (rootSchema) => { | ||
| // Only object roots can key a WeakMap. A boolean root has no refs to walk and | ||
| // the draft-07 upgrade is a no-op for it, so a throwaway cache is fine. | ||
| if (typeof rootSchema !== 'object' || rootSchema === null) { | ||
| const upgraded = rootSchema; | ||
| return { upgraded, dynamicRefMap: {}, resolveRefCache: new Map(), extractRefsCache: new WeakMap() }; | ||
| } | ||
| const existing = rootCaches.get(rootSchema); | ||
| if (existing) | ||
| return existing; | ||
| const upgraded = upgradeDraft07Schema(rootSchema); | ||
| const cache = { | ||
| upgraded, | ||
| dynamicRefMap: buildDynamicRefMap(upgraded), | ||
| resolveRefCache: new Map(), | ||
| extractRefsCache: new WeakMap(), | ||
| }; | ||
| rootCaches.set(rootSchema, cache); | ||
| return cache; | ||
| }; | ||
| /** Memoized `resolveRef` keyed by ref string within a single root document. */ | ||
| const cachedResolveRef = (cache, ref) => { | ||
| if (cache.resolveRefCache.has(ref)) | ||
| return cache.resolveRefCache.get(ref); | ||
| const resolved = resolveRef(ref, cache.upgraded); | ||
| cache.resolveRefCache.set(ref, resolved); | ||
| return resolved; | ||
| }; | ||
| /** Memoized `extractRefs` keyed by the (stable) resolved subschema identity. */ | ||
| const cachedExtractRefs = (cache, schema) => { | ||
| if (typeof schema !== 'object' || schema === null) | ||
| return extractRefs(schema); | ||
| const existing = cache.extractRefsCache.get(schema); | ||
| if (existing) | ||
| return existing; | ||
| const refs = extractRefs(schema); | ||
| cache.extractRefsCache.set(schema, refs); | ||
| return refs; | ||
| }; | ||
| /** | ||
| * Walks a JSON Schema and its entire `$ref` / `$dynamicRef` graph, invoking | ||
| * `visit` once per distinct output file: first the root, then every reachable | ||
| * definition (breadth-first). This is the single, shared traversal the parser, | ||
| * validator, and example generators were each re-implementing. | ||
| * | ||
| * For every node the walker has already upgraded draft-07 inputs, resolved the | ||
| * ref, rewritten `$dynamicRef` to `$ref`, and derived the type/file names — so | ||
| * callers only have to turn `node.schema` into file content. Definitions | ||
| * reachable only via `$dynamicAnchor` are seeded too, so nothing the generated | ||
| * code imports goes ungenerated. A ref that fails to resolve is reported via | ||
| * `console.warn` and skipped, matching the generators' prior behavior. | ||
| * | ||
| * Resolution work is memoized per root document (see {@link RootCache}), so | ||
| * running several generators over the same loaded schema does the expensive | ||
| * walking once. | ||
| * | ||
| * @param rootSchema - The root JSON Schema to walk. | ||
| * @param rootTypeName - The name for the root type (e.g. `'Document'`). | ||
| * @param options - Naming options ({@link WalkRefGraphOptions}). | ||
| * @param visit - Called once per output file with a fully prepared {@link RefNode}. | ||
| */ | ||
| export const walkRefGraph = (rootSchema, rootTypeName, options, visit) => { | ||
| const typeSuffix = options.typeSuffix ?? ''; | ||
| const cache = getRootCache(rootSchema); | ||
| const { upgraded, dynamicRefMap } = cache; | ||
| const processedRefs = new Set(); | ||
| const processedFilenames = new Set(); | ||
| // Root node first — its filename reserves a slot so a later ref that maps to | ||
| // the same name does not emit a duplicate file. | ||
| const rootFilename = rootTypeName.toLowerCase(); | ||
| processedFilenames.add(rootFilename); | ||
| visit({ | ||
| ref: undefined, | ||
| typeName: rootTypeName, | ||
| filename: rootFilename, | ||
| schema: resolveDynamicRefs(upgraded, dynamicRefMap), | ||
| rootSchema: upgraded, | ||
| isRoot: true, | ||
| }); | ||
| const queue = [ | ||
| ...cachedExtractRefs(cache, upgraded), | ||
| ...extractDynamicAnchorDefs(upgraded), | ||
| ]; | ||
| while (queue.length > 0) { | ||
| const ref = queue.shift(); | ||
| if (!ref || processedRefs.has(ref)) | ||
| continue; | ||
| processedRefs.add(ref); | ||
| const resolved = cachedResolveRef(cache, ref); | ||
| if (!resolved) { | ||
| console.warn(`Warning: Could not resolve ref: ${ref}`); | ||
| continue; | ||
| } | ||
| const filename = refToFilename(ref); | ||
| if (!processedFilenames.has(filename)) { | ||
| processedFilenames.add(filename); | ||
| visit({ | ||
| ref, | ||
| typeName: refToName(ref, typeSuffix), | ||
| filename, | ||
| schema: resolveDynamicRefs(resolved, dynamicRefMap), | ||
| rootSchema: upgraded, | ||
| isRoot: false, | ||
| }); | ||
| } | ||
| // Always queue nested refs from the resolved schema, even when its file was | ||
| // a duplicate: two ref strings can share a filename yet reach different | ||
| // sub-definitions (e.g. a URI key and its short-name alias). | ||
| for (const nested of cachedExtractRefs(cache, resolved)) { | ||
| if (!processedRefs.has(nested)) | ||
| queue.push(nested); | ||
| } | ||
| } | ||
| }; |
| import { describe, expect, it } from 'vitest' | ||
| import { extractDynamicAnchorDefs } from './extract-dynamic-anchor-defs' | ||
| describe('extract-dynamic-anchor-defs', () => { | ||
| it('collects refs for every $defs entry with a $dynamicAnchor', () => { | ||
| const schema = { | ||
| $defs: { | ||
| schema: { $dynamicAnchor: 'meta', type: 'object' }, | ||
| contact: { type: 'object' }, | ||
| node: { $dynamicAnchor: 'node', type: 'string' }, | ||
| }, | ||
| } | ||
| expect(extractDynamicAnchorDefs(schema)).toEqual(['#/$defs/schema', '#/$defs/node']) | ||
| }) | ||
| it('returns an empty array when no $defs are present', () => { | ||
| expect(extractDynamicAnchorDefs({ type: 'object' })).toEqual([]) | ||
| }) | ||
| it('returns an empty array when no definition has a $dynamicAnchor', () => { | ||
| expect(extractDynamicAnchorDefs({ $defs: { a: { type: 'string' } } })).toEqual([]) | ||
| }) | ||
| it('ignores a non-object schema', () => { | ||
| expect(extractDynamicAnchorDefs(true)).toEqual([]) | ||
| }) | ||
| }) |
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12' | ||
| /** | ||
| * Collects `#/$defs/<key>` refs for every root definition that carries a | ||
| * `$dynamicAnchor`. | ||
| * | ||
| * These definitions are reachable only through `$dynamicRef`, which the ref | ||
| * walker does not follow directly (a `$dynamicRef` is rewritten to a concrete | ||
| * `$ref` by `resolveDynamicRefs` *after* the schema is extracted, so plain | ||
| * `extractRefs` never sees it). Seeding them explicitly guarantees a file is | ||
| * generated for each dynamic-anchor target — without this, a generator would | ||
| * emit code that imports a type whose file was never produced. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * // $defs.schema has $dynamicAnchor: "meta" | ||
| * extractDynamicAnchorDefs(rootSchema) // ['#/$defs/schema'] | ||
| * ``` | ||
| */ | ||
| export const extractDynamicAnchorDefs = (schema: JSONSchema): string[] => { | ||
| const refs: string[] = [] | ||
| if (typeof schema !== 'object' || schema === null) return refs | ||
| if (!('$defs' in schema) || typeof schema['$defs'] !== 'object' || schema['$defs'] === null) return refs | ||
| for (const [key, value] of Object.entries(schema['$defs'])) { | ||
| if (typeof value === 'object' && value !== null && '$dynamicAnchor' in value) { | ||
| refs.push(`#/$defs/${key}`) | ||
| } | ||
| } | ||
| return refs | ||
| } |
| import { describe, expect, it } from 'vitest' | ||
| import { generateIndexBarrel } from './generate-index-barrel' | ||
| describe('generate-index-barrel', () => { | ||
| it('re-exports types and consts, sorted by filename', () => { | ||
| const files = [ | ||
| { filename: 'contact.ts', content: 'export type Contact = {}\nexport const parseContact = () => {}\n' }, | ||
| { filename: 'address.ts', content: 'export type Address = {}\nexport const parseAddress = () => {}\n' }, | ||
| ] | ||
| expect(generateIndexBarrel(files)).toBe( | ||
| "export { type Address, parseAddress } from './address';\n" + | ||
| "export { type Contact, parseContact } from './contact';\n", | ||
| ) | ||
| }) | ||
| it('emits type-only re-exports when typesOnly is set', () => { | ||
| const files = [{ filename: 'contact.ts', content: 'export type Contact = {}\n' }] | ||
| expect(generateIndexBarrel(files, { typesOnly: true })).toBe("export type { Contact } from './contact';\n") | ||
| }) | ||
| it('never re-exports internal _helpers modules', () => { | ||
| const files = [ | ||
| { filename: 'document.ts', content: 'export type Document = {}\n' }, | ||
| { filename: '_helpers/is-object.ts', content: 'export const isObject = () => {}\n' }, | ||
| ] | ||
| expect(generateIndexBarrel(files)).toBe("export { type Document } from './document';\n") | ||
| }) | ||
| it('skips files that export nothing', () => { | ||
| const files = [ | ||
| { filename: 'document.ts', content: 'export type Document = {}\n' }, | ||
| { filename: 'empty.ts', content: '// nothing here\n' }, | ||
| ] | ||
| expect(generateIndexBarrel(files)).toBe("export { type Document } from './document';\n") | ||
| }) | ||
| }) |
| /** A generated file: its name (with extension) and TypeScript source. */ | ||
| export type IndexBarrelFile = { | ||
| filename: string | ||
| content: string | ||
| } | ||
| /** Options controlling how the barrel re-exports each module. */ | ||
| export type GenerateIndexBarrelOptions = { | ||
| /** | ||
| * When true, every re-export is type-only (`export type { ... }`). Used by the | ||
| * types-only parser output, where no runtime values exist to re-export. | ||
| * Defaults to `false`. | ||
| */ | ||
| readonly typesOnly?: boolean | ||
| } | ||
| // Generated files declare their public surface with these two forms, so we can | ||
| // recover the export names from the source text without parsing it. | ||
| const TYPE_EXPORT_RE = /^export type (\w+)/gm | ||
| const CONST_EXPORT_RE = /^export const (\w+)/gm | ||
| /** | ||
| * Builds the `index.ts` barrel that re-exports every generated module. This is | ||
| * the shared version of the near-identical barrel each generator used to build | ||
| * inline: it scans each file's source for `export type` / `export const` | ||
| * declarations and emits one re-export line per module, sorted by filename. | ||
| * | ||
| * Files under `_helpers/` are internal runtime helpers (embedded-mode output) | ||
| * and are never re-exported. Modules that expose nothing are skipped. | ||
| * | ||
| * @param files - The generated files to barrel (the `index.ts` itself excluded). | ||
| * @param options - See {@link GenerateIndexBarrelOptions}. | ||
| * @returns The `index.ts` file content. | ||
| */ | ||
| export const generateIndexBarrel = (files: IndexBarrelFile[], options: GenerateIndexBarrelOptions = {}): string => { | ||
| const typesOnly = options.typesOnly ?? false | ||
| const sortedFiles = files | ||
| .filter((file) => !file.filename.startsWith('_helpers/')) | ||
| .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 | ||
| if (typesOnly) { | ||
| indexContent += `export type { ${typeNames.join(', ')} } from './${moduleName}';\n` | ||
| } else { | ||
| const typeExports = typeNames.map((name) => `type ${name}`) | ||
| indexContent += `export { ${[...typeExports, ...constNames].join(', ')} } from './${moduleName}';\n` | ||
| } | ||
| } | ||
| return indexContent | ||
| } |
| import { describe, expect, it, vi } from 'vitest' | ||
| import { type RefNode, walkRefGraph } from './walk-ref-graph' | ||
| /** Collects every visited node so a test can assert what the walker produced. */ | ||
| const collect = (rootSchema: Parameters<typeof walkRefGraph>[0], rootTypeName: string, typeSuffix = ''): RefNode[] => { | ||
| const nodes: RefNode[] = [] | ||
| walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => nodes.push(node)) | ||
| return nodes | ||
| } | ||
| describe('walk-ref-graph', () => { | ||
| it('visits the root first, then each referenced definition', () => { | ||
| const schema = { | ||
| type: 'object', | ||
| properties: { contact: { $ref: '#/$defs/contact' } }, | ||
| $defs: { contact: { type: 'object', properties: { email: { type: 'string' } } } }, | ||
| } | ||
| const nodes = collect(schema, 'Document') | ||
| expect(nodes.map((n) => ({ ref: n.ref, typeName: n.typeName, filename: n.filename, isRoot: n.isRoot }))).toEqual([ | ||
| { ref: undefined, typeName: 'Document', filename: 'document', isRoot: true }, | ||
| { ref: '#/$defs/contact', typeName: 'Contact', filename: 'contact', isRoot: false }, | ||
| ]) | ||
| }) | ||
| it('follows nested refs breadth-first and resolves each schema', () => { | ||
| const schema = { | ||
| properties: { user: { $ref: '#/$defs/user' } }, | ||
| $defs: { | ||
| user: { type: 'object', properties: { address: { $ref: '#/$defs/address' } } }, | ||
| address: { type: 'object', properties: { city: { type: 'string' } } }, | ||
| }, | ||
| } | ||
| const nodes = collect(schema, 'Document') | ||
| expect(nodes.map((n) => n.filename)).toEqual(['document', 'user', 'address']) | ||
| expect(nodes[2]?.schema).toEqual({ type: 'object', properties: { city: { type: 'string' } } }) | ||
| }) | ||
| it('applies the type suffix to ref-derived names but not the root', () => { | ||
| const schema = { properties: { contact: { $ref: '#/$defs/contact' } }, $defs: { contact: { type: 'object' } } } | ||
| const nodes = collect(schema, 'Document', 'Object') | ||
| expect(nodes.map((n) => n.typeName)).toEqual(['Document', 'ContactObject']) | ||
| }) | ||
| it('does not visit two refs that map to the same filename twice', () => { | ||
| // A URI key and its short-name alias both resolve to the same definition and | ||
| // share a filename, so only one file should be emitted. | ||
| const schema = { | ||
| allOf: [{ $ref: 'http://example.com/channel.json' }, { $ref: '#/$defs/channel' }], | ||
| $defs: { | ||
| 'http://example.com/channel.json': { type: 'object' }, | ||
| channel: { type: 'object' }, | ||
| }, | ||
| } | ||
| const filenames = collect(schema, 'Document').map((n) => n.filename) | ||
| expect(filenames.filter((f) => f === 'channel')).toHaveLength(1) | ||
| }) | ||
| it('seeds $dynamicAnchor definitions and rewrites $dynamicRef to $ref', () => { | ||
| const schema = { | ||
| type: 'object', | ||
| properties: { payload: { $dynamicRef: '#meta' } }, | ||
| $defs: { schema: { $dynamicAnchor: 'meta', type: 'object' } }, | ||
| } | ||
| const nodes = collect(schema, 'Document') | ||
| // The dynamic-anchor target gets its own file even though no plain $ref points at it... | ||
| expect(nodes.map((n) => n.filename)).toContain('schema') | ||
| // ...and the $dynamicRef on the root is rewritten to a concrete $ref. | ||
| const root = nodes.find((n) => n.isRoot) | ||
| expect(root?.schema).toMatchObject({ properties: { payload: { $ref: '#/$defs/schema' } } }) | ||
| }) | ||
| it('warns and skips a ref that cannot be resolved', () => { | ||
| const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) | ||
| const schema = { properties: { missing: { $ref: '#/$defs/nope' } }, $defs: {} } | ||
| const nodes = collect(schema, 'Document') | ||
| expect(nodes.map((n) => n.filename)).toEqual(['document']) | ||
| expect(warn).toHaveBeenCalledWith('Warning: Could not resolve ref: #/$defs/nope') | ||
| warn.mockRestore() | ||
| }) | ||
| it('reuses cached resolution across repeated walks of the same schema object', () => { | ||
| // The second walk should produce the same nodes from the per-root cache, | ||
| // proving the cache does not corrupt or drop results on reuse. | ||
| const schema = { properties: { contact: { $ref: '#/$defs/contact' } }, $defs: { contact: { type: 'object' } } } | ||
| const first = collect(schema, 'Document').map((n) => n.filename) | ||
| const second = collect(schema, 'Document').map((n) => n.filename) | ||
| expect(second).toEqual(first) | ||
| }) | ||
| }) |
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12' | ||
| import { buildDynamicRefMap } from './build-dynamic-ref-map' | ||
| import { extractDynamicAnchorDefs } from './extract-dynamic-anchor-defs' | ||
| import { extractRefs } from './extract-refs' | ||
| import { refToFilename } from './ref-to-filename' | ||
| import { refToName } from './ref-to-name' | ||
| import { resolveDynamicRefs } from './resolve-dynamic-refs' | ||
| import { resolveRef } from './resolve-ref' | ||
| import { upgradeDraft07Schema } from './upgrade-draft07-schema' | ||
| /** | ||
| * One node of the `$ref` graph, handed to the `visit` callback. Everything a | ||
| * generator needs to emit a single output file is pre-computed here so the | ||
| * traversal, naming, and `$dynamicRef` rewriting live in one place instead of | ||
| * being copy-pasted into every generator. | ||
| */ | ||
| export type RefNode = { | ||
| /** The `$ref` string this node was reached through, or `undefined` for the root schema. */ | ||
| ref: string | undefined | ||
| /** PascalCase type name — the root type name verbatim, or `refToName(ref, typeSuffix)`. */ | ||
| typeName: string | ||
| /** kebab-case filename without extension — the lowercased root type name, or `refToFilename(ref)`. */ | ||
| filename: string | ||
| /** The subschema to generate, with any `$dynamicRef` already rewritten to `$ref`. */ | ||
| schema: JSONSchema | ||
| /** The upgraded root document, for callers that resolve imports against it. */ | ||
| rootSchema: Record<string, unknown> | ||
| /** True for the root schema node, which is always visited first. */ | ||
| isRoot: boolean | ||
| } | ||
| /** Options controlling how `$ref`-derived names are produced. */ | ||
| export type WalkRefGraphOptions = { | ||
| /** | ||
| * Suffix appended to every `$ref`-derived type name (e.g. `'Object'` → | ||
| * `ContactObject`). The root type name is used verbatim and is not affected. | ||
| * Defaults to `''`. | ||
| */ | ||
| readonly typeSuffix?: string | ||
| } | ||
| /** | ||
| * The reusable, schema-scoped work the walker memoizes. Keyed by the *original* | ||
| * root schema object so repeated walks of the same document — the parsers, | ||
| * validators, and examples generators all running over one loaded schema — | ||
| * pay for the draft-07 upgrade, the dynamic-ref map, and each `resolveRef` / | ||
| * `extractRefs` exactly once. JSON Schema inputs are treated as immutable here; | ||
| * the `WeakMap` drops the entry once the caller releases the schema. | ||
| */ | ||
| type RootCache = { | ||
| upgraded: Record<string, unknown> | ||
| dynamicRefMap: Record<string, string> | ||
| resolveRefCache: Map<string, Record<string, unknown> | undefined> | ||
| extractRefsCache: WeakMap<object, Set<string>> | ||
| } | ||
| const rootCaches = new WeakMap<object, RootCache>() | ||
| const getRootCache = (rootSchema: JSONSchema): RootCache => { | ||
| // Only object roots can key a WeakMap. A boolean root has no refs to walk and | ||
| // the draft-07 upgrade is a no-op for it, so a throwaway cache is fine. | ||
| if (typeof rootSchema !== 'object' || rootSchema === null) { | ||
| const upgraded = rootSchema as unknown as Record<string, unknown> | ||
| return { upgraded, dynamicRefMap: {}, resolveRefCache: new Map(), extractRefsCache: new WeakMap() } | ||
| } | ||
| const existing = rootCaches.get(rootSchema) | ||
| if (existing) return existing | ||
| const upgraded = upgradeDraft07Schema(rootSchema as Record<string, unknown>) | ||
| const cache: RootCache = { | ||
| upgraded, | ||
| dynamicRefMap: buildDynamicRefMap(upgraded as JSONSchema), | ||
| resolveRefCache: new Map(), | ||
| extractRefsCache: new WeakMap(), | ||
| } | ||
| rootCaches.set(rootSchema, cache) | ||
| return cache | ||
| } | ||
| /** Memoized `resolveRef` keyed by ref string within a single root document. */ | ||
| const cachedResolveRef = (cache: RootCache, ref: string): Record<string, unknown> | undefined => { | ||
| if (cache.resolveRefCache.has(ref)) return cache.resolveRefCache.get(ref) | ||
| const resolved = resolveRef(ref, cache.upgraded) | ||
| cache.resolveRefCache.set(ref, resolved) | ||
| return resolved | ||
| } | ||
| /** Memoized `extractRefs` keyed by the (stable) resolved subschema identity. */ | ||
| const cachedExtractRefs = (cache: RootCache, schema: JSONSchema): Set<string> => { | ||
| if (typeof schema !== 'object' || schema === null) return extractRefs(schema) | ||
| const existing = cache.extractRefsCache.get(schema) | ||
| if (existing) return existing | ||
| const refs = extractRefs(schema) | ||
| cache.extractRefsCache.set(schema, refs) | ||
| return refs | ||
| } | ||
| /** | ||
| * Walks a JSON Schema and its entire `$ref` / `$dynamicRef` graph, invoking | ||
| * `visit` once per distinct output file: first the root, then every reachable | ||
| * definition (breadth-first). This is the single, shared traversal the parser, | ||
| * validator, and example generators were each re-implementing. | ||
| * | ||
| * For every node the walker has already upgraded draft-07 inputs, resolved the | ||
| * ref, rewritten `$dynamicRef` to `$ref`, and derived the type/file names — so | ||
| * callers only have to turn `node.schema` into file content. Definitions | ||
| * reachable only via `$dynamicAnchor` are seeded too, so nothing the generated | ||
| * code imports goes ungenerated. A ref that fails to resolve is reported via | ||
| * `console.warn` and skipped, matching the generators' prior behavior. | ||
| * | ||
| * Resolution work is memoized per root document (see {@link RootCache}), so | ||
| * running several generators over the same loaded schema does the expensive | ||
| * walking once. | ||
| * | ||
| * @param rootSchema - The root JSON Schema to walk. | ||
| * @param rootTypeName - The name for the root type (e.g. `'Document'`). | ||
| * @param options - Naming options ({@link WalkRefGraphOptions}). | ||
| * @param visit - Called once per output file with a fully prepared {@link RefNode}. | ||
| */ | ||
| export const walkRefGraph = ( | ||
| rootSchema: JSONSchema, | ||
| rootTypeName: string, | ||
| options: WalkRefGraphOptions, | ||
| visit: (node: RefNode) => void, | ||
| ): void => { | ||
| const typeSuffix = options.typeSuffix ?? '' | ||
| const cache = getRootCache(rootSchema) | ||
| const { upgraded, dynamicRefMap } = cache | ||
| const processedRefs = new Set<string>() | ||
| const processedFilenames = new Set<string>() | ||
| // Root node first — its filename reserves a slot so a later ref that maps to | ||
| // the same name does not emit a duplicate file. | ||
| const rootFilename = rootTypeName.toLowerCase() | ||
| processedFilenames.add(rootFilename) | ||
| visit({ | ||
| ref: undefined, | ||
| typeName: rootTypeName, | ||
| filename: rootFilename, | ||
| schema: resolveDynamicRefs(upgraded as JSONSchema, dynamicRefMap), | ||
| rootSchema: upgraded, | ||
| isRoot: true, | ||
| }) | ||
| const queue: string[] = [ | ||
| ...cachedExtractRefs(cache, upgraded as JSONSchema), | ||
| ...extractDynamicAnchorDefs(upgraded as JSONSchema), | ||
| ] | ||
| while (queue.length > 0) { | ||
| const ref = queue.shift() | ||
| if (!ref || processedRefs.has(ref)) continue | ||
| processedRefs.add(ref) | ||
| const resolved = cachedResolveRef(cache, ref) | ||
| if (!resolved) { | ||
| console.warn(`Warning: Could not resolve ref: ${ref}`) | ||
| continue | ||
| } | ||
| const filename = refToFilename(ref) | ||
| if (!processedFilenames.has(filename)) { | ||
| processedFilenames.add(filename) | ||
| visit({ | ||
| ref, | ||
| typeName: refToName(ref, typeSuffix), | ||
| filename, | ||
| schema: resolveDynamicRefs(resolved as JSONSchema, dynamicRefMap), | ||
| rootSchema: upgraded, | ||
| isRoot: false, | ||
| }) | ||
| } | ||
| // Always queue nested refs from the resolved schema, even when its file was | ||
| // a duplicate: two ref strings can share a filename yet reach different | ||
| // sub-definitions (e.g. a URI key and its short-name alias). | ||
| for (const nested of cachedExtractRefs(cache, resolved as JSONSchema)) { | ||
| if (!processedRefs.has(nested)) queue.push(nested) | ||
| } | ||
| } | ||
| } |
@@ -18,1 +18,2 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export declare const buildDynamicRefMap: (rootSchema: JSONSchema) => Record<string, string>; | ||
| //# sourceMappingURL=build-dynamic-ref-map.d.ts.map |
@@ -1,109 +0,33 @@ | ||
| // src/has-ref.ts | ||
| var hasRef = (value) => { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value) && "$ref" in value && typeof value.$ref === "string"; | ||
| }; | ||
| // src/schema-guards.ts | ||
| var isSchemaObject = (schema) => { | ||
| return typeof schema === "object" && schema !== null && typeof schema !== "boolean"; | ||
| }; | ||
| var hasType = (schema) => { | ||
| return isSchemaObject(schema) && "type" in schema && typeof schema.type === "string"; | ||
| }; | ||
| var isObjectSchema = (schema) => { | ||
| return isSchemaObject(schema) && (("type" in schema) && schema.type === "object" || ("properties" in schema)); | ||
| }; | ||
| var hasProperties = (schema) => { | ||
| return isSchemaObject(schema) && "properties" in schema && typeof schema.properties === "object" && schema.properties !== null; | ||
| }; | ||
| var hasEnum = (schema) => { | ||
| return isSchemaObject(schema) && "enum" in schema && Array.isArray(schema.enum); | ||
| }; | ||
| var hasConst = (schema) => { | ||
| return isSchemaObject(schema) && "const" in schema; | ||
| }; | ||
| var hasPattern = (schema) => { | ||
| return isSchemaObject(schema) && "pattern" in schema && typeof schema.pattern === "string"; | ||
| }; | ||
| var hasFormat = (schema) => { | ||
| return isSchemaObject(schema) && "format" in schema && typeof schema.format === "string"; | ||
| }; | ||
| var hasDefault = (schema) => { | ||
| return isSchemaObject(schema) && "default" in schema; | ||
| }; | ||
| var hasExamples = (schema) => { | ||
| return isSchemaObject(schema) && "examples" in schema && Array.isArray(schema.examples); | ||
| }; | ||
| var hasOneOf = (schema) => { | ||
| return isSchemaObject(schema) && "oneOf" in schema && Array.isArray(schema.oneOf); | ||
| }; | ||
| var hasAnyOf = (schema) => { | ||
| return isSchemaObject(schema) && "anyOf" in schema && Array.isArray(schema.anyOf); | ||
| }; | ||
| var hasAllOf = (schema) => { | ||
| return isSchemaObject(schema) && "allOf" in schema && Array.isArray(schema.allOf); | ||
| }; | ||
| var hasRequired = (schema) => { | ||
| return isSchemaObject(schema) && "required" in schema && Array.isArray(schema.required); | ||
| }; | ||
| var hasItems = (schema) => { | ||
| return isSchemaObject(schema) && "items" in schema && typeof schema.items === "object" && schema.items !== null && typeof schema.items !== "boolean"; | ||
| }; | ||
| var hasAdditionalProperties = (schema) => { | ||
| return isSchemaObject(schema) && "additionalProperties" in schema; | ||
| }; | ||
| var hasMinLength = (schema) => { | ||
| return isSchemaObject(schema) && "minLength" in schema && typeof schema.minLength === "number"; | ||
| }; | ||
| var hasMaxLength = (schema) => { | ||
| return isSchemaObject(schema) && "maxLength" in schema && typeof schema.maxLength === "number"; | ||
| }; | ||
| var hasMinimum = (schema) => { | ||
| return isSchemaObject(schema) && "minimum" in schema && typeof schema.minimum === "number"; | ||
| }; | ||
| var hasMaximum = (schema) => { | ||
| return isSchemaObject(schema) && "maximum" in schema && typeof schema.maximum === "number"; | ||
| }; | ||
| var hasExclusiveMinimum = (schema) => { | ||
| return isSchemaObject(schema) && "exclusiveMinimum" in schema && typeof schema.exclusiveMinimum === "number"; | ||
| }; | ||
| var hasExclusiveMaximum = (schema) => { | ||
| return isSchemaObject(schema) && "exclusiveMaximum" in schema && typeof schema.exclusiveMaximum === "number"; | ||
| }; | ||
| var hasMultipleOf = (schema) => { | ||
| return isSchemaObject(schema) && "multipleOf" in schema && typeof schema.multipleOf === "number"; | ||
| }; | ||
| var hasMinItems = (schema) => { | ||
| return isSchemaObject(schema) && "minItems" in schema && typeof schema.minItems === "number"; | ||
| }; | ||
| var hasMaxItems = (schema) => { | ||
| return isSchemaObject(schema) && "maxItems" in schema && typeof schema.maxItems === "number"; | ||
| }; | ||
| var hasUniqueItems = (schema) => { | ||
| return isSchemaObject(schema) && "uniqueItems" in schema && typeof schema.uniqueItems === "boolean"; | ||
| }; | ||
| var hasMinProperties = (schema) => { | ||
| return isSchemaObject(schema) && "minProperties" in schema && typeof schema.minProperties === "number"; | ||
| }; | ||
| var hasMaxProperties = (schema) => { | ||
| return isSchemaObject(schema) && "maxProperties" in schema && typeof schema.maxProperties === "number"; | ||
| }; | ||
| // src/build-dynamic-ref-map.ts | ||
| var buildDynamicRefMap = (rootSchema) => { | ||
| const map = {}; | ||
| if (!isSchemaObject(rootSchema) || !("$defs" in rootSchema)) { | ||
| import { isSchemaObject } from './schema-guards.js'; | ||
| /** | ||
| * Builds a map of $dynamicRef anchor values to their corresponding $ref paths. | ||
| * | ||
| * JSON Schema 2020-12 uses $dynamicAnchor and $dynamicRef for late-binding references. | ||
| * In the OpenAPI spec, $dynamicAnchor: "meta" on the schema definition allows properties | ||
| * like media-type.schema to reference it via $dynamicRef: "#meta". | ||
| * | ||
| * This function scans all $defs for entries with $dynamicAnchor and builds a lookup | ||
| * so we can convert $dynamicRef values to concrete $ref paths. | ||
| * | ||
| * @example | ||
| * // Given a schema with $defs.schema having $dynamicAnchor: "meta" | ||
| * buildDynamicRefMap(rootSchema) | ||
| * // Returns: { "#meta": "#/$defs/schema" } | ||
| */ | ||
| export const 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; | ||
| } | ||
| 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; | ||
| }; | ||
| export { | ||
| buildDynamicRefMap | ||
| }; |
@@ -21,1 +21,2 @@ /** | ||
| export declare const deriveRootTypeName: (schema: unknown) => string; | ||
| //# sourceMappingURL=derive-root-type-name.d.ts.map |
@@ -1,19 +0,39 @@ | ||
| // src/is-object.ts | ||
| var isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value); | ||
| // src/derive-root-type-name.ts | ||
| var titleToPascalCase = (title) => { | ||
| const words = title.split(/[^a-zA-Z0-9]+/).filter(Boolean); | ||
| const pascal = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(""); | ||
| return pascal.replace(/^\d+/, ""); | ||
| import { isObject } from './is-object.js'; | ||
| /** | ||
| * Converts an arbitrary title string into a PascalCase TypeScript identifier. | ||
| * Splits on any run of non-alphanumeric characters, capitalizes the first | ||
| * letter of each word while preserving the rest (so acronyms like "API" or | ||
| * "JSON" survive intact), and drops leading digits since an identifier may not | ||
| * start with a number. | ||
| */ | ||
| const titleToPascalCase = (title) => { | ||
| const words = title.split(/[^a-zA-Z0-9]+/).filter(Boolean); | ||
| const pascal = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(''); | ||
| return pascal.replace(/^\d+/, ''); | ||
| }; | ||
| var deriveRootTypeName = (schema) => { | ||
| if (!isObject(schema) || typeof schema["title"] !== "string") { | ||
| return "Document"; | ||
| } | ||
| const name = titleToPascalCase(schema["title"]); | ||
| return name || "Document"; | ||
| /** | ||
| * Derives the root type name for a generated schema from its `title` keyword. | ||
| * | ||
| * We name the root after the schema itself instead of a generic "Document" so | ||
| * the generated types and parsers read naturally (e.g. an OpenAPI schema yields | ||
| * `OpenApi` / `parseOpenApi`). When the schema has no usable `title`, we fall | ||
| * back to "Document" to keep output deterministic. | ||
| * | ||
| * @param schema - The root JSON Schema. A boolean schema or one without a | ||
| * string `title` falls back to the default. | ||
| * @returns A PascalCase type name derived from the title, or "Document". | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * deriveRootTypeName({ title: 'OpenAPI Document' }) // 'OpenAPIDocument' | ||
| * deriveRootTypeName({ title: 'my-config' }) // 'MyConfig' | ||
| * deriveRootTypeName({ type: 'object' }) // 'Document' | ||
| * ``` | ||
| */ | ||
| export const deriveRootTypeName = (schema) => { | ||
| if (!isObject(schema) || typeof schema['title'] !== 'string') { | ||
| return 'Document'; | ||
| } | ||
| const name = titleToPascalCase(schema['title']); | ||
| return name || 'Document'; | ||
| }; | ||
| export { | ||
| deriveRootTypeName | ||
| }; |
@@ -24,1 +24,2 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export declare const extractRefs: (schema: JSONSchema) => Set<string>; | ||
| //# sourceMappingURL=extract-refs.d.ts.map |
+64
-33
@@ -1,36 +0,67 @@ | ||
| // src/extract-refs.ts | ||
| var isResolvableRef = (ref) => { | ||
| if (ref === "#") | ||
| /** | ||
| * Returns true if a $ref value should be queued for processing. | ||
| * | ||
| * Accepted forms: | ||
| * - Internal: `#/$defs/foo` or `#/definitions/foo` | ||
| * - URI key: `http://example.com/foo.json` (no fragment, or empty fragment) | ||
| * - URI with fragment: `http://example.com/foo.json#/definitions/bar` | ||
| * | ||
| * Excluded: | ||
| * - `#` alone (self-reference, not a standalone definition) | ||
| * - Relative path refs (e.g. `/components/messages/foo`) — these point into | ||
| * example data in the schema document, not into type definitions | ||
| */ | ||
| const isResolvableRef = (ref) => { | ||
| if (ref === '#') | ||
| return false; | ||
| if (ref.startsWith('#')) | ||
| return true; | ||
| if (ref.startsWith('http://') || ref.startsWith('https://')) | ||
| return true; | ||
| 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; | ||
| /** | ||
| * Extracts all $ref values from a JSON Schema recursively. | ||
| * Returns both internal (`#`-prefixed) and URI refs so the build pipeline | ||
| * can resolve and generate files for all referenced definitions. | ||
| * | ||
| * @param schema - The JSON Schema to extract refs from | ||
| * @returns A Set of unique ref strings found in the schema | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const schema = { | ||
| * type: 'object', | ||
| * properties: { | ||
| * contact: { $ref: '#/$defs/contact' }, | ||
| * channel: { $ref: 'http://example.com/channel.json' }, | ||
| * } | ||
| * } | ||
| * const refs = extractRefs(schema) | ||
| * // refs = Set(['#/$defs/contact', 'http://example.com/channel.json']) | ||
| * ``` | ||
| */ | ||
| export const 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']); | ||
| } | ||
| // Recursively traverse all properties using for...in to avoid intermediate array allocation | ||
| for (const key in record) { | ||
| traverse(record[key]); | ||
| } | ||
| }; | ||
| traverse(schema); | ||
| return refs; | ||
| }; | ||
| export { | ||
| extractRefs | ||
| }; |
@@ -18,1 +18,2 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export declare const generateTypeDefinition: (schema: JSONSchema, typeName: string, options?: TypeOptions) => string; | ||
| //# sourceMappingURL=generate-type-definition.d.ts.map |
+447
-565
@@ -1,600 +0,482 @@ | ||
| // src/has-ref.ts | ||
| var hasRef = (value) => { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value) && "$ref" in value && typeof value.$ref === "string"; | ||
| }; | ||
| // src/schema-guards.ts | ||
| var isSchemaObject = (schema) => { | ||
| return typeof schema === "object" && schema !== null && typeof schema !== "boolean"; | ||
| }; | ||
| var hasType = (schema) => { | ||
| return isSchemaObject(schema) && "type" in schema && typeof schema.type === "string"; | ||
| }; | ||
| var isObjectSchema = (schema) => { | ||
| return isSchemaObject(schema) && (("type" in schema) && schema.type === "object" || ("properties" in schema)); | ||
| }; | ||
| var hasProperties = (schema) => { | ||
| return isSchemaObject(schema) && "properties" in schema && typeof schema.properties === "object" && schema.properties !== null; | ||
| }; | ||
| var hasEnum = (schema) => { | ||
| return isSchemaObject(schema) && "enum" in schema && Array.isArray(schema.enum); | ||
| }; | ||
| var hasConst = (schema) => { | ||
| return isSchemaObject(schema) && "const" in schema; | ||
| }; | ||
| var hasPattern = (schema) => { | ||
| return isSchemaObject(schema) && "pattern" in schema && typeof schema.pattern === "string"; | ||
| }; | ||
| var hasFormat = (schema) => { | ||
| return isSchemaObject(schema) && "format" in schema && typeof schema.format === "string"; | ||
| }; | ||
| var hasDefault = (schema) => { | ||
| return isSchemaObject(schema) && "default" in schema; | ||
| }; | ||
| var hasExamples = (schema) => { | ||
| return isSchemaObject(schema) && "examples" in schema && Array.isArray(schema.examples); | ||
| }; | ||
| var hasOneOf = (schema) => { | ||
| return isSchemaObject(schema) && "oneOf" in schema && Array.isArray(schema.oneOf); | ||
| }; | ||
| var hasAnyOf = (schema) => { | ||
| return isSchemaObject(schema) && "anyOf" in schema && Array.isArray(schema.anyOf); | ||
| }; | ||
| var hasAllOf = (schema) => { | ||
| return isSchemaObject(schema) && "allOf" in schema && Array.isArray(schema.allOf); | ||
| }; | ||
| var hasRequired = (schema) => { | ||
| return isSchemaObject(schema) && "required" in schema && Array.isArray(schema.required); | ||
| }; | ||
| var hasItems = (schema) => { | ||
| return isSchemaObject(schema) && "items" in schema && typeof schema.items === "object" && schema.items !== null && typeof schema.items !== "boolean"; | ||
| }; | ||
| var hasAdditionalProperties = (schema) => { | ||
| return isSchemaObject(schema) && "additionalProperties" in schema; | ||
| }; | ||
| var hasMinLength = (schema) => { | ||
| return isSchemaObject(schema) && "minLength" in schema && typeof schema.minLength === "number"; | ||
| }; | ||
| var hasMaxLength = (schema) => { | ||
| return isSchemaObject(schema) && "maxLength" in schema && typeof schema.maxLength === "number"; | ||
| }; | ||
| var hasMinimum = (schema) => { | ||
| return isSchemaObject(schema) && "minimum" in schema && typeof schema.minimum === "number"; | ||
| }; | ||
| var hasMaximum = (schema) => { | ||
| return isSchemaObject(schema) && "maximum" in schema && typeof schema.maximum === "number"; | ||
| }; | ||
| var hasExclusiveMinimum = (schema) => { | ||
| return isSchemaObject(schema) && "exclusiveMinimum" in schema && typeof schema.exclusiveMinimum === "number"; | ||
| }; | ||
| var hasExclusiveMaximum = (schema) => { | ||
| return isSchemaObject(schema) && "exclusiveMaximum" in schema && typeof schema.exclusiveMaximum === "number"; | ||
| }; | ||
| var hasMultipleOf = (schema) => { | ||
| return isSchemaObject(schema) && "multipleOf" in schema && typeof schema.multipleOf === "number"; | ||
| }; | ||
| var hasMinItems = (schema) => { | ||
| return isSchemaObject(schema) && "minItems" in schema && typeof schema.minItems === "number"; | ||
| }; | ||
| var hasMaxItems = (schema) => { | ||
| return isSchemaObject(schema) && "maxItems" in schema && typeof schema.maxItems === "number"; | ||
| }; | ||
| var hasUniqueItems = (schema) => { | ||
| return isSchemaObject(schema) && "uniqueItems" in schema && typeof schema.uniqueItems === "boolean"; | ||
| }; | ||
| var hasMinProperties = (schema) => { | ||
| return isSchemaObject(schema) && "minProperties" in schema && typeof schema.minProperties === "number"; | ||
| }; | ||
| var hasMaxProperties = (schema) => { | ||
| return isSchemaObject(schema) && "maxProperties" in schema && typeof schema.maxProperties === "number"; | ||
| }; | ||
| // src/mjst-extension.ts | ||
| 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 (!isSchemaObject(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; | ||
| }; | ||
| // src/ref-to-filename.ts | ||
| 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; | ||
| }; | ||
| // src/ref-to-name.ts | ||
| 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(refToFilename(ref), suffix); | ||
| // src/safe-accessor.ts | ||
| var JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; | ||
| var safeAccessor = (variable, key) => { | ||
| if (JS_IDENTIFIER.test(key)) { | ||
| return `${variable}.${key}`; | ||
| } | ||
| if (variable.endsWith("?")) { | ||
| return `${variable}.['${key}']`; | ||
| } | ||
| return `${variable}['${key}']`; | ||
| }; | ||
| var safeKey = (key) => { | ||
| if (JS_IDENTIFIER.test(key)) { | ||
| return key; | ||
| } | ||
| return `'${key}'`; | ||
| }; | ||
| // src/generate-type-definition.ts | ||
| var getConditionalObjectSchema = (schema) => { | ||
| if (!isSchemaObject(schema)) { | ||
| return null; | ||
| } | ||
| if (!("if" in schema) || !("then" in schema)) { | ||
| return null; | ||
| } | ||
| const ifSchema = schema.if; | ||
| const thenSchema = schema.then; | ||
| if (!isSchemaObject(ifSchema) || !isSchemaObject(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); | ||
| import { getMjstBrand, getMjstInstanceOf, getMjstPrimitive } from './mjst-extension.js'; | ||
| import { refToName } from './ref-to-name.js'; | ||
| import { safeKey } from './safe-accessor.js'; | ||
| import { isObjectSchema, isSchemaObject } from './schema-guards.js'; | ||
| const getConditionalObjectSchema = (schema) => { | ||
| if (!isSchemaObject(schema)) { | ||
| return null; | ||
| } | ||
| } | ||
| if (hasIfProperties) { | ||
| for (const key in ifProperties) { | ||
| required.add(key); | ||
| if (!('if' in schema) || !('then' in schema)) { | ||
| return null; | ||
| } | ||
| } | ||
| if (Array.isArray(thenSchema.required)) { | ||
| for (const key of thenSchema.required) { | ||
| required.add(key); | ||
| const ifSchema = schema.if; | ||
| const thenSchema = schema.then; | ||
| if (!isSchemaObject(ifSchema) || !isSchemaObject(thenSchema)) { | ||
| return null; | ||
| } | ||
| } | ||
| if (hasThenProperties) { | ||
| for (const key in thenProperties) { | ||
| required.add(key); | ||
| 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 thenRef = typeof thenSchema.$ref === "string" ? thenSchema.$ref : null; | ||
| return { | ||
| schema: { | ||
| type: "object", | ||
| properties, | ||
| ...required.size > 0 ? { required: Array.from(required) } : {} | ||
| }, | ||
| thenRef | ||
| }; | ||
| 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 (!isSchemaObject(schema)) { | ||
| return false; | ||
| } | ||
| if (isObjectSchema(schema)) { | ||
| return true; | ||
| } | ||
| return "patternProperties" in schema || "additionalProperties" in schema || "if" in schema && "then" in schema; | ||
| const isObjectLikeSchema = (schema) => { | ||
| if (!isSchemaObject(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"; | ||
| const 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; | ||
| const buildJsDocBlock = (title, description, commentUrl) => { | ||
| let block = `/**\n`; | ||
| block += `* ${title}\n`; | ||
| block += `*\n`; | ||
| block += `* ${description}\n`; | ||
| if (commentUrl?.startsWith('http')) { | ||
| block += `* \n`; | ||
| block += `* @see {@link ${commentUrl}}\n`; | ||
| } | ||
| block += `*/\n`; | ||
| return block; | ||
| }; | ||
| var getTypeScriptType = (schema, options = {}) => { | ||
| const base = getUnbrandedType(schema, options); | ||
| const brand = getMjstBrand(schema); | ||
| return brand ? `(${base} & { readonly __brand: '${brand}' })` : base; | ||
| /** | ||
| * Converts a JSON Schema type to its TypeScript equivalent, applying any | ||
| * `x-mjst` brand. Branding is type-level only, so we compute the underlying | ||
| * type and intersect it with a unique brand marker. This is the recursion entry | ||
| * point, so branded nested fields are wrapped too. | ||
| */ | ||
| const 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"; | ||
| /** Wraps a `Record<...>` in `Readonly<...>` when readonly output is requested. */ | ||
| const recordType = (keyType, valueType, options) => options.readonly ? `Readonly<Record<${keyType}, ${valueType}>>` : `Record<${keyType}, ${valueType}>`; | ||
| /** | ||
| * Converts a JSON Schema type to its TypeScript equivalent, ignoring any brand. | ||
| * Recursively handles nested objects and arrays. | ||
| */ | ||
| const getUnbrandedType = (schema, options = {}) => { | ||
| // Boolean schema: `true` means any value is valid (unknown), `false` means no value is valid (never) | ||
| if (typeof schema === 'boolean') { | ||
| return getBooleanSubSchemaType(schema); | ||
| } | ||
| return refToName(schema.$ref, options.typeSuffix); | ||
| } | ||
| if (schema.$dynamicRef) { | ||
| if (schema.$dynamicRef === "#meta") { | ||
| return `Schema${options.typeSuffix ?? ""}`; | ||
| // Check if schema is an object (not a boolean schema) | ||
| if (typeof schema !== 'object' || schema === null) { | ||
| return 'unknown'; | ||
| } | ||
| return refToName(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]); | ||
| // An x-mjst instanceOf hint means the value is a runtime class (e.g. Date) | ||
| // that JSON Schema cannot describe — emit the class name as the type directly. | ||
| const instanceOf = getMjstInstanceOf(schema); | ||
| if (instanceOf) { | ||
| return instanceOf; | ||
| } | ||
| let enumUnion = JSON.stringify(schema.enum[0]); | ||
| for (let i = 1;i < schema.enum.length; i++) { | ||
| enumUnion += " | " + JSON.stringify(schema.enum[i]); | ||
| // An x-mjst primitive hint (e.g. bigint) names a non-JSON primitive — emit it | ||
| // directly as the TypeScript type. | ||
| const primitive = getMjstPrimitive(schema); | ||
| if (primitive) { | ||
| return primitive; | ||
| } | ||
| 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]); | ||
| // Handle $ref | ||
| if (schema.$ref) { | ||
| // External refs (e.g. http://json-schema.org/...) cannot be resolved locally — treat as unknown | ||
| if (!schema.$ref.startsWith('#')) { | ||
| return 'unknown'; | ||
| } | ||
| return refToName(schema.$ref, options.typeSuffix); | ||
| } | ||
| 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); | ||
| // Handle $dynamicRef (used for recursive schemas) | ||
| if (schema.$dynamicRef) { | ||
| // For #meta, this refers to the Schema type itself (JSON Schema 2020-12 $dynamicAnchor pattern) | ||
| if (schema.$dynamicRef === '#meta') { | ||
| return `Schema${options.typeSuffix ?? ''}`; | ||
| } | ||
| return refToName(schema.$dynamicRef, options.typeSuffix); | ||
| } | ||
| 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); | ||
| // Handle const - literal type | ||
| if (schema.const !== undefined) { | ||
| return JSON.stringify(schema.const); | ||
| } | ||
| 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); | ||
| // Handle enum - union of literal types | ||
| 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; | ||
| } | ||
| return intersectionTypes; | ||
| } | ||
| const conditionalResult = getConditionalObjectSchema(schema); | ||
| if (conditionalResult) { | ||
| const baseType = getTypeScriptType(conditionalResult.schema, options); | ||
| if (conditionalResult.thenRef) { | ||
| return `(${baseType}) & ${refToName(conditionalResult.thenRef, options.typeSuffix)}`; | ||
| // Handle multi-value enum - union of literal types | ||
| 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; | ||
| } | ||
| 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); | ||
| // Handle union types (oneOf, anyOf) - check this before returning unknown | ||
| if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length > 0) { | ||
| // schema.oneOf[0] is safe: we guard with .length > 0 above | ||
| 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.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.anyOf && Array.isArray(schema.anyOf) && schema.anyOf.length > 0) { | ||
| // schema.anyOf[0] is safe: we guard with .length > 0 above | ||
| 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.default !== undefined) { | ||
| if (typeof schema.default === "string") { | ||
| return "string"; | ||
| } | ||
| if (typeof schema.default === "number") { | ||
| return "number"; | ||
| } | ||
| if (typeof schema.default === "boolean") { | ||
| return "boolean"; | ||
| } | ||
| // Handle allOf (intersection types) | ||
| if (schema.allOf && Array.isArray(schema.allOf) && schema.allOf.length > 0) { | ||
| // schema.allOf[0] is safe: we guard with .length > 0 above | ||
| let intersectionTypes = getTypeScriptType(schema.allOf[0], options); | ||
| for (let i = 1; i < schema.allOf.length; i++) { | ||
| intersectionTypes += ' & ' + getTypeScriptType(schema.allOf[i], options); | ||
| } | ||
| return intersectionTypes; | ||
| } | ||
| 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]); | ||
| // Handle object-like conditional schemas that use if/then without declaring type | ||
| const conditionalResult = getConditionalObjectSchema(schema); | ||
| if (conditionalResult) { | ||
| const baseType = getTypeScriptType(conditionalResult.schema, options); | ||
| if (conditionalResult.thenRef) { | ||
| return `(${baseType}) & ${refToName(conditionalResult.thenRef, options.typeSuffix)}`; | ||
| } | ||
| return baseType; | ||
| } | ||
| 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) => isSchemaObject(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 = isSchemaObject(propSchema) && typeof propSchema.description === "string" ? propSchema.description : isSchemaObject(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 + ";"; | ||
| // No type so we return unknown | ||
| if (!schema.type) { | ||
| if (schema.additionalProperties !== undefined) { | ||
| if (typeof schema.additionalProperties === 'boolean') { | ||
| return recordType('string', getBooleanSubSchemaType(schema.additionalProperties), options); | ||
| } | ||
| } | ||
| return `{ | ||
| ` + properties2 + ` | ||
| }`; | ||
| return recordType('string', getTypeScriptType(schema.additionalProperties, options), options); | ||
| } | ||
| 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; | ||
| 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); | ||
| // The ^x- pattern is a common JSON Schema convention for vendor extensions | ||
| // that maps naturally to the TypeScript template literal `x-${string}` | ||
| if (pattern === '^x-') { | ||
| return recordType('`x-${string}`', valueType, options); | ||
| } | ||
| return recordType('string', valueType, options); | ||
| } | ||
| } | ||
| } | ||
| 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); | ||
| if (schema.default !== undefined) { | ||
| if (typeof schema.default === 'string') { | ||
| return 'string'; | ||
| } | ||
| return recordType("string", valueType, options); | ||
| } | ||
| if (typeof schema.default === 'number') { | ||
| return 'number'; | ||
| } | ||
| if (typeof schema.default === 'boolean') { | ||
| return 'boolean'; | ||
| } | ||
| } | ||
| } | ||
| 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 = isSchemaObject(schema) && typeof schema.description === "string" && schema.description || isSchemaObject(schema) && typeof schema.$comment === "string" && schema.$comment || undefined; | ||
| if (topLevelComment) { | ||
| result += buildJsDocBlock(typeName, topLevelComment); | ||
| return 'unknown'; | ||
| } | ||
| 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 = isSchemaObject(schema) && typeof schema.description === "string" && schema.description || isSchemaObject(schema) && typeof schema.$comment === "string" && schema.$comment || undefined; | ||
| if (topLevelComment) { | ||
| jsDocTitle = typeName; | ||
| jsDocDescription = topLevelComment; | ||
| // Handle type as an array (union of types) | ||
| 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; | ||
| } | ||
| 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; | ||
| switch (schema.type) { | ||
| // String | ||
| case 'string': | ||
| return 'string'; | ||
| // Number | ||
| case 'number': | ||
| case 'integer': | ||
| return 'number'; | ||
| // Boolean | ||
| case 'boolean': | ||
| return 'boolean'; | ||
| // Array | ||
| case 'array': | ||
| if (schema.items) { | ||
| const itemType = getTypeScriptType(schema.items, options); | ||
| // Wrap union types in parentheses so `(A | B)[]` is not misread as `A | B[]` | ||
| const wrappedItemType = itemType.includes(' | ') ? `(${itemType})` : itemType; | ||
| return options.readonly ? `readonly ${wrappedItemType}[]` : `${wrappedItemType}[]`; | ||
| } | ||
| return options.readonly ? 'readonly unknown[]' : 'unknown[]'; | ||
| // Object | ||
| case 'object': | ||
| if (schema.properties) { | ||
| const readonlyPrefix = options.readonly ? 'readonly ' : ''; | ||
| const hasDescriptions = Object.values(schema.properties).some((p) => isSchemaObject(p) && (typeof p.description === 'string' || typeof p.$comment === 'string')); | ||
| if (hasDescriptions) { | ||
| let properties = ''; | ||
| let first = true; | ||
| for (const key in schema.properties) { | ||
| // schema.properties[key] is safe: key comes from iterating schema.properties | ||
| const propSchema = schema.properties[key]; | ||
| const isRequired = schema.required?.includes(key) ?? false; | ||
| const optional = isRequired ? '' : '?'; | ||
| const propType = getTypeScriptType(propSchema, options); | ||
| const inlineDescription = isSchemaObject(propSchema) && typeof propSchema.description === 'string' | ||
| ? propSchema.description | ||
| : isSchemaObject(propSchema) && typeof propSchema.$comment === 'string' | ||
| ? propSchema.$comment | ||
| : undefined; | ||
| if (!first) | ||
| properties += '\n'; | ||
| first = false; | ||
| if (inlineDescription) { | ||
| properties += | ||
| ' /** ' + | ||
| inlineDescription + | ||
| ' */\n ' + | ||
| readonlyPrefix + | ||
| safeKey(key) + | ||
| optional + | ||
| ': ' + | ||
| propType + | ||
| ';'; | ||
| } | ||
| else { | ||
| properties += ' ' + readonlyPrefix + safeKey(key) + optional + ': ' + propType + ';'; | ||
| } | ||
| } | ||
| return '{\n' + properties + '\n}'; | ||
| } | ||
| let properties = ''; | ||
| let first = true; | ||
| for (const key in schema.properties) { | ||
| // schema.properties[key] is safe: key comes from iterating 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 + ' }'; | ||
| } | ||
| // Handle additionalProperties with $ref or $dynamicRef | ||
| if (schema.additionalProperties && typeof schema.additionalProperties === 'object') { | ||
| const additionalPropType = getTypeScriptType(schema.additionalProperties, options); | ||
| return recordType('string', additionalPropType, options); | ||
| } | ||
| // Handle patternProperties as a Record type | ||
| 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 to unknown | ||
| default: | ||
| return 'unknown'; | ||
| } | ||
| 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; | ||
| }; | ||
| /** | ||
| * Generates a TypeScript type definition from a JSON Schema. | ||
| * Handles required vs optional properties based on the schema's required array. | ||
| * Uses $comment as inline JSDoc description when present. | ||
| */ | ||
| export const generateTypeDefinition = (schema, typeName, options = {}) => { | ||
| const readonlyPrefix = options.readonly ? 'readonly ' : ''; | ||
| // Handle non-object schemas first | ||
| if (!isObjectLikeSchema(schema)) { | ||
| const tsType = getTypeScriptType(schema, options); | ||
| let result = ''; | ||
| const topLevelComment = (isSchemaObject(schema) && typeof schema.description === 'string' && schema.description) || | ||
| (isSchemaObject(schema) && typeof schema.$comment === 'string' && schema.$comment) || | ||
| undefined; | ||
| if (topLevelComment) { | ||
| result += buildJsDocBlock(typeName, topLevelComment); | ||
| } | ||
| result += `export type ${typeName} = ${tsType};`; | ||
| return result; | ||
| } | ||
| 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 = isSchemaObject(propSchema) && typeof propSchema.description === "string" ? propSchema.description : isSchemaObject(propSchema) && typeof propSchema.$comment === "string" ? propSchema.$comment : undefined; | ||
| if (inlineDescription) { | ||
| properties += " /** " + inlineDescription + ` */ | ||
| ` + quotedKey + optional + ": " + propType + ";"; | ||
| } else { | ||
| properties += " " + quotedKey + optional + ": " + propType + ";"; | ||
| } | ||
| } | ||
| const allOfIntersections = []; | ||
| if (isSchemaObject(schema) && Array.isArray(schema.allOf)) { | ||
| for (const entry of schema.allOf) { | ||
| if (isSchemaObject(entry) && entry.$ref) { | ||
| allOfIntersections.push(refToName(entry.$ref, options.typeSuffix)); | ||
| if (isObjectLikeSchema(schema)) { | ||
| const conditionalResult = getConditionalObjectSchema(schema); | ||
| const normalizedSchema = conditionalResult?.schema ?? schema; | ||
| const conditionalThenRef = conditionalResult?.thenRef ?? null; | ||
| let jsDocTitle; | ||
| let jsDocDescription; | ||
| const topLevelComment = (isSchemaObject(schema) && typeof schema.description === 'string' && schema.description) || | ||
| (isSchemaObject(schema) && typeof schema.$comment === 'string' && schema.$comment) || | ||
| undefined; | ||
| if (topLevelComment) { | ||
| jsDocTitle = typeName; | ||
| jsDocDescription = topLevelComment; | ||
| } | ||
| } | ||
| const hasProperties = normalizedSchema.properties && Object.keys(normalizedSchema.properties).length > 0; | ||
| const hasAdditionalProperties = normalizedSchema.additionalProperties && typeof normalizedSchema.additionalProperties === 'object'; | ||
| const hasPatternProperties = normalizedSchema.patternProperties && | ||
| typeof normalizedSchema.patternProperties === 'object' && | ||
| Object.keys(normalizedSchema.patternProperties).length > 0; | ||
| // Handle objects with only patternProperties (no fixed properties) | ||
| if (!hasProperties && 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); | ||
| // The ^x- pattern is a common JSON Schema convention for vendor extensions | ||
| // that maps naturally to the TypeScript template literal `x-${string}` | ||
| const keyType = firstPattern === '^x-' ? '`x-${string}`' : 'string'; | ||
| let result = ''; | ||
| if (jsDocTitle && jsDocDescription) { | ||
| result += buildJsDocBlock(jsDocTitle, jsDocDescription); | ||
| } | ||
| result += `export type ${typeName} = ${recordType(keyType, patternPropType, options)};`; | ||
| return result; | ||
| } | ||
| // Handle objects with only additionalProperties (no fixed properties) | ||
| if (!hasProperties && hasAdditionalProperties && normalizedSchema.additionalProperties) { | ||
| const additionalPropType = getTypeScriptType(normalizedSchema.additionalProperties, options); | ||
| let result = ''; | ||
| if (jsDocTitle && jsDocDescription) { | ||
| result += buildJsDocBlock(jsDocTitle, jsDocDescription); | ||
| } | ||
| result += `export type ${typeName} = {\n ${readonlyPrefix}[key: string]: ${additionalPropType};\n};`; | ||
| return result; | ||
| } | ||
| const schemaProps = normalizedSchema.properties ?? {}; | ||
| let properties = ''; | ||
| let isFirstProp = true; | ||
| for (const key in schemaProps) { | ||
| // schemaProps[key] is safe: key comes from iterating 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 += '\n'; | ||
| isFirstProp = false; | ||
| // Add JSDoc comment from $comment or description if available | ||
| const inlineDescription = isSchemaObject(propSchema) && typeof propSchema.description === 'string' | ||
| ? propSchema.description | ||
| : isSchemaObject(propSchema) && typeof propSchema.$comment === 'string' | ||
| ? propSchema.$comment | ||
| : undefined; | ||
| if (inlineDescription) { | ||
| properties += ' /** ' + inlineDescription + ' */\n ' + quotedKey + optional + ': ' + propType + ';'; | ||
| } | ||
| else { | ||
| properties += ' ' + quotedKey + optional + ': ' + propType + ';'; | ||
| } | ||
| } | ||
| // Collect allOf $ref intersections | ||
| const allOfIntersections = []; | ||
| if (isSchemaObject(schema) && Array.isArray(schema.allOf)) { | ||
| for (const entry of schema.allOf) { | ||
| if (isSchemaObject(entry) && entry.$ref) { | ||
| allOfIntersections.push(refToName(entry.$ref, options.typeSuffix)); | ||
| } | ||
| } | ||
| } | ||
| // JSON Schema 2019-09+ allows $ref as a sibling to other keywords. | ||
| // Treat it as an additional intersection type (e.g. for specification-extensions). | ||
| if (isSchemaObject(schema) && typeof schema.$ref === 'string' && schema.$ref.startsWith('#')) { | ||
| allOfIntersections.push(refToName(schema.$ref, options.typeSuffix)); | ||
| } | ||
| let result = ''; | ||
| if (jsDocTitle && jsDocDescription) { | ||
| result += buildJsDocBlock(jsDocTitle, jsDocDescription); | ||
| } | ||
| let typeBody = '{\n' + properties + '\n}'; | ||
| if (conditionalThenRef) { | ||
| typeBody += ' & ' + refToName(conditionalThenRef, options.typeSuffix); | ||
| } | ||
| for (const intersectionType of allOfIntersections) { | ||
| typeBody += ' & ' + intersectionType; | ||
| } | ||
| result += 'export type ' + typeName + ' = ' + typeBody + ';'; | ||
| return result; | ||
| } | ||
| if (isSchemaObject(schema) && typeof schema.$ref === "string" && schema.$ref.startsWith("#")) { | ||
| allOfIntersections.push(refToName(schema.$ref, options.typeSuffix)); | ||
| } | ||
| let result = ""; | ||
| if (jsDocTitle && jsDocDescription) { | ||
| result += buildJsDocBlock(jsDocTitle, jsDocDescription); | ||
| } | ||
| let typeBody = `{ | ||
| ` + properties + ` | ||
| }`; | ||
| if (conditionalThenRef) { | ||
| typeBody += " & " + refToName(conditionalThenRef, options.typeSuffix); | ||
| } | ||
| for (const intersectionType of allOfIntersections) { | ||
| typeBody += " & " + intersectionType; | ||
| } | ||
| result += "export type " + typeName + " = " + typeBody + ";"; | ||
| return result; | ||
| } | ||
| return "export type " + typeName + " = unknown;"; | ||
| return 'export type ' + typeName + ' = unknown;'; | ||
| }; | ||
| export { | ||
| generateTypeDefinition | ||
| }; |
@@ -5,1 +5,2 @@ /** Type guard to check if a value is a non-null, non-array object with a string $ref property */ | ||
| } & Record<string, unknown>; | ||
| //# sourceMappingURL=has-ref.d.ts.map |
+7
-6
@@ -1,7 +0,8 @@ | ||
| // src/has-ref.ts | ||
| var hasRef = (value) => { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value) && "$ref" in value && typeof value.$ref === "string"; | ||
| /** Type guard to check if a value is a non-null, non-array object with a string $ref property */ | ||
| export const hasRef = (value) => { | ||
| return (typeof value === 'object' && | ||
| value !== null && | ||
| !Array.isArray(value) && | ||
| '$ref' in value && | ||
| typeof value.$ref === 'string'); | ||
| }; | ||
| export { | ||
| hasRef | ||
| }; |
@@ -15,1 +15,2 @@ /** | ||
| export declare const isObject: (value: unknown) => value is Record<string, unknown>; | ||
| //# sourceMappingURL=is-object.d.ts.map |
+14
-5
@@ -1,5 +0,14 @@ | ||
| // src/is-object.ts | ||
| var isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value); | ||
| export { | ||
| isObject | ||
| }; | ||
| /** | ||
| * Returns true if the provided value is a plain object. | ||
| * Optimized for JSON data validation: checks that value is truthy, typeof object, | ||
| * and not an array. This correctly handles all JSON value types. | ||
| * | ||
| * Examples: | ||
| * isObject({}) // true | ||
| * isObject({ a: 1 }) // true | ||
| * isObject([]) // false (Array) | ||
| * isObject(null) // false | ||
| * isObject(123) // false | ||
| * isObject('string') // false | ||
| */ | ||
| export const isObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value); |
@@ -44,1 +44,2 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export declare const getMjstBrand: (schema: JSONSchema) => string | undefined; | ||
| //# sourceMappingURL=mjst-extension.d.ts.map |
+47
-119
@@ -1,123 +0,51 @@ | ||
| // src/has-ref.ts | ||
| var hasRef = (value) => { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value) && "$ref" in value && typeof value.$ref === "string"; | ||
| import { isSchemaObject } from './schema-guards.js'; | ||
| /** | ||
| * Vendor extension keyword carrying mjst-specific runtime hints that plain JSON | ||
| * Schema cannot express on its own. Adapters (TypeBox, Zod, ...) emit it when a | ||
| * source construct has no native JSON Schema equivalent, and the generators read | ||
| * it to produce the right TypeScript type and runtime checks. | ||
| */ | ||
| export const MJST_EXTENSION_KEY = 'x-mjst'; | ||
| // Only identifier-safe class names are honoured, so a malicious or malformed | ||
| // schema cannot inject arbitrary code into the generated output. | ||
| const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/; | ||
| // The non-JSON primitives we know how to generate type/runtime handling for. | ||
| // Anything outside this set is ignored so unknown hints degrade gracefully. | ||
| const SUPPORTED_PRIMITIVES = new Set(['bigint']); | ||
| // Brand names are embedded inside a single-quoted string literal in generated | ||
| // output, so we only allow characters that cannot break out of the literal. | ||
| const SAFE_BRAND = /^[\w$ -]+$/; | ||
| const readExtensionString = (schema, field) => { | ||
| if (!isSchemaObject(schema)) | ||
| return undefined; | ||
| const extension = schema[MJST_EXTENSION_KEY]; | ||
| if (typeof extension !== 'object' || extension === null) | ||
| return undefined; | ||
| const value = extension[field]; | ||
| return typeof value === 'string' ? value : undefined; | ||
| }; | ||
| // src/schema-guards.ts | ||
| var isSchemaObject = (schema) => { | ||
| return typeof schema === "object" && schema !== null && typeof schema !== "boolean"; | ||
| /** | ||
| * Reads the `instanceOf` class name from a schema's `x-mjst` extension, when it | ||
| * is present and a safe identifier. Returns undefined otherwise so callers fall | ||
| * back to ordinary type handling. | ||
| */ | ||
| export const getMjstInstanceOf = (schema) => { | ||
| const instanceOf = readExtensionString(schema, 'instanceOf'); | ||
| return instanceOf !== undefined && IDENTIFIER.test(instanceOf) ? instanceOf : undefined; | ||
| }; | ||
| var hasType = (schema) => { | ||
| return isSchemaObject(schema) && "type" in schema && typeof schema.type === "string"; | ||
| /** | ||
| * Reads the `primitive` type name from a schema's `x-mjst` extension, when it is | ||
| * one we support (e.g. `'bigint'`). Returns undefined otherwise. | ||
| */ | ||
| export const getMjstPrimitive = (schema) => { | ||
| const primitive = readExtensionString(schema, 'primitive'); | ||
| return primitive !== undefined && SUPPORTED_PRIMITIVES.has(primitive) ? primitive : undefined; | ||
| }; | ||
| var isObjectSchema = (schema) => { | ||
| return isSchemaObject(schema) && (("type" in schema) && schema.type === "object" || ("properties" in schema)); | ||
| /** | ||
| * Reads the nominal `brand` name from a schema's `x-mjst` extension, when it is | ||
| * present and safe to embed in generated output. Returns undefined otherwise. | ||
| */ | ||
| export const getMjstBrand = (schema) => { | ||
| const brand = readExtensionString(schema, 'brand'); | ||
| return brand !== undefined && SAFE_BRAND.test(brand) ? brand : undefined; | ||
| }; | ||
| var hasProperties = (schema) => { | ||
| return isSchemaObject(schema) && "properties" in schema && typeof schema.properties === "object" && schema.properties !== null; | ||
| }; | ||
| var hasEnum = (schema) => { | ||
| return isSchemaObject(schema) && "enum" in schema && Array.isArray(schema.enum); | ||
| }; | ||
| var hasConst = (schema) => { | ||
| return isSchemaObject(schema) && "const" in schema; | ||
| }; | ||
| var hasPattern = (schema) => { | ||
| return isSchemaObject(schema) && "pattern" in schema && typeof schema.pattern === "string"; | ||
| }; | ||
| var hasFormat = (schema) => { | ||
| return isSchemaObject(schema) && "format" in schema && typeof schema.format === "string"; | ||
| }; | ||
| var hasDefault = (schema) => { | ||
| return isSchemaObject(schema) && "default" in schema; | ||
| }; | ||
| var hasExamples = (schema) => { | ||
| return isSchemaObject(schema) && "examples" in schema && Array.isArray(schema.examples); | ||
| }; | ||
| var hasOneOf = (schema) => { | ||
| return isSchemaObject(schema) && "oneOf" in schema && Array.isArray(schema.oneOf); | ||
| }; | ||
| var hasAnyOf = (schema) => { | ||
| return isSchemaObject(schema) && "anyOf" in schema && Array.isArray(schema.anyOf); | ||
| }; | ||
| var hasAllOf = (schema) => { | ||
| return isSchemaObject(schema) && "allOf" in schema && Array.isArray(schema.allOf); | ||
| }; | ||
| var hasRequired = (schema) => { | ||
| return isSchemaObject(schema) && "required" in schema && Array.isArray(schema.required); | ||
| }; | ||
| var hasItems = (schema) => { | ||
| return isSchemaObject(schema) && "items" in schema && typeof schema.items === "object" && schema.items !== null && typeof schema.items !== "boolean"; | ||
| }; | ||
| var hasAdditionalProperties = (schema) => { | ||
| return isSchemaObject(schema) && "additionalProperties" in schema; | ||
| }; | ||
| var hasMinLength = (schema) => { | ||
| return isSchemaObject(schema) && "minLength" in schema && typeof schema.minLength === "number"; | ||
| }; | ||
| var hasMaxLength = (schema) => { | ||
| return isSchemaObject(schema) && "maxLength" in schema && typeof schema.maxLength === "number"; | ||
| }; | ||
| var hasMinimum = (schema) => { | ||
| return isSchemaObject(schema) && "minimum" in schema && typeof schema.minimum === "number"; | ||
| }; | ||
| var hasMaximum = (schema) => { | ||
| return isSchemaObject(schema) && "maximum" in schema && typeof schema.maximum === "number"; | ||
| }; | ||
| var hasExclusiveMinimum = (schema) => { | ||
| return isSchemaObject(schema) && "exclusiveMinimum" in schema && typeof schema.exclusiveMinimum === "number"; | ||
| }; | ||
| var hasExclusiveMaximum = (schema) => { | ||
| return isSchemaObject(schema) && "exclusiveMaximum" in schema && typeof schema.exclusiveMaximum === "number"; | ||
| }; | ||
| var hasMultipleOf = (schema) => { | ||
| return isSchemaObject(schema) && "multipleOf" in schema && typeof schema.multipleOf === "number"; | ||
| }; | ||
| var hasMinItems = (schema) => { | ||
| return isSchemaObject(schema) && "minItems" in schema && typeof schema.minItems === "number"; | ||
| }; | ||
| var hasMaxItems = (schema) => { | ||
| return isSchemaObject(schema) && "maxItems" in schema && typeof schema.maxItems === "number"; | ||
| }; | ||
| var hasUniqueItems = (schema) => { | ||
| return isSchemaObject(schema) && "uniqueItems" in schema && typeof schema.uniqueItems === "boolean"; | ||
| }; | ||
| var hasMinProperties = (schema) => { | ||
| return isSchemaObject(schema) && "minProperties" in schema && typeof schema.minProperties === "number"; | ||
| }; | ||
| var hasMaxProperties = (schema) => { | ||
| return isSchemaObject(schema) && "maxProperties" in schema && typeof schema.maxProperties === "number"; | ||
| }; | ||
| // src/mjst-extension.ts | ||
| 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 (!isSchemaObject(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; | ||
| }; | ||
| export { | ||
| getMjstPrimitive, | ||
| getMjstInstanceOf, | ||
| getMjstBrand, | ||
| MJST_EXTENSION_KEY | ||
| }; |
@@ -18,1 +18,2 @@ type PropertyDocumentation = { | ||
| export {}; | ||
| //# sourceMappingURL=parse-documentation.d.ts.map |
+124
-87
@@ -1,95 +0,132 @@ | ||
| // src/parse-documentation.ts | ||
| var parseDocumentation = (markdownDocumentation, commentUrl, fallbackCommentUrl) => { | ||
| try { | ||
| const markdown = markdownDocumentation; | ||
| const fragmentId = commentUrl.split("#")[1]; | ||
| if (!fragmentId) { | ||
| return null; | ||
| } | ||
| const baseUrl = commentUrl.split("#")[0]; | ||
| const titleWords = fragmentId.split("-"); | ||
| let sectionTitle = ""; | ||
| for (let i = 0;i < titleWords.length; i++) { | ||
| if (i > 0) | ||
| sectionTitle += " "; | ||
| const word = titleWords[i] ?? ""; | ||
| sectionTitle += word.charAt(0).toUpperCase() + word.slice(1); | ||
| } | ||
| const headingLevelMatch = markdown.match(/^(#{2,5})\s+\S.*Object\s*$/m); | ||
| const headingHashes = headingLevelMatch?.[1] ?? "####"; | ||
| const subHeadingHashes = headingHashes + "#"; | ||
| const escapedHashes = headingHashes.replace(/#/g, "\\#"); | ||
| const sectionRegex = new RegExp(`${escapedHashes}\\s+${sectionTitle}\\s*\\n([\\s\\S]*?)(?=\\n${escapedHashes}\\s|$)`, "i"); | ||
| const sectionMatch = markdown.match(sectionRegex); | ||
| if (!sectionMatch) { | ||
| return null; | ||
| } | ||
| const sectionContent = sectionMatch?.[1]; | ||
| if (!sectionContent) { | ||
| return null; | ||
| } | ||
| const escapedSubHashes = subHeadingHashes.replace(/#/g, "\\#"); | ||
| const descriptionMatch = sectionContent.match(new RegExp(`^([\\s\\S]*?)(?=\\n?${escapedSubHashes}|$)`)); | ||
| const description = descriptionMatch?.[1]?.trim().replace(/\n/g, " ") || ""; | ||
| const fixedFieldsRegex = new RegExp(`${escapedSubHashes} Fixed Fields\\s*\\n([\\s\\S]*)`); | ||
| const fixedFieldsMatch = sectionContent.match(fixedFieldsRegex); | ||
| const properties = {}; | ||
| if (fixedFieldsMatch?.[1]) { | ||
| const fixedFieldsContent = fixedFieldsMatch[1]; | ||
| const lines = fixedFieldsContent.split(` | ||
| `); | ||
| let inTable = false; | ||
| for (const line of lines) { | ||
| if (line.includes("Field Name") || line.includes("---|")) { | ||
| inTable = true; | ||
| continue; | ||
| /** | ||
| * Fetches and parses OpenAPI specification documentation from the markdown source. | ||
| * | ||
| * When a section has no Fixed Fields table (e.g. Header Object delegates to Parameter Object), | ||
| * pass `fallbackCommentUrl` to inherit property documentation from another section. | ||
| */ | ||
| export const parseDocumentation = (markdownDocumentation, commentUrl, fallbackCommentUrl) => { | ||
| try { | ||
| const markdown = markdownDocumentation; | ||
| // Extract the fragment ID from the URL (e.g., #info-object) | ||
| const fragmentId = commentUrl.split('#')[1]; | ||
| if (!fragmentId) { | ||
| return null; | ||
| } | ||
| if (line.startsWith("#")) { | ||
| inTable = false; | ||
| continue; | ||
| // Extract the base URL (everything before the #) | ||
| const baseUrl = commentUrl.split('#')[0]; | ||
| // Convert fragment ID to title case (e.g., "info-object" -> "Info Object") | ||
| const titleWords = fragmentId.split('-'); | ||
| let sectionTitle = ''; | ||
| for (let i = 0; i < titleWords.length; i++) { | ||
| if (i > 0) | ||
| sectionTitle += ' '; | ||
| const word = titleWords[i] ?? ''; | ||
| sectionTitle += word.charAt(0).toUpperCase() + word.slice(1); | ||
| } | ||
| if (!inTable || !line.trim() || !line.includes("|")) { | ||
| continue; | ||
| // Detect the heading level used for object sections in this markdown file. | ||
| // OAS 3.1 uses #### while OAS 3.2 uses ###, so we probe for both. | ||
| const headingLevelMatch = markdown.match(/^(#{2,5})\s+\S.*Object\s*$/m); | ||
| const headingHashes = headingLevelMatch?.[1] ?? '####'; | ||
| const subHeadingHashes = headingHashes + '#'; | ||
| // Find the section in the markdown | ||
| // Match the section heading and capture everything until the next same-level heading or end of file | ||
| const escapedHashes = headingHashes.replace(/#/g, '\\#'); | ||
| const sectionRegex = new RegExp(`${escapedHashes}\\s+${sectionTitle}\\s*\\n([\\s\\S]*?)(?=\\n${escapedHashes}\\s|$)`, 'i'); | ||
| const sectionMatch = markdown.match(sectionRegex); | ||
| if (!sectionMatch) { | ||
| return null; | ||
| } | ||
| const lineWithPlaceholder = line.replace(/\\\|/g, "___PIPE___"); | ||
| const cells = lineWithPlaceholder.split("|").map((cell) => cell.replace(/___PIPE___/g, "|").trim()).filter((cell) => cell); | ||
| if (cells.length >= 3) { | ||
| const fieldName = cells[0]?.replace(/<[^>]*>/g, "").trim(); | ||
| const descriptionCellIndex = cells.length >= 4 ? 3 : 2; | ||
| let fieldDescription = cells[descriptionCellIndex]?.trim() || ""; | ||
| const isRequired = fieldDescription.includes("REQUIRED"); | ||
| fieldDescription = fieldDescription.replace(/\(#([^)]+)\)/g, `(${baseUrl}#$1)`); | ||
| const isValidFieldName = fieldName && !fieldName.includes("Field Name") && !fieldName.startsWith("`") && !fieldName.startsWith("[") && /^[a-zA-Z]/.test(fieldName); | ||
| if (isValidFieldName) { | ||
| properties[fieldName] = { | ||
| description: fieldDescription, | ||
| isRequired | ||
| }; | ||
| } | ||
| const sectionContent = sectionMatch?.[1]; | ||
| if (!sectionContent) { | ||
| return null; | ||
| } | ||
| } | ||
| } | ||
| const title = sectionTitle.replace(/\sObject$/, " object"); | ||
| if (Object.keys(properties).length === 0 && fallbackCommentUrl) { | ||
| const fallback = parseDocumentation(markdownDocumentation, fallbackCommentUrl); | ||
| if (fallback && Object.keys(fallback.properties).length > 0) { | ||
| // Extract the description (paragraphs before the "Fixed Fields" sub-heading) | ||
| // \n? handles the case where sectionContent starts directly with the sub-heading (no leading newline), | ||
| // which happens when the section heading is followed by a blank line consumed by \s* in sectionRegex. | ||
| const escapedSubHashes = subHeadingHashes.replace(/#/g, '\\#'); | ||
| const descriptionMatch = sectionContent.match(new RegExp(`^([\\s\\S]*?)(?=\\n?${escapedSubHashes}|$)`)); | ||
| const description = descriptionMatch?.[1]?.trim().replace(/\n/g, ' ') || ''; | ||
| // Extract all Fixed Fields tables within the section (some objects like Encoding Object | ||
| // split their fields across multiple sub-tables under different sub-sub-headings). | ||
| // Capture everything from the first "Fixed Fields" sub-heading to the end of the section — | ||
| // the section content is already bounded by the outer section regex so we don't need | ||
| // a stop condition here. | ||
| const fixedFieldsRegex = new RegExp(`${escapedSubHashes} Fixed Fields\\s*\\n([\\s\\S]*)`); | ||
| const fixedFieldsMatch = sectionContent.match(fixedFieldsRegex); | ||
| const properties = {}; | ||
| if (fixedFieldsMatch?.[1]) { | ||
| const fixedFieldsContent = fixedFieldsMatch[1]; | ||
| // Parse all markdown table rows across all sub-tables within the Fixed Fields section | ||
| const lines = fixedFieldsContent.split('\n'); | ||
| let inTable = false; | ||
| for (const line of lines) { | ||
| // Skip the header row and separator row | ||
| if (line.includes('Field Name') || line.includes('---|')) { | ||
| inTable = true; | ||
| continue; | ||
| } | ||
| // A ###### sub-heading resets inTable so we re-enter on the next header row | ||
| if (line.startsWith('#')) { | ||
| inTable = false; | ||
| continue; | ||
| } | ||
| if (!inTable || !line.trim() || !line.includes('|')) { | ||
| continue; | ||
| } | ||
| // Parse table row: | fieldName | type | description | | ||
| // First, replace escaped pipes (\|) with a placeholder to avoid splitting on them | ||
| const lineWithPlaceholder = line.replace(/\\\|/g, '___PIPE___'); | ||
| const cells = lineWithPlaceholder | ||
| .split('|') | ||
| .map((cell) => cell.replace(/___PIPE___/g, '|').trim()) | ||
| .filter((cell) => cell); | ||
| if (cells.length >= 3) { | ||
| const fieldName = cells[0]?.replace(/<[^>]*>/g, '').trim(); // Remove HTML tags | ||
| // Some OpenAPI tables include an "Applies To" column before Description. | ||
| // We always want the right-most column as the property description. | ||
| const descriptionCellIndex = cells.length >= 4 ? 3 : 2; | ||
| let fieldDescription = cells[descriptionCellIndex]?.trim() || ''; | ||
| const isRequired = fieldDescription.includes('REQUIRED'); | ||
| // Replace relative anchor links with full URLs using the base URL | ||
| fieldDescription = fieldDescription.replace(/\(#([^)]+)\)/g, `(${baseUrl}#$1)`); | ||
| // Only accept field names that look like valid identifiers (camelCase or plain words). | ||
| // This filters out rows from non-field tables (e.g. type/contentType default tables) | ||
| // where the "field name" column contains type annotations like `string` or [_absent_]. | ||
| const isValidFieldName = fieldName && | ||
| !fieldName.includes('Field Name') && | ||
| !fieldName.startsWith('`') && | ||
| !fieldName.startsWith('[') && | ||
| /^[a-zA-Z]/.test(fieldName); | ||
| if (isValidFieldName) { | ||
| properties[fieldName] = { | ||
| description: fieldDescription, | ||
| isRequired, | ||
| }; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Format title as "Info object" instead of "Info Object" | ||
| const title = sectionTitle.replace(/\sObject$/, ' object'); | ||
| // If no properties were found and a fallback URL is provided, inherit properties from it | ||
| if (Object.keys(properties).length === 0 && fallbackCommentUrl) { | ||
| const fallback = parseDocumentation(markdownDocumentation, fallbackCommentUrl); | ||
| if (fallback && Object.keys(fallback.properties).length > 0) { | ||
| return { | ||
| title, | ||
| description, | ||
| properties: fallback.properties, | ||
| }; | ||
| } | ||
| } | ||
| return { | ||
| title, | ||
| description, | ||
| properties: fallback.properties | ||
| title, | ||
| description, | ||
| properties, | ||
| }; | ||
| } | ||
| } | ||
| return { | ||
| title, | ||
| description, | ||
| properties | ||
| }; | ||
| } catch (error) { | ||
| console.error("Failed to fetch OpenAPI documentation:", error); | ||
| return null; | ||
| } | ||
| catch (error) { | ||
| console.error('Failed to fetch OpenAPI documentation:', error); | ||
| return null; | ||
| } | ||
| }; | ||
| export { | ||
| parseDocumentation | ||
| }; |
@@ -37,1 +37,2 @@ /** | ||
| export declare const refToFilename: (ref: string) => string; | ||
| //# sourceMappingURL=ref-to-filename.d.ts.map |
+106
-42
@@ -1,44 +0,108 @@ | ||
| // src/ref-to-filename.ts | ||
| 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)}`; | ||
| /** | ||
| * Converts a PascalCase or camelCase string to kebab-case. | ||
| * Handles consecutive uppercase sequences (e.g. "APIKey" → "api-key") and | ||
| * known mixed-case acronyms like "OAuth" that would otherwise split incorrectly. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * toKebabCase('ServerVariable') // 'server-variable' | ||
| * toKebabCase('APIKeySecurityScheme') // 'api-key-security-scheme' | ||
| * toKebabCase('OAuthFlows') // 'oauth-flows' | ||
| * toKebabCase('already-kebab') // 'already-kebab' | ||
| * ``` | ||
| */ | ||
| export const toKebabCase = (value) => value | ||
| // Collapse known mixed-case acronyms before splitting so they stay together | ||
| .replace(/OAuth/g, 'Oauth') | ||
| .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') | ||
| .replace(/([a-z\d])([A-Z])/g, '$1-$2') | ||
| .toLowerCase(); | ||
| /** | ||
| * Derives a unique kebab-case filename from a URI ref. | ||
| * | ||
| * For a plain URI (no fragment), uses the path segments after the host, | ||
| * stripping version numbers, `.json` extension, and joining with `-`. | ||
| * For a URI with a fragment, appends the fragment's last path segment. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * uriRefToFilename('http://asyncapi.com/definitions/3.1.0/channel.json') | ||
| * // 'channel' | ||
| * uriRefToFilename('http://asyncapi.com/bindings/kafka/0.5.0/channel.json') | ||
| * // 'kafka-channel-binding' | ||
| * uriRefToFilename('http://asyncapi.com/bindings/sns/0.1.0/channel.json#/definitions/queue') | ||
| * // 'sns-channel-queue' | ||
| * ``` | ||
| */ | ||
| const uriRefToFilename = (uri) => { | ||
| const hashIndex = uri.indexOf('#'); | ||
| const baseUri = hashIndex === -1 ? uri : uri.slice(0, hashIndex); | ||
| const fragment = hashIndex === -1 ? '' : uri.slice(hashIndex + 1); | ||
| // Strip protocol + host, remove .json extension | ||
| const withoutProtocol = baseUri.replace(/^https?:\/\/[^/]+\//, ''); | ||
| const withoutExt = withoutProtocol.replace(/\.json$/, ''); | ||
| // Drop structural/noise segments: | ||
| // - "definitions" and "$defs" container keys | ||
| // - Version numbers that immediately follow "definitions" (e.g. "3.1.0" in "definitions/3.1.0/channel") | ||
| // but NOT version numbers in other positions (e.g. "0.5.0" in "bindings/kafka/0.5.0/channel") | ||
| // since those are needed to disambiguate multiple versions of the same binding | ||
| 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; | ||
| // Skip a version segment only if the previous (non-skipped) segment was "definitions" | ||
| const prevRaw = rawSegments[i - 1]; | ||
| if (/^\d+\.\d+/.test(s) && prevRaw !== undefined && SKIP_KEYS.has(prevRaw)) | ||
| continue; | ||
| segments.push(s); | ||
| } | ||
| // Join remaining segments, converting to kebab-case and replacing dots with dashes | ||
| const baseName = segments.map((s) => toKebabCase(s).replace(/\./g, '-')).join('-'); | ||
| if (!fragment) | ||
| return baseName; | ||
| // Append the last meaningful segment of the fragment, skipping structural keys | ||
| 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; | ||
| /** | ||
| * Converts a JSON Schema $ref to a filename. | ||
| * | ||
| * Handles three ref forms: | ||
| * - Internal `#/$defs/contact` → `contact` | ||
| * - Internal `#/definitions/ServerVariable` → `server-variable` | ||
| * - URI `http://example.com/definitions/3.1.0/channel.json` → `channel` | ||
| * - URI with fragment `http://example.com/channel.json#/definitions/queue` → `channel-queue` | ||
| * | ||
| * @param ref - The $ref string | ||
| * @returns The filename without extension | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * refToFilename('#/$defs/contact') // 'contact' | ||
| * refToFilename('#/$defs/server-variable') // 'server-variable' | ||
| * refToFilename('#/definitions/ServerVariable') // 'server-variable' | ||
| * refToFilename('#/definitions/APIKeySecurityScheme') // 'api-key-security-scheme' | ||
| * refToFilename('http://asyncapi.com/definitions/3.1.0/channel.json') // 'channel' | ||
| * ``` | ||
| */ | ||
| export const refToFilename = (ref) => { | ||
| // URI ref — derive name from URI path | ||
| if (ref.startsWith('http://') || ref.startsWith('https://')) { | ||
| return uriRefToFilename(ref); | ||
| } | ||
| // Internal ref — extract the last segment after the last / | ||
| const segments = ref.split('/'); | ||
| // Non-null assertion is safe here: split always returns at least one element | ||
| let filename = segments[segments.length - 1]; | ||
| // Normalise PascalCase/camelCase keys (e.g. from draft-07 "definitions") to kebab-case | ||
| if (/[A-Z]/.test(filename)) { | ||
| filename = toKebabCase(filename); | ||
| } | ||
| return filename; | ||
| }; | ||
| export { | ||
| toKebabCase, | ||
| refToFilename | ||
| }; |
@@ -22,1 +22,2 @@ /** | ||
| export declare const refToName: (ref: string, suffix?: string) => string; | ||
| //# sourceMappingURL=ref-to-name.d.ts.map |
+39
-53
@@ -1,54 +0,40 @@ | ||
| // src/ref-to-filename.ts | ||
| 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)}`; | ||
| import { refToFilename } from './ref-to-filename.js'; | ||
| /** | ||
| * Converts a kebab-case filename to PascalCase, appending an optional suffix. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * kebabToPascal('server-variable') // 'ServerVariable' | ||
| * kebabToPascal('channel') // 'Channel' | ||
| * kebabToPascal('channel', 'Object') // 'ChannelObject' | ||
| * ``` | ||
| */ | ||
| const 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 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; | ||
| }; | ||
| // src/ref-to-name.ts | ||
| 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(refToFilename(ref), suffix); | ||
| export { | ||
| refToName | ||
| }; | ||
| /** | ||
| * Converts a JSON Schema $ref to a type name. | ||
| * Derives the filename via `refToFilename` then converts to PascalCase, | ||
| * appending an optional suffix. | ||
| * | ||
| * Handles all ref forms: internal `#/$defs/...`, `#/definitions/...`, and URI refs. | ||
| * | ||
| * @param ref - The $ref string | ||
| * @param suffix - Optional suffix appended to the PascalCase name. Defaults to | ||
| * `''` (no suffix). Pass e.g. `'Object'` to get `ContactObject`. | ||
| * @returns The type name in PascalCase with the suffix applied | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * refToName('#/$defs/contact') // 'Contact' | ||
| * refToName('#/$defs/server-variable') // 'ServerVariable' | ||
| * refToName('#/$defs/contact', 'Object') // 'ContactObject' | ||
| * refToName('http://asyncapi.com/definitions/3.1.0/channel.json') // 'Channel' | ||
| * ``` | ||
| */ | ||
| export const refToName = (ref, suffix = '') => kebabToPascal(refToFilename(ref), suffix); |
@@ -14,1 +14,2 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export declare const resolveDynamicRefs: (schema: JSONSchema, dynamicRefMap: Record<string, string>) => JSONSchema; | ||
| //# sourceMappingURL=resolve-dynamic-refs.d.ts.map |
@@ -1,31 +0,39 @@ | ||
| // src/resolve-dynamic-refs.ts | ||
| 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; | ||
| /** | ||
| * Replaces $dynamicRef with $ref in a schema using the provided anchor-to-path map. | ||
| * | ||
| * This creates a deep clone of the schema to avoid mutating the original, then walks | ||
| * the clone and converts any { $dynamicRef: "#meta" } to { $ref: "#/$defs/schema" } | ||
| * (or whatever the dynamicRefMap dictates). | ||
| * | ||
| * Only the direct properties and their nested schemas are walked — this does not need | ||
| * to be infinitely deep because the build system generates separate files for each $def, | ||
| * so each schema is relatively shallow. | ||
| */ | ||
| export const resolveDynamicRefs = (schema, dynamicRefMap) => { | ||
| if (typeof schema !== 'object' || schema === null) { | ||
| return schema; | ||
| } | ||
| const record = obj; | ||
| if ("$dynamicRef" in record && typeof record["$dynamicRef"] === "string") { | ||
| const resolved = dynamicRefMap[record["$dynamicRef"]]; | ||
| if (resolved) { | ||
| record["$ref"] = resolved; | ||
| delete record["$dynamicRef"]; | ||
| } | ||
| // Skip if there are no dynamic refs to resolve | ||
| if (Object.keys(dynamicRefMap).length === 0) { | ||
| return schema; | ||
| } | ||
| for (const key in record) { | ||
| walk(record[key]); | ||
| } | ||
| }; | ||
| walk(clone); | ||
| return clone; | ||
| 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; | ||
| }; | ||
| export { | ||
| resolveDynamicRefs | ||
| }; |
@@ -27,1 +27,2 @@ /** | ||
| export declare const resolveRef: (ref: string, rootSchema: Record<string, unknown>) => Record<string, unknown> | undefined; | ||
| //# sourceMappingURL=resolve-ref.d.ts.map |
+74
-39
@@ -1,42 +0,77 @@ | ||
| // src/resolve-ref.ts | ||
| 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; | ||
| /** | ||
| * Navigates a JSON Pointer fragment (e.g. `/$defs/foo` or `/definitions/bar`) | ||
| * through a schema object, returning the target or undefined if not found. | ||
| */ | ||
| const 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 undefined; | ||
| } | ||
| } | ||
| else { | ||
| return undefined; | ||
| } | ||
| } | ||
| } | ||
| return current; | ||
| 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); | ||
| /** | ||
| * Resolves a JSON Schema $ref pointer to the actual schema definition. | ||
| * | ||
| * Supports three ref forms: | ||
| * - Internal: `#/$defs/contact` — navigates the root schema by JSON Pointer | ||
| * - URI key: `http://example.com/foo.json` — looks up the key directly in `$defs` | ||
| * - URI with fragment: `http://example.com/foo.json#/definitions/bar` — looks up | ||
| * the base URI in `$defs`, then navigates the fragment within that definition | ||
| * | ||
| * @param ref - The $ref string | ||
| * @param rootSchema - The root schema containing the definitions | ||
| * @returns The resolved schema or undefined if not found | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const rootSchema = { | ||
| * $defs: { | ||
| * contact: { type: 'object', properties: { email: { type: 'string' } } }, | ||
| * 'http://example.com/server.json': { type: 'object' }, | ||
| * } | ||
| * } | ||
| * resolveRef('#/$defs/contact', rootSchema) | ||
| * resolveRef('http://example.com/server.json', rootSchema) | ||
| * ``` | ||
| */ | ||
| export const resolveRef = (ref, rootSchema) => { | ||
| // Internal reference: navigate from root by JSON Pointer | ||
| if (ref.startsWith('#')) { | ||
| return navigatePointer(ref.slice(1), rootSchema); | ||
| } | ||
| // URI ref: may have a fragment (e.g. "http://foo.com/bar.json#/definitions/baz") | ||
| // A trailing bare "#" (e.g. "http://foo.com/schema#") means "the whole document" — treat as no fragment | ||
| 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; | ||
| // Look up the base URI as a key in $defs | ||
| const defs = rootSchema['$defs']; | ||
| if (typeof defs !== 'object' || defs === null) | ||
| return undefined; | ||
| const defsRecord = defs; | ||
| const base = defsRecord[baseUri]; | ||
| if (typeof base !== 'object' || base === null) | ||
| return undefined; | ||
| // No fragment — return the definition directly | ||
| if (!fragment) | ||
| return base; | ||
| // Normalize the fragment: draft-07 schemas use `/definitions/` but after | ||
| // upgradeDraft07Schema the key is renamed to `/$defs/` | ||
| const normalizedFragment = fragment.replace(/^\/definitions\//, '/$defs/'); | ||
| // Navigate the fragment within the resolved definition | ||
| return navigatePointer(normalizedFragment, base); | ||
| }; | ||
| export { | ||
| resolveRef | ||
| }; |
@@ -25,1 +25,2 @@ /** | ||
| export declare const safeKey: (key: string) => string; | ||
| //# sourceMappingURL=safe-accessor.d.ts.map |
+42
-19
@@ -1,21 +0,44 @@ | ||
| // src/safe-accessor.ts | ||
| var JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; | ||
| var safeAccessor = (variable, key) => { | ||
| if (JS_IDENTIFIER.test(key)) { | ||
| return `${variable}.${key}`; | ||
| } | ||
| if (variable.endsWith("?")) { | ||
| return `${variable}.['${key}']`; | ||
| } | ||
| return `${variable}['${key}']`; | ||
| /** | ||
| * Checks whether a property name is a valid JavaScript identifier that can be | ||
| * accessed with dot notation. Property names containing hyphens, dots, or | ||
| * other special characters (e.g., "x-linkedin") must use bracket notation. | ||
| */ | ||
| const JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; | ||
| /** | ||
| * Generates a safe property accessor for a given key on an object variable. | ||
| * Uses dot notation for simple identifiers and bracket notation for keys | ||
| * that contain special characters like hyphens. | ||
| * | ||
| * @param variable - The variable name (e.g., "input", "input?") | ||
| * @param key - The property name to access | ||
| * @returns A valid JS property access expression | ||
| * | ||
| * @example | ||
| * safeAccessor("input", "name") // "input.name" | ||
| * safeAccessor("input?", "x-linkedin") // "input?.['x-linkedin']" | ||
| * safeAccessor("input", "x-linkedin") // "input['x-linkedin']" | ||
| */ | ||
| export const safeAccessor = (variable, key) => { | ||
| if (JS_IDENTIFIER.test(key)) { | ||
| return `${variable}.${key}`; | ||
| } | ||
| // Handle optional chaining: "input?" -> "input?.['key']" | ||
| if (variable.endsWith('?')) { | ||
| return `${variable}.['${key}']`; | ||
| } | ||
| return `${variable}['${key}']`; | ||
| }; | ||
| var safeKey = (key) => { | ||
| if (JS_IDENTIFIER.test(key)) { | ||
| return key; | ||
| } | ||
| return `'${key}'`; | ||
| /** | ||
| * Generates a safe property key for use in object literals. | ||
| * Wraps keys that are not valid identifiers in quotes. | ||
| * | ||
| * @example | ||
| * safeKey("name") // "name" | ||
| * safeKey("x-linkedin") // "'x-linkedin'" | ||
| */ | ||
| export const safeKey = (key) => { | ||
| if (JS_IDENTIFIER.test(key)) { | ||
| return key; | ||
| } | ||
| return `'${key}'`; | ||
| }; | ||
| export { | ||
| safeKey, | ||
| safeAccessor | ||
| }; |
@@ -111,2 +111,3 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| }; | ||
| export { hasRef } from './has-ref'; | ||
| export { hasRef } from './has-ref.js'; | ||
| //# sourceMappingURL=schema-guards.d.ts.map |
+92
-93
@@ -1,121 +0,120 @@ | ||
| // src/has-ref.ts | ||
| var hasRef = (value) => { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value) && "$ref" in value && typeof value.$ref === "string"; | ||
| /** Type guard to check if schema is not false */ | ||
| export const isSchemaObject = (schema) => { | ||
| return typeof schema === 'object' && schema !== null && typeof schema !== 'boolean'; | ||
| }; | ||
| // src/schema-guards.ts | ||
| var isSchemaObject = (schema) => { | ||
| return typeof schema === "object" && schema !== null && typeof schema !== "boolean"; | ||
| /** Type guard to check if schema has a type property */ | ||
| export const hasType = (schema) => { | ||
| return isSchemaObject(schema) && 'type' in schema && typeof schema.type === 'string'; | ||
| }; | ||
| var hasType = (schema) => { | ||
| return isSchemaObject(schema) && "type" in schema && typeof schema.type === "string"; | ||
| /** Type guard to check if schema is an object schema */ | ||
| export const isObjectSchema = (schema) => { | ||
| return isSchemaObject(schema) && (('type' in schema && schema.type === 'object') || 'properties' in schema); | ||
| }; | ||
| var isObjectSchema = (schema) => { | ||
| return isSchemaObject(schema) && (("type" in schema) && schema.type === "object" || ("properties" in schema)); | ||
| /** Type guard to check if schema has properties */ | ||
| export const hasProperties = (schema) => { | ||
| return (isSchemaObject(schema) && | ||
| 'properties' in schema && | ||
| typeof schema.properties === 'object' && | ||
| schema.properties !== null); | ||
| }; | ||
| var hasProperties = (schema) => { | ||
| return isSchemaObject(schema) && "properties" in schema && typeof schema.properties === "object" && schema.properties !== null; | ||
| /** Type guard to check if schema has enum */ | ||
| export const hasEnum = (schema) => { | ||
| return isSchemaObject(schema) && 'enum' in schema && Array.isArray(schema.enum); | ||
| }; | ||
| var hasEnum = (schema) => { | ||
| return isSchemaObject(schema) && "enum" in schema && Array.isArray(schema.enum); | ||
| /** Type guard to check if schema has const */ | ||
| export const hasConst = (schema) => { | ||
| return isSchemaObject(schema) && 'const' in schema; | ||
| }; | ||
| var hasConst = (schema) => { | ||
| return isSchemaObject(schema) && "const" in schema; | ||
| /** Type guard to check if schema has pattern */ | ||
| export const hasPattern = (schema) => { | ||
| return isSchemaObject(schema) && 'pattern' in schema && typeof schema.pattern === 'string'; | ||
| }; | ||
| var hasPattern = (schema) => { | ||
| return isSchemaObject(schema) && "pattern" in schema && typeof schema.pattern === "string"; | ||
| /** Type guard to check if schema has format */ | ||
| export const hasFormat = (schema) => { | ||
| return isSchemaObject(schema) && 'format' in schema && typeof schema.format === 'string'; | ||
| }; | ||
| var hasFormat = (schema) => { | ||
| return isSchemaObject(schema) && "format" in schema && typeof schema.format === "string"; | ||
| /** Type guard to check if schema has default */ | ||
| export const hasDefault = (schema) => { | ||
| return isSchemaObject(schema) && 'default' in schema; | ||
| }; | ||
| var hasDefault = (schema) => { | ||
| return isSchemaObject(schema) && "default" in schema; | ||
| /** Type guard to check if schema has examples */ | ||
| export const hasExamples = (schema) => { | ||
| return isSchemaObject(schema) && 'examples' in schema && Array.isArray(schema.examples); | ||
| }; | ||
| var hasExamples = (schema) => { | ||
| return isSchemaObject(schema) && "examples" in schema && Array.isArray(schema.examples); | ||
| /** Type guard to check if schema has oneOf */ | ||
| export const hasOneOf = (schema) => { | ||
| return isSchemaObject(schema) && 'oneOf' in schema && Array.isArray(schema.oneOf); | ||
| }; | ||
| var hasOneOf = (schema) => { | ||
| return isSchemaObject(schema) && "oneOf" in schema && Array.isArray(schema.oneOf); | ||
| /** Type guard to check if schema has anyOf */ | ||
| export const hasAnyOf = (schema) => { | ||
| return isSchemaObject(schema) && 'anyOf' in schema && Array.isArray(schema.anyOf); | ||
| }; | ||
| var hasAnyOf = (schema) => { | ||
| return isSchemaObject(schema) && "anyOf" in schema && Array.isArray(schema.anyOf); | ||
| /** Type guard to check if schema has allOf */ | ||
| export const hasAllOf = (schema) => { | ||
| return isSchemaObject(schema) && 'allOf' in schema && Array.isArray(schema.allOf); | ||
| }; | ||
| var hasAllOf = (schema) => { | ||
| return isSchemaObject(schema) && "allOf" in schema && Array.isArray(schema.allOf); | ||
| /** Type guard to check if schema has required */ | ||
| export const hasRequired = (schema) => { | ||
| return isSchemaObject(schema) && 'required' in schema && Array.isArray(schema.required); | ||
| }; | ||
| var hasRequired = (schema) => { | ||
| return isSchemaObject(schema) && "required" in schema && Array.isArray(schema.required); | ||
| /** Type guard to check if schema has items (and it's not just boolean) */ | ||
| export const hasItems = (schema) => { | ||
| return (isSchemaObject(schema) && | ||
| 'items' in schema && | ||
| typeof schema.items === 'object' && | ||
| schema.items !== null && | ||
| typeof schema.items !== 'boolean'); | ||
| }; | ||
| var hasItems = (schema) => { | ||
| return isSchemaObject(schema) && "items" in schema && typeof schema.items === "object" && schema.items !== null && typeof schema.items !== "boolean"; | ||
| /** Type guard to check if schema has additionalProperties */ | ||
| export const hasAdditionalProperties = (schema) => { | ||
| return isSchemaObject(schema) && 'additionalProperties' in schema; | ||
| }; | ||
| var hasAdditionalProperties = (schema) => { | ||
| return isSchemaObject(schema) && "additionalProperties" in schema; | ||
| /** Type guard to check if schema has minLength */ | ||
| export const hasMinLength = (schema) => { | ||
| return isSchemaObject(schema) && 'minLength' in schema && typeof schema.minLength === 'number'; | ||
| }; | ||
| var hasMinLength = (schema) => { | ||
| return isSchemaObject(schema) && "minLength" in schema && typeof schema.minLength === "number"; | ||
| /** Type guard to check if schema has maxLength */ | ||
| export const hasMaxLength = (schema) => { | ||
| return isSchemaObject(schema) && 'maxLength' in schema && typeof schema.maxLength === 'number'; | ||
| }; | ||
| var hasMaxLength = (schema) => { | ||
| return isSchemaObject(schema) && "maxLength" in schema && typeof schema.maxLength === "number"; | ||
| /** Type guard to check if schema has minimum */ | ||
| export const hasMinimum = (schema) => { | ||
| return isSchemaObject(schema) && 'minimum' in schema && typeof schema.minimum === 'number'; | ||
| }; | ||
| var hasMinimum = (schema) => { | ||
| return isSchemaObject(schema) && "minimum" in schema && typeof schema.minimum === "number"; | ||
| /** Type guard to check if schema has maximum */ | ||
| export const hasMaximum = (schema) => { | ||
| return isSchemaObject(schema) && 'maximum' in schema && typeof schema.maximum === 'number'; | ||
| }; | ||
| var hasMaximum = (schema) => { | ||
| return isSchemaObject(schema) && "maximum" in schema && typeof schema.maximum === "number"; | ||
| /** Type guard to check if schema has exclusiveMinimum */ | ||
| export const hasExclusiveMinimum = (schema) => { | ||
| return isSchemaObject(schema) && 'exclusiveMinimum' in schema && typeof schema.exclusiveMinimum === 'number'; | ||
| }; | ||
| var hasExclusiveMinimum = (schema) => { | ||
| return isSchemaObject(schema) && "exclusiveMinimum" in schema && typeof schema.exclusiveMinimum === "number"; | ||
| /** Type guard to check if schema has exclusiveMaximum */ | ||
| export const hasExclusiveMaximum = (schema) => { | ||
| return isSchemaObject(schema) && 'exclusiveMaximum' in schema && typeof schema.exclusiveMaximum === 'number'; | ||
| }; | ||
| var hasExclusiveMaximum = (schema) => { | ||
| return isSchemaObject(schema) && "exclusiveMaximum" in schema && typeof schema.exclusiveMaximum === "number"; | ||
| /** Type guard to check if schema has multipleOf */ | ||
| export const hasMultipleOf = (schema) => { | ||
| return isSchemaObject(schema) && 'multipleOf' in schema && typeof schema.multipleOf === 'number'; | ||
| }; | ||
| var hasMultipleOf = (schema) => { | ||
| return isSchemaObject(schema) && "multipleOf" in schema && typeof schema.multipleOf === "number"; | ||
| /** Type guard to check if schema has minItems */ | ||
| export const hasMinItems = (schema) => { | ||
| return isSchemaObject(schema) && 'minItems' in schema && typeof schema.minItems === 'number'; | ||
| }; | ||
| var hasMinItems = (schema) => { | ||
| return isSchemaObject(schema) && "minItems" in schema && typeof schema.minItems === "number"; | ||
| /** Type guard to check if schema has maxItems */ | ||
| export const hasMaxItems = (schema) => { | ||
| return isSchemaObject(schema) && 'maxItems' in schema && typeof schema.maxItems === 'number'; | ||
| }; | ||
| var hasMaxItems = (schema) => { | ||
| return isSchemaObject(schema) && "maxItems" in schema && typeof schema.maxItems === "number"; | ||
| /** Type guard to check if schema has uniqueItems */ | ||
| export const hasUniqueItems = (schema) => { | ||
| return isSchemaObject(schema) && 'uniqueItems' in schema && typeof schema.uniqueItems === 'boolean'; | ||
| }; | ||
| var hasUniqueItems = (schema) => { | ||
| return isSchemaObject(schema) && "uniqueItems" in schema && typeof schema.uniqueItems === "boolean"; | ||
| /** Type guard to check if schema has minProperties */ | ||
| export const hasMinProperties = (schema) => { | ||
| return isSchemaObject(schema) && 'minProperties' in schema && typeof schema.minProperties === 'number'; | ||
| }; | ||
| var hasMinProperties = (schema) => { | ||
| return isSchemaObject(schema) && "minProperties" in schema && typeof schema.minProperties === "number"; | ||
| /** Type guard to check if schema has maxProperties */ | ||
| export const hasMaxProperties = (schema) => { | ||
| return isSchemaObject(schema) && 'maxProperties' in schema && typeof schema.maxProperties === 'number'; | ||
| }; | ||
| var hasMaxProperties = (schema) => { | ||
| return isSchemaObject(schema) && "maxProperties" in schema && typeof schema.maxProperties === "number"; | ||
| }; | ||
| export { | ||
| isSchemaObject, | ||
| isObjectSchema, | ||
| hasUniqueItems, | ||
| hasType, | ||
| hasRequired, | ||
| hasRef, | ||
| hasProperties, | ||
| hasPattern, | ||
| hasOneOf, | ||
| hasMultipleOf, | ||
| hasMinimum, | ||
| hasMinProperties, | ||
| hasMinLength, | ||
| hasMinItems, | ||
| hasMaximum, | ||
| hasMaxProperties, | ||
| hasMaxLength, | ||
| hasMaxItems, | ||
| hasItems, | ||
| hasFormat, | ||
| hasExclusiveMinimum, | ||
| hasExclusiveMaximum, | ||
| hasExamples, | ||
| hasEnum, | ||
| hasDefault, | ||
| hasConst, | ||
| hasAnyOf, | ||
| hasAllOf, | ||
| hasAdditionalProperties | ||
| }; | ||
| export { hasRef } from './has-ref.js'; |
@@ -34,1 +34,2 @@ /** | ||
| export declare const upgradeDraft07Schema: (schema: Record<string, unknown>) => Record<string, unknown>; | ||
| //# sourceMappingURL=upgrade-draft07-schema.d.ts.map |
+152
-130
@@ -1,138 +0,160 @@ | ||
| // src/ref-to-filename.ts | ||
| 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)}`; | ||
| /** | ||
| * Upgrades a JSON Schema draft-07 document to be compatible with the | ||
| * draft 2020-12 conventions used by the build pipeline. | ||
| * | ||
| * Draft-07 schemas differ from 2020-12 in two ways that affect our pipeline: | ||
| * - They use `definitions` instead of `$defs` | ||
| * - Their `definitions` keys (and `$ref` values) may be full URIs | ||
| * (e.g. `http://asyncapi.com/definitions/3.1.0/channel.json`) rather than | ||
| * short names (e.g. `channel`) | ||
| * | ||
| * This function: | ||
| * 1. Renames `definitions` → `$defs` at the root level only | ||
| * 2. Hoists any nested `$defs` (originally `definitions` inside sub-schemas) | ||
| * up to the root `$defs` with a prefixed name, rewriting internal refs | ||
| * so they resolve correctly from the root | ||
| * 3. Rewrites bare `$ref: "#"` self-references within each definition to | ||
| * point back to that definition's root-level `$defs` entry | ||
| * | ||
| * Only applied when the schema declares `$schema: http://json-schema.org/draft-07/schema`. | ||
| */ | ||
| import { refToFilename, toKebabCase } from './ref-to-filename.js'; | ||
| /** | ||
| * Returns true if the schema is a draft-07 document that needs upgrading. | ||
| */ | ||
| export const isDraft07Schema = (schema) => typeof schema['$schema'] === 'string' && schema['$schema'].includes('draft-07'); | ||
| /** | ||
| * Rewrites `$ref` values in a schema tree using an explicit string→string map. | ||
| * Also rewrites bare `$ref: "#"` to the given `selfRef` path when provided. | ||
| */ | ||
| const 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 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; | ||
| }; | ||
| // src/upgrade-draft07-schema.ts | ||
| 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); | ||
| /** | ||
| * Hoists nested `$defs` from each root-level definition up to the root `$defs`. | ||
| * | ||
| * When a definition contains its own `$defs` (originally `definitions` in draft-07, | ||
| * e.g. the json-schema meta-schema or avro schema), those nested defs are moved to | ||
| * the root with a `parentName-childName` prefix. All internal `#/$defs/X` refs | ||
| * within the parent and its nested defs are rewritten to `#/$defs/parentName-X`. | ||
| * Bare `$ref: "#"` within nested defs is rewritten to `#/$defs/parentName`. | ||
| */ | ||
| const 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; | ||
| } | ||
| // Derive a short kebab-case prefix from the parent name (which may be a URI) | ||
| const parentPrefix = parentName.startsWith('http://') || parentName.startsWith('https://') ? refToFilename(parentName) : parentName; | ||
| // Build a map from local ref → hoisted ref for every nested def | ||
| const localToHoisted = new Map(); | ||
| for (const localName of Object.keys(nestedDefs)) { | ||
| const hoistedName = `${parentPrefix}-${toKebabCase(localName)}`; | ||
| localToHoisted.set(`#/$defs/${localName}`, `#/$defs/${hoistedName}`); | ||
| } | ||
| const selfRef = `#/$defs/${parentPrefix}`; | ||
| // Rewrite refs in the parent. Keep the nested $defs in place so that | ||
| // URI-with-fragment refs (e.g. "http://foo.json#/$defs/queue") can still | ||
| // navigate into the parent's nested defs after resolution. | ||
| const rewrittenParent = rewriteRefs(parentObj, localToHoisted, selfRef); | ||
| hoisted[parentName] = rewrittenParent; | ||
| // Hoist each nested def, rewriting its internal refs too | ||
| for (const [localName, localSchema] of Object.entries(nestedDefs)) { | ||
| const hoistedName = `${parentPrefix}-${toKebabCase(localName)}`; | ||
| hoisted[hoistedName] = rewriteRefs(localSchema, localToHoisted, selfRef); | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| return hoisted; | ||
| }; | ||
| var hoistNestedDefs = (defs) => { | ||
| const hoisted = {}; | ||
| for (const [parentName, parentSchema] of Object.entries(defs)) { | ||
| if (typeof parentSchema !== "object" || parentSchema === null) { | ||
| hoisted[parentName] = parentSchema; | ||
| continue; | ||
| /** | ||
| * Upgrades a draft-07 schema so it is compatible with the build pipeline. | ||
| * If the schema is not draft-07, it is returned unchanged. | ||
| * | ||
| * @param schema - The raw JSON Schema (any draft) | ||
| * @returns The schema with `definitions` renamed to `$defs` at the root, | ||
| * nested defs hoisted to the root, and internal refs rewritten | ||
| */ | ||
| export const upgradeDraft07Schema = (schema) => { | ||
| if (!isDraft07Schema(schema)) | ||
| return schema; | ||
| // Rename root-level `definitions` to `$defs` (keep all other keys as-is) | ||
| const { definitions, $schema: _, ...rest } = schema; | ||
| const rawDefs = (definitions ?? {}); | ||
| // Recursively rename `definitions` → `$defs` inside each definition's body | ||
| // so nested defs are accessible before hoisting | ||
| const renamedDefs = {}; | ||
| for (const [key, value] of Object.entries(rawDefs)) { | ||
| renamedDefs[key] = renameNestedDefs(value); | ||
| } | ||
| const parentObj = parentSchema; | ||
| const nestedDefs = parentObj["$defs"]; | ||
| if (!nestedDefs || typeof nestedDefs !== "object") { | ||
| hoisted[parentName] = parentSchema; | ||
| continue; | ||
| // Hoist nested $defs up to root so the pipeline can resolve all refs flatly | ||
| const hoistedDefs = hoistNestedDefs(renamedDefs); | ||
| // Add short-name aliases for URI-keyed definitions so that internal refs | ||
| // like `#/$defs/draft-07-schema` (produced by self-ref rewriting in hoistNestedDefs) | ||
| // resolve correctly alongside the original URI key lookups. | ||
| for (const key of Object.keys(hoistedDefs)) { | ||
| if (key.startsWith('http://') || key.startsWith('https://')) { | ||
| const shortName = refToFilename(key); | ||
| if (shortName && !(shortName in hoistedDefs)) { | ||
| hoistedDefs[shortName] = hoistedDefs[key]; | ||
| } | ||
| } | ||
| } | ||
| const parentPrefix = parentName.startsWith("http://") || parentName.startsWith("https://") ? refToFilename(parentName) : parentName; | ||
| const localToHoisted = new Map; | ||
| for (const localName of Object.keys(nestedDefs)) { | ||
| const hoistedName = `${parentPrefix}-${toKebabCase(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}-${toKebabCase(localName)}`; | ||
| hoisted[hoistedName] = rewriteRefs(localSchema, localToHoisted, selfRef); | ||
| } | ||
| } | ||
| return hoisted; | ||
| return { | ||
| ...rest, | ||
| $defs: hoistedDefs, | ||
| }; | ||
| }; | ||
| 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 = refToFilename(key); | ||
| if (shortName && !(shortName in hoistedDefs)) { | ||
| hoistedDefs[shortName] = hoistedDefs[key]; | ||
| } | ||
| /** | ||
| * Recursively renames `definitions` → `$defs` within a schema value and | ||
| * rewrites `$ref: "#/definitions/X"` to `$ref: "#/$defs/X"` so that | ||
| * `hoistNestedDefs` can map them to their hoisted root-level equivalents. | ||
| * Does NOT hoist — hoisting is done separately at the root level. | ||
| */ | ||
| const 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 { | ||
| ...rest, | ||
| $defs: hoistedDefs | ||
| }; | ||
| return result; | ||
| }; | ||
| 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; | ||
| }; | ||
| export { | ||
| upgradeDraft07Schema, | ||
| isDraft07Schema | ||
| }; |
| /** Parses the items of an array with a parser function */ | ||
| export declare const validateArray: (input: unknown, parser: (input: unknown) => unknown) => any[]; | ||
| //# sourceMappingURL=validate-array.d.ts.map |
+12
-14
@@ -1,15 +0,13 @@ | ||
| // src/validate-array.ts | ||
| var validateArray = (input, parser) => { | ||
| if (!Array.isArray(input)) { | ||
| return []; | ||
| } | ||
| const len = input.length; | ||
| const result = new Array(len); | ||
| for (let i = 0;i < len; i++) { | ||
| result[i] = parser(input[i]); | ||
| } | ||
| return result; | ||
| /** Parses the items of an array with a parser function */ | ||
| export const validateArray = (input, parser) => { | ||
| if (!Array.isArray(input)) { | ||
| return []; | ||
| } | ||
| // Pre-allocate the result array for better performance than push() | ||
| const len = input.length; | ||
| const result = new Array(len); | ||
| for (let i = 0; i < len; i++) { | ||
| result[i] = parser(input[i]); | ||
| } | ||
| return result; | ||
| }; | ||
| export { | ||
| validateArray | ||
| }; |
@@ -6,1 +6,2 @@ /** | ||
| export declare const validateRecord: (input: unknown, parser: (input: unknown) => unknown) => Record<string, unknown>; | ||
| //# sourceMappingURL=validate-record.d.ts.map |
+15
-17
@@ -1,18 +0,16 @@ | ||
| // src/is-object.ts | ||
| var isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value); | ||
| // src/validate-record.ts | ||
| var validateRecord = (input, parser) => { | ||
| if (!isObject(input)) { | ||
| return {}; | ||
| } | ||
| const record = input; | ||
| const result = {}; | ||
| for (const key in record) { | ||
| result[key] = parser(record[key]); | ||
| } | ||
| return result; | ||
| import { isObject } from './is-object.js'; | ||
| /** | ||
| * Parses the values of a record with a parser function. | ||
| * Uses for...in instead of Object.entries() to avoid allocating an intermediate array. | ||
| */ | ||
| export const validateRecord = (input, parser) => { | ||
| if (!isObject(input)) { | ||
| return {}; | ||
| } | ||
| const record = input; | ||
| const result = {}; | ||
| for (const key in record) { | ||
| result[key] = parser(record[key]); | ||
| } | ||
| return result; | ||
| }; | ||
| export { | ||
| validateRecord | ||
| }; |
+9
-88
| { | ||
| "name": "@amritk/helpers", | ||
| "version": "0.6.2", | ||
| "version": "0.7.0", | ||
| "description": "Shared utilities for the mjst code generation ecosystem.", | ||
@@ -30,5 +30,5 @@ "type": "module", | ||
| "scripts": { | ||
| "build": "bun run build:code && bun run build:types", | ||
| "build:code": "bun build ./src/build-dynamic-ref-map.ts ./src/derive-root-type-name.ts ./src/extract-refs.ts ./src/generate-type-definition.ts ./src/has-ref.ts ./src/is-object.ts ./src/mjst-extension.ts ./src/parse-documentation.ts ./src/ref-to-filename.ts ./src/ref-to-name.ts ./src/resolve-dynamic-refs.ts ./src/resolve-ref.ts ./src/safe-accessor.ts ./src/schema-guards.ts ./src/upgrade-draft07-schema.ts ./src/validate-array.ts ./src/validate-record.ts --outdir=dist --root=src --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 ../.. helpers" | ||
| }, | ||
@@ -40,88 +40,9 @@ "dependencies": { | ||
| "./package.json": "./package.json", | ||
| "./build-dynamic-ref-map": { | ||
| "bun": "./src/build-dynamic-ref-map.ts", | ||
| "default": "./dist/build-dynamic-ref-map.js", | ||
| "types": "./dist/build-dynamic-ref-map.d.ts" | ||
| }, | ||
| "./derive-root-type-name": { | ||
| "bun": "./src/derive-root-type-name.ts", | ||
| "default": "./dist/derive-root-type-name.js", | ||
| "types": "./dist/derive-root-type-name.d.ts" | ||
| }, | ||
| "./extract-refs": { | ||
| "bun": "./src/extract-refs.ts", | ||
| "default": "./dist/extract-refs.js", | ||
| "types": "./dist/extract-refs.d.ts" | ||
| }, | ||
| "./generate-type-definition": { | ||
| "bun": "./src/generate-type-definition.ts", | ||
| "default": "./dist/generate-type-definition.js", | ||
| "types": "./dist/generate-type-definition.d.ts" | ||
| }, | ||
| "./has-ref": { | ||
| "bun": "./src/has-ref.ts", | ||
| "default": "./dist/has-ref.js", | ||
| "types": "./dist/has-ref.d.ts" | ||
| }, | ||
| "./is-object": { | ||
| "bun": "./src/is-object.ts", | ||
| "default": "./dist/is-object.js", | ||
| "types": "./dist/is-object.d.ts" | ||
| }, | ||
| "./mjst-extension": { | ||
| "bun": "./src/mjst-extension.ts", | ||
| "default": "./dist/mjst-extension.js", | ||
| "types": "./dist/mjst-extension.d.ts" | ||
| }, | ||
| "./parse-documentation": { | ||
| "bun": "./src/parse-documentation.ts", | ||
| "default": "./dist/parse-documentation.js", | ||
| "types": "./dist/parse-documentation.d.ts" | ||
| }, | ||
| "./ref-to-filename": { | ||
| "bun": "./src/ref-to-filename.ts", | ||
| "default": "./dist/ref-to-filename.js", | ||
| "types": "./dist/ref-to-filename.d.ts" | ||
| }, | ||
| "./ref-to-name": { | ||
| "bun": "./src/ref-to-name.ts", | ||
| "default": "./dist/ref-to-name.js", | ||
| "types": "./dist/ref-to-name.d.ts" | ||
| }, | ||
| "./resolve-dynamic-refs": { | ||
| "bun": "./src/resolve-dynamic-refs.ts", | ||
| "default": "./dist/resolve-dynamic-refs.js", | ||
| "types": "./dist/resolve-dynamic-refs.d.ts" | ||
| }, | ||
| "./resolve-ref": { | ||
| "bun": "./src/resolve-ref.ts", | ||
| "default": "./dist/resolve-ref.js", | ||
| "types": "./dist/resolve-ref.d.ts" | ||
| }, | ||
| "./safe-accessor": { | ||
| "bun": "./src/safe-accessor.ts", | ||
| "default": "./dist/safe-accessor.js", | ||
| "types": "./dist/safe-accessor.d.ts" | ||
| }, | ||
| "./schema-guards": { | ||
| "bun": "./src/schema-guards.ts", | ||
| "default": "./dist/schema-guards.js", | ||
| "types": "./dist/schema-guards.d.ts" | ||
| }, | ||
| "./upgrade-draft07-schema": { | ||
| "bun": "./src/upgrade-draft07-schema.ts", | ||
| "default": "./dist/upgrade-draft07-schema.js", | ||
| "types": "./dist/upgrade-draft07-schema.d.ts" | ||
| }, | ||
| "./validate-array": { | ||
| "bun": "./src/validate-array.ts", | ||
| "default": "./dist/validate-array.js", | ||
| "types": "./dist/validate-array.d.ts" | ||
| }, | ||
| "./validate-record": { | ||
| "bun": "./src/validate-record.ts", | ||
| "default": "./dist/validate-record.js", | ||
| "types": "./dist/validate-record.d.ts" | ||
| "./*": { | ||
| "development": "./src/*.ts", | ||
| "import": "./dist/*.js", | ||
| "types": "./dist/*.d.ts", | ||
| "default": "./dist/*.js" | ||
| } | ||
| } | ||
| } |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
284631
20.3%100
47.06%6582
11.14%