🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@amritk/helpers

Package Overview
Dependencies
Maintainers
1
Versions
25
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@amritk/helpers - npm Package Compare versions

Comparing version
0.13.2
to
0.13.3
+30
-53
dist/build-dynamic-ref-map.js

@@ -1,55 +0,32 @@

import { isSchemaObject } from './schema-guards.js';
/** Escapes a JSON Pointer segment (RFC 6901): `~` → `~0`, `/` → `~1`. */
const escapeSegment = (segment) => segment.indexOf('~') !== -1 || segment.indexOf('/') !== -1
? segment.replace(/~/g, '~0').replace(/\//g, '~1')
: segment;
// Keywords whose values are data, not subschemas — a `$dynamicAnchor` key inside
// an enum member or example value is instance data and must not register.
const NON_SCHEMA_KEYWORDS = new Set(['enum', 'const', 'default', 'examples']);
/**
* 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".
*
* The whole document is scanned — a `$dynamicAnchor` may sit anywhere, not just
* on a direct `$defs` entry — and each anchor maps to the JSON Pointer of the
* subschema that declares it. When the same anchor name appears more than once,
* the first occurrence in document order wins, matching how the resolvers bind
* a name to a single document-global target.
*
* @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))
return map;
const walk = (node, pointer) => {
if (node === null || typeof node !== 'object')
return;
if (Array.isArray(node)) {
for (let i = 0; i < node.length; i++)
walk(node[i], `${pointer}/${i}`);
return;
}
const record = node;
const anchor = record['$dynamicAnchor'];
// The document root is skipped: mapping it would rewrite `$dynamicRef` to
// `$ref: "#"`, a self-reference the file-per-definition generators cannot
// name an output file for. Every nested subschema gets a real pointer.
if (pointer !== '' && typeof anchor === 'string' && !(`#${anchor}` in map)) {
map[`#${anchor}`] = `#${pointer}`;
}
for (const key of Object.keys(record)) {
if (NON_SCHEMA_KEYWORDS.has(key))
continue;
walk(record[key], `${pointer}/${escapeSegment(key)}`);
}
};
walk(rootSchema, '');
import { isSchemaObject } from "./schema-guards.js";
const escapeSegment = (segment) => segment.indexOf("~") !== -1 || segment.indexOf("/") !== -1 ? segment.replace(/~/g, "~0").replace(/\//g, "~1") : segment;
const NON_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set(["enum", "const", "default", "examples"]);
const buildDynamicRefMap = (rootSchema) => {
const map = {};
if (!isSchemaObject(rootSchema))
return map;
const walk = (node, pointer) => {
if (node === null || typeof node !== "object")
return;
if (Array.isArray(node)) {
for (let i = 0; i < node.length; i++)
walk(node[i], `${pointer}/${i}`);
return;
}
const record = node;
const anchor = record["$dynamicAnchor"];
if (pointer !== "" && typeof anchor === "string" && !(`#${anchor}` in map)) {
map[`#${anchor}`] = `#${pointer}`;
}
for (const key of Object.keys(record)) {
if (NON_SCHEMA_KEYWORDS.has(key))
continue;
walk(record[key], `${pointer}/${escapeSegment(key)}`);
}
};
walk(rootSchema, "");
return map;
};
export {
buildDynamicRefMap
};

@@ -1,52 +0,22 @@

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.
*/
import { isObject } from "./is-object.js";
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+/, '');
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+/, "");
};
/**
* Derives the root type name for a generated schema.
*
* 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`, and a `spec-plan.json` file yields `SpecPlan` /
* `parseSpecPlan`). The schema's `title` keyword wins when present; otherwise
* the caller can supply the schema's filename (without extension) as a
* `fallbackName` so the name still reflects the source. When neither yields a
* usable identifier we fall back to "Document" to keep output deterministic.
*
* @param schema - The root JSON Schema. A boolean schema or one without a
* string `title` uses `fallbackName`.
* @param fallbackName - Optional name (typically the schema's base filename)
* used when the schema has no usable `title`. PascalCased the same way.
* @returns A PascalCase type name derived from the title or fallback, or
* "Document".
*
* @example
* ```ts
* deriveRootTypeName({ title: 'OpenAPI Document' }) // 'OpenAPIDocument'
* deriveRootTypeName({ title: 'my-config' }) // 'MyConfig'
* deriveRootTypeName({ type: 'object' }, 'spec-plan') // 'SpecPlan'
* deriveRootTypeName({ type: 'object' }) // 'Document'
* ```
*/
export const deriveRootTypeName = (schema, fallbackName) => {
if (isObject(schema) && typeof schema['title'] === 'string') {
const fromTitle = titleToPascalCase(schema['title']);
if (fromTitle)
return fromTitle;
}
if (typeof fallbackName === 'string') {
const fromFilename = titleToPascalCase(fallbackName);
if (fromFilename)
return fromFilename;
}
return 'Document';
const deriveRootTypeName = (schema, fallbackName) => {
if (isObject(schema) && typeof schema["title"] === "string") {
const fromTitle = titleToPascalCase(schema["title"]);
if (fromTitle)
return fromTitle;
}
if (typeof fallbackName === "string") {
const fromFilename = titleToPascalCase(fallbackName);
if (fromFilename)
return fromFilename;
}
return "Document";
};
export {
deriveRootTypeName
};

@@ -1,83 +0,42 @@

/**
* Escapes a JSON Schema `pattern` so it can be embedded between the slashes of
* a generated regex literal (`/…/`), and validates that the pattern is a legal
* regex at generation time.
*
* A `pattern` is an ECMA-262 regex *body*, and the generated text goes into a
* regex literal — not a string literal — so backslashes are regex syntax and
* must be left exactly as-is (doubling `\d` to `\\d` would change it from "a
* digit" to "a literal backslash followed by d"). Two things would otherwise
* corrupt the surrounding literal:
* - an *unescaped* `/`, which would close the literal early; and
* - a raw line terminator (LF, CR, U+2028, U+2029), which is not allowed
* inside a regex literal and would split the emitted `/…/` across lines (a
* syntax error). Each is rewritten to its backslash escape, which matches
* the identical character, so the regex's meaning is unchanged.
*
* Additionally, an *invalid* pattern (e.g. `([`) would be emitted verbatim as
* `/([/…` and produce output that does not parse. We compile the pattern with
* `new RegExp` here and throw a clear generator-time error instead, so a bad
* schema fails loudly during generation rather than emitting broken code.
*
* @example
* escapeRegexPattern('\\d{4}/\\d{2}') // → '\\d{4}\\/\\d{2}' (i.e. \d{4}\/\d{2})
* @throws if `pattern` is not a valid regular expression.
*/
// Line terminators, keyed by code point, and the backslash escape each maps to.
// These are the four characters disallowed *raw* inside a regex literal.
const lineTerminatorEscapes = {
10: '\\n', // LF
13: '\\r', // CR
8232: '\\u2028', // LINE SEPARATOR
8233: '\\u2029', // PARAGRAPH SEPARATOR
10: "\\n",
// LF
13: "\\r",
// CR
8232: "\\u2028",
// LINE SEPARATOR
8233: "\\u2029"
// PARAGRAPH SEPARATOR
};
// Memoized results: the same schema pattern is escaped once per parser, per
// validator, and per assertion context, on every generation — and the dominant
// cost is the validating `new RegExp` compile. Schemas carry a bounded set of
// patterns; the cap only guards a pathological long-lived process.
const escapeCache = new Map();
const ESCAPE_CACHE_LIMIT = 1000;
export const escapeRegexPattern = (pattern) => {
const cached = escapeCache.get(pattern);
if (cached !== undefined)
return cached;
// Validate at generation time — an invalid pattern must fail here, not emit
// a `/([/` literal that breaks the generated file.
try {
new RegExp(pattern);
}
catch (error) {
throw new Error(`Invalid regex pattern ${JSON.stringify(pattern)}: ${error instanceof Error ? error.message : String(error)}`);
}
// Match either an escape sequence (`\` + any char, kept verbatim) or a single
// character that would corrupt the literal (a bare `/` or a line terminator).
// The line terminators are written as `\u….` escapes so this source file is
// itself free of the raw characters it guards against. Consuming escape pairs
// first means the slash in `\/` is never seen as bare, so it is not
// double-escaped.
const escaped = pattern.replace(/\\[\s\S]|[/\n\r\u2028\u2029]/g, (match) => {
if (match === '/')
return '\\/';
const terminatorEscape = lineTerminatorEscapes[match.charCodeAt(0)];
if (terminatorEscape !== undefined && match.length === 1)
return terminatorEscape;
// An escape pair like `\/` or `\d`: keep exactly as authored.
return match;
});
// The empty pattern is valid JSON Schema (it matches everything), but an empty
// regex body would emit `//` \u2014 a line comment, not a literal \u2014 and break the
// generated file. Emit `(?:)` instead, exactly what `new RegExp('').source`
// yields: an empty non-capturing group that still matches everything.
if (escaped === '') {
escapeCache.set(pattern, '(?:)');
return '(?:)';
}
if (escapeCache.size >= ESCAPE_CACHE_LIMIT) {
// Evict the single oldest entry (Maps iterate in insertion order) instead
// of clearing wholesale — a corpus slightly over the limit keeps its hot
// set instead of collapsing to a 0% hit rate every pass.
escapeCache.delete(escapeCache.keys().next().value);
}
escapeCache.set(pattern, escaped);
return escaped;
const escapeCache = /* @__PURE__ */ new Map();
const ESCAPE_CACHE_LIMIT = 1e3;
const escapeRegexPattern = (pattern) => {
const cached = escapeCache.get(pattern);
if (cached !== void 0)
return cached;
try {
new RegExp(pattern);
} catch (error) {
throw new Error(`Invalid regex pattern ${JSON.stringify(pattern)}: ${error instanceof Error ? error.message : String(error)}`);
}
const escaped = pattern.replace(/\\[\s\S]|[/\n\r\u2028\u2029]/g, (match) => {
if (match === "/")
return "\\/";
const terminatorEscape = lineTerminatorEscapes[match.charCodeAt(0)];
if (terminatorEscape !== void 0 && match.length === 1)
return terminatorEscape;
return match;
});
if (escaped === "") {
escapeCache.set(pattern, "(?:)");
return "(?:)";
}
if (escapeCache.size >= ESCAPE_CACHE_LIMIT) {
escapeCache.delete(escapeCache.keys().next().value);
}
escapeCache.set(pattern, escaped);
return escaped;
};
export {
escapeRegexPattern
};

@@ -1,22 +0,5 @@

import { buildDynamicRefMap } from './build-dynamic-ref-map.js';
/**
* Collects a `#/...` ref for every subschema in the document that carries a
* `$dynamicAnchor` — anywhere in the tree, not just direct `$defs` entries.
*
* 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.
*
* Delegates to {@link buildDynamicRefMap} so the set of seeded refs and the
* `$dynamicRef` rewrite targets can never disagree.
*
* @example
* ```ts
* // $defs.schema has $dynamicAnchor: "meta"
* extractDynamicAnchorDefs(rootSchema) // ['#/$defs/schema']
* ```
*/
export const extractDynamicAnchorDefs = (schema) => Object.values(buildDynamicRefMap(schema));
import { buildDynamicRefMap } from "./build-dynamic-ref-map.js";
const extractDynamicAnchorDefs = (schema) => Object.values(buildDynamicRefMap(schema));
export {
extractDynamicAnchorDefs
};

@@ -1,67 +0,35 @@

/**
* 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;
if (ref === "#")
return false;
if (ref.startsWith("#"))
return true;
if (ref.startsWith("http://") || ref.startsWith("https://"))
return true;
return false;
};
/**
* 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;
const extractRefs = (schema) => {
const refs = /* @__PURE__ */ 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;
};
export {
extractRefs
};

@@ -1,92 +0,61 @@

// Generated files declare their public surface with `export type <Name>` /
// `export const <Name>` at line starts, so the names can be recovered from the
// source text without parsing it.
/** Reads the identifier following `prefix` when `content` starts with it at `at`. */
const exportNameAt = (content, at, prefix) => {
if (!content.startsWith(prefix, at))
return null;
let end = at + prefix.length;
while (end < content.length) {
const code = content.charCodeAt(end);
const isWord = (code >= 97 && code <= 122) || (code >= 65 && code <= 90) || (code >= 48 && code <= 57) || code === 95;
if (!isWord)
break;
end++;
}
return end > at + prefix.length ? content.slice(at + prefix.length, end) : null;
if (!content.startsWith(prefix, at))
return null;
let end = at + prefix.length;
while (end < content.length) {
const code = content.charCodeAt(end);
const isWord = code >= 97 && code <= 122 || code >= 65 && code <= 90 || code >= 48 && code <= 57 || code === 95;
if (!isWord)
break;
end++;
}
return end > at + prefix.length ? content.slice(at + prefix.length, end) : null;
};
/** True for every JS LineTerminator code unit — the same set the old `/m` regexes anchored after. */
const isLineTerminator = (code) => code === 10 || code === 13 || code === 8232 || code === 8233;
/**
* Collects `export type` / `export const` names with a single line-start walk.
* A multiline-anchored regex scan (`/^export .../gm`) does the same job but
* showed up at several percent of total generation time in CPU profiles — the
* regex engine re-anchors at every line of every generated file on every build.
*/
const collectExportNames = (content, typeNames, constNames) => {
let lineStart = 0;
while (lineStart < content.length) {
// charCode prefilter: almost every line starts with whitespace, a brace, or
// a keyword other than `export` — one integer compare skips the substring
// comparison for all of them (101 === 'e').
if (content.charCodeAt(lineStart) === 101 && content.startsWith('export ', lineStart)) {
const typeName = exportNameAt(content, lineStart, 'export type ');
if (typeName !== null) {
typeNames.push(typeName);
}
else {
const constName = exportNameAt(content, lineStart, 'export const ');
if (constName !== null)
constNames.push(constName);
}
}
// Advance past the next line break of ANY JS flavor (LF, CR, CRLF,
// U+2028, U+2029) — matching the multiline regexes this walk replaced,
// which treated all of them as line starts.
let next = lineStart;
while (next < content.length && !isLineTerminator(content.charCodeAt(next)))
next++;
if (next >= content.length)
break;
// \r\n counts as one break.
lineStart = content.charCodeAt(next) === 13 && content.charCodeAt(next + 1) === 10 ? next + 2 : next + 1;
let lineStart = 0;
while (lineStart < content.length) {
if (content.charCodeAt(lineStart) === 101 && content.startsWith("export ", lineStart)) {
const typeName = exportNameAt(content, lineStart, "export type ");
if (typeName !== null) {
typeNames.push(typeName);
} else {
const constName = exportNameAt(content, lineStart, "export const ");
if (constName !== null)
constNames.push(constName);
}
}
let next = lineStart;
while (next < content.length && !isLineTerminator(content.charCodeAt(next)))
next++;
if (next >= content.length)
break;
lineStart = content.charCodeAt(next) === 13 && content.charCodeAt(next + 1) === 10 ? next + 2 : next + 1;
}
};
/**
* 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 importExt = options.importExt ?? 'js';
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 = [];
collectExportNames(file.content, typeNames, constNames);
if (typeNames.length === 0 && constNames.length === 0)
continue;
// An explicit extension so the barrel resolves under Node ESM, not only Bun.
if (typesOnly) {
indexContent += `export type { ${typeNames.join(', ')} } from './${moduleName}.${importExt}';\n`;
}
else {
const typeExports = typeNames.map((name) => `type ${name}`);
indexContent += `export { ${[...typeExports, ...constNames].join(', ')} } from './${moduleName}.${importExt}';\n`;
}
const generateIndexBarrel = (files, options = {}) => {
const typesOnly = options.typesOnly ?? false;
const importExt = options.importExt ?? "js";
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 = [];
collectExportNames(file.content, typeNames, constNames);
if (typeNames.length === 0 && constNames.length === 0)
continue;
if (typesOnly) {
indexContent += `export type { ${typeNames.join(", ")} } from './${moduleName}.${importExt}';
`;
} else {
const typeExports = typeNames.map((name) => `type ${name}`);
indexContent += `export { ${[...typeExports, ...constNames].join(", ")} } from './${moduleName}.${importExt}';
`;
}
return indexContent;
}
return indexContent;
};
export {
generateIndexBarrel
};

@@ -1,501 +0,415 @@

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';
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 (!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 = /* @__PURE__ */ new Set();
if (Array.isArray(ifSchema.required)) {
for (const key of ifSchema.required) {
required.add(key);
}
if (!('if' in schema) || !('then' in schema)) {
return null;
}
if (hasIfProperties) {
for (const key in ifProperties) {
required.add(key);
}
const ifSchema = schema.if;
const thenSchema = schema.then;
if (!isSchemaObject(ifSchema) || !isSchemaObject(thenSchema)) {
return null;
}
if (Array.isArray(thenSchema.required)) {
for (const key of thenSchema.required) {
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;
}
if (hasThenProperties) {
for (const key in thenProperties) {
required.add(key);
}
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,
};
}
const thenRef = typeof thenSchema.$ref === "string" ? thenSchema.$ref : null;
return {
schema: {
type: "object",
properties,
...required.size > 0 ? { required: Array.from(required) } : {}
},
thenRef
};
};
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);
if (!isSchemaObject(schema)) {
return false;
}
if (isObjectSchema(schema)) {
return true;
}
return "patternProperties" in schema || "additionalProperties" in schema || "if" in schema && "then" in schema;
};
const getBooleanSubSchemaType = (schema) => {
return schema ? 'unknown' : 'never';
return schema ? "unknown" : "never";
};
/**
* Renders a possibly multi-line description as JSDoc body lines, prefixing each
* line with `${indent}* `. Blank lines become a bare `${indent}*` so we never
* emit trailing whitespace. Without this, embedded newlines would leave
* continuation lines unprefixed and break the comment block.
*/
const renderJsDocBody = (description, indent) => description
.split('\n')
.map((line) => (line.length > 0 ? `${indent}* ${line}\n` : `${indent}*\n`))
.join('');
const renderJsDocBody = (description, indent) => description.split("\n").map((line) => line.length > 0 ? `${indent}* ${line}
` : `${indent}*
`).join("");
const buildJsDocBlock = (title, description, commentUrl) => {
let block = `/**\n`;
block += `* ${title}\n`;
block += `*\n`;
block += renderJsDocBody(description, '');
if (commentUrl?.startsWith('http')) {
block += `* \n`;
block += `* @see {@link ${commentUrl}}\n`;
}
block += `*/\n`;
return block;
let block = `/**
`;
block += `* ${title}
`;
block += `*
`;
block += renderJsDocBody(description, "");
if (commentUrl?.startsWith("http")) {
block += `*
`;
block += `* @see {@link ${commentUrl}}
`;
}
block += `*/
`;
return block;
};
/**
* Builds the inline JSDoc comment that precedes a generated property. A
* single-line description stays compact (` /** text *\/`); a multi-line
* description expands into an asterisk-prefixed block so every line is
* commented. The returned string always ends with a newline, ready for the
* property declaration to follow.
*/
const buildInlinePropertyComment = (description) => {
if (!description.includes('\n')) {
return ` /** ${description} */\n`;
}
return ` /**\n${renderJsDocBody(description, ' ')} */\n`;
if (!description.includes("\n")) {
return ` /** ${description} */
`;
}
return ` /**
${renderJsDocBody(description, " ")} */
`;
};
/**
* 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;
const base = getUnbrandedType(schema, options);
const brand = getMjstBrand(schema);
return brand ? `(${base} & { readonly __brand: '${brand}' })` : base;
};
/** Wraps a `Record<...>` in `Readonly<...>` when readonly output is requested. */
const recordType = (keyType, valueType, options) => options.readonly ? `Readonly<Record<${keyType}, ${valueType}>>` : `Record<${keyType}, ${valueType}>`;
/**
* Builds the `Record<…>` type for a schema's `patternProperties`. Every pattern's
* value type is unioned (deduplicated) rather than only the first being used, so
* `{ '^a': string-schema, '^b': number-schema }` becomes `Record<string, string |
* number>` instead of silently dropping the second pattern. The template-literal
* `` `x-${string}` `` key is kept only when the sole pattern is the `^x-` vendor
* convention. Returns `undefined` when there are no usable patterns.
*/
const patternPropertiesRecordType = (patternProperties, options) => {
const entries = Object.entries(patternProperties);
if (entries.length === 0)
return undefined;
const seen = new Set();
const valueTypes = [];
for (const [, value] of entries) {
const valueType = typeof value === 'boolean' ? getBooleanSubSchemaType(value) : getTypeScriptType(value, options);
if (!seen.has(valueType)) {
seen.add(valueType);
valueTypes.push(valueType);
}
const entries = Object.entries(patternProperties);
if (entries.length === 0)
return void 0;
const seen = /* @__PURE__ */ new Set();
const valueTypes = [];
for (const [, value] of entries) {
const valueType2 = typeof value === "boolean" ? getBooleanSubSchemaType(value) : getTypeScriptType(value, options);
if (!seen.has(valueType2)) {
seen.add(valueType2);
valueTypes.push(valueType2);
}
if (valueTypes.length === 0)
return undefined;
const valueType = valueTypes.join(' | ');
// The `^x-` vendor-extension convention maps to a template-literal key, but only
// when it is the single pattern (a union of key patterns can't be expressed).
const keyType = entries.length === 1 && entries[0]?.[0] === '^x-' ? '`x-${string}`' : 'string';
return recordType(keyType, valueType, options);
}
if (valueTypes.length === 0)
return void 0;
const valueType = valueTypes.join(" | ");
const keyType = entries.length === 1 && entries[0]?.[0] === "^x-" ? "`x-${string}`" : "string";
return recordType(keyType, valueType, options);
};
/**
* 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);
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";
}
// Check if schema is an object (not a boolean schema)
if (typeof schema !== 'object' || schema === null) {
return 'unknown';
return refToName(schema.$ref, options.typeSuffix);
}
if (schema.$dynamicRef) {
if (schema.$dynamicRef === "#meta") {
return `Schema${options.typeSuffix ?? ""}`;
}
// 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;
return refToName(schema.$dynamicRef, options.typeSuffix);
}
if (schema.const !== void 0) {
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 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;
let enumUnion = JSON.stringify(schema.enum[0]);
for (let i = 1; i < schema.enum.length; i++) {
enumUnion += " | " + 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 enumUnion;
}
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 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;
return baseType;
}
if (!schema.type) {
if (schema.additionalProperties !== void 0) {
if (typeof schema.additionalProperties === "boolean") {
return recordType("string", getBooleanSubSchemaType(schema.additionalProperties), options);
}
return recordType("string", getTypeScriptType(schema.additionalProperties, options), 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.patternProperties && typeof schema.patternProperties === "object") {
const record = patternPropertiesRecordType(schema.patternProperties, options);
if (record !== void 0)
return record;
}
// 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;
if (schema.default !== void 0) {
if (typeof schema.default === "string") {
return "string";
}
if (typeof schema.default === "number") {
return "number";
}
if (typeof schema.default === "boolean") {
return "boolean";
}
}
// 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 "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]);
}
// 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 typeUnion;
}
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);
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 requiredSet = new Set(Array.isArray(schema.required) ? schema.required : []);
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 = requiredSet.has(key);
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 : void 0;
if (!first2)
properties2 += "\n";
first2 = false;
if (inlineDescription) {
properties2 += buildInlinePropertyComment(inlineDescription) + " " + readonlyPrefix + safeKey(key) + optional + ": " + propType + ";";
} else {
properties2 += " " + readonlyPrefix + safeKey(key) + optional + ": " + propType + ";";
}
return recordType('string', getTypeScriptType(schema.additionalProperties, options), options);
}
return "{\n" + properties2 + "\n}";
}
if (schema.patternProperties && typeof schema.patternProperties === 'object') {
const record = patternPropertiesRecordType(schema.patternProperties, options);
if (record !== undefined)
return record;
let properties = "";
let first = true;
for (const key in schema.properties) {
const propSchema = schema.properties[key];
const isRequired = requiredSet.has(key);
const optional = isRequired ? "" : "?";
const propType = getTypeScriptType(propSchema, options);
if (!first)
properties += "; ";
properties += readonlyPrefix + safeKey(key) + optional + ": " + propType;
first = false;
}
if (schema.default !== undefined) {
if (typeof schema.default === 'string') {
return 'string';
}
if (typeof schema.default === 'number') {
return 'number';
}
if (typeof schema.default === 'boolean') {
return 'boolean';
}
}
return 'unknown';
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 record = patternPropertiesRecordType(schema.patternProperties, options);
if (record !== void 0)
return record;
}
return "object";
// Default to unknown
default:
return "unknown";
}
};
const 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 || void 0;
if (topLevelComment) {
result += buildJsDocBlock(typeName, 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;
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 || void 0;
if (topLevelComment) {
jsDocTitle = typeName;
jsDocDescription = topLevelComment;
}
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 ' : '';
// Build the required-key set once instead of an O(required) `includes`
// per property (O(properties × required) for a wide object).
const requiredSet = new Set(Array.isArray(schema.required) ? schema.required : []);
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 = requiredSet.has(key);
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 +=
buildInlinePropertyComment(inlineDescription) +
' ' +
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 = requiredSet.has(key);
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 record = patternPropertiesRecordType(schema.patternProperties, options);
if (record !== undefined)
return record;
}
return 'object';
// Default to unknown
default:
return 'unknown';
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;
if (!hasProperties && hasPatternProperties && normalizedSchema.patternProperties) {
const record = patternPropertiesRecordType(normalizedSchema.patternProperties, options) ?? "Record<string, unknown>";
let result2 = "";
if (jsDocTitle && jsDocDescription) {
result2 += buildJsDocBlock(jsDocTitle, jsDocDescription);
}
result2 += `export type ${typeName} = ${record};`;
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;
if (!hasProperties && hasAdditionalProperties && 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;
}
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 schemaProps = normalizedSchema.properties ?? {};
const requiredSet = new Set(Array.isArray(normalizedSchema.required) ? normalizedSchema.required : []);
let properties = "";
let isFirstProp = true;
for (const key in schemaProps) {
const propSchema = schemaProps[key];
const isRequired = requiredSet.has(key);
const optional = isRequired ? "" : "?";
const propType = getTypeScriptType(propSchema, options);
const quotedKey = readonlyPrefix + safeKey(key);
if (!isFirstProp)
properties += "\n";
isFirstProp = false;
const inlineDescription = isSchemaObject(propSchema) && typeof propSchema.description === "string" ? propSchema.description : isSchemaObject(propSchema) && typeof propSchema.$comment === "string" ? propSchema.$comment : void 0;
if (inlineDescription) {
properties += buildInlinePropertyComment(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));
}
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 record = patternPropertiesRecordType(normalizedSchema.patternProperties, options) ?? 'Record<string, unknown>';
let result = '';
if (jsDocTitle && jsDocDescription) {
result += buildJsDocBlock(jsDocTitle, jsDocDescription);
}
result += `export type ${typeName} = ${record};`;
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 ?? {};
const requiredSet = new Set(Array.isArray(normalizedSchema.required) ? normalizedSchema.required : []);
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 = requiredSet.has(key);
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 +=
buildInlinePropertyComment(inlineDescription) + ' ' + 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;
}
}
return 'export type ' + typeName + ' = unknown;';
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;
}
return "export type " + typeName + " = unknown;";
};
export {
generateTypeDefinition
};

@@ -1,8 +0,6 @@

/** 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');
const hasRef = (value) => {
return typeof value === "object" && value !== null && !Array.isArray(value) && "$ref" in value && typeof value.$ref === "string";
};
export {
hasRef
};

@@ -1,14 +0,4 @@

/**
* 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);
const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
export {
isObject
};

@@ -1,51 +0,32 @@

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.
import { isSchemaObject } from "./schema-guards.js";
const MJST_EXTENSION_KEY = "x-mjst";
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 SUPPORTED_PRIMITIVES = /* @__PURE__ */ new Set(["bigint"]);
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;
if (!isSchemaObject(schema))
return void 0;
const extension = schema[MJST_EXTENSION_KEY];
if (typeof extension !== "object" || extension === null)
return void 0;
const value = extension[field];
return typeof value === "string" ? value : void 0;
};
/**
* 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;
const getMjstInstanceOf = (schema) => {
const instanceOf = readExtensionString(schema, "instanceOf");
return instanceOf !== void 0 && IDENTIFIER.test(instanceOf) ? instanceOf : void 0;
};
/**
* 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;
const getMjstPrimitive = (schema) => {
const primitive = readExtensionString(schema, "primitive");
return primitive !== void 0 && SUPPORTED_PRIMITIVES.has(primitive) ? primitive : void 0;
};
/**
* 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;
const getMjstBrand = (schema) => {
const brand = readExtensionString(schema, "brand");
return brand !== void 0 && SAFE_BRAND.test(brand) ? brand : void 0;
};
export {
MJST_EXTENSION_KEY,
getMjstBrand,
getMjstInstanceOf,
getMjstPrimitive
};

@@ -1,30 +0,16 @@

/**
* Emits code for a JSON Schema `multipleOf` check that agrees with the runtime
* interpreter (`packages/runtime-validators/src/interpreter/interpret.ts`).
*
* The naive `x % m === 0` is float-wrong: IEEE-754 makes `0.3 % 0.1` evaluate to
* `0.0999…`, so `0.3` would spuriously fail `multipleOf: 0.1`. The interpreter
* instead divides and compares the quotient to its nearest integer within a
* tolerance that *scales with the quotient's magnitude* — a fixed epsilon falsely
* rejects large values (e.g. `1234567.89` against `multipleOf: 0.01`) whose
* representation error in the quotient already exceeds it. Keeping the emitted
* check identical to the interpreter's keeps generated validators/parsers and the
* interpreter from disagreeing on the same document.
*
* `valueExpr` is inlined three times, so it must be a side-effect-free expression
* (a property read or a cached variable — which is all the generators ever pass).
*/
const quotientTolerance = (valueExpr, divisor) => {
const q = `${valueExpr} / ${divisor}`;
return { q, tol: `1e-8 * Math.max(1, Math.abs(${q}))` };
const q = `${valueExpr} / ${divisor}`;
return { q, tol: `1e-8 * Math.max(1, Math.abs(${q}))` };
};
/** Boolean expression that is TRUE when `valueExpr` is a valid multiple of `divisor`. */
export const multipleOfPassExpr = (valueExpr, divisor) => {
const { q, tol } = quotientTolerance(valueExpr, divisor);
return `Math.abs(${q} - Math.round(${q})) <= ${tol}`;
const multipleOfPassExpr = (valueExpr, divisor) => {
const { q, tol } = quotientTolerance(valueExpr, divisor);
return `Math.abs(${q} - Math.round(${q})) <= ${tol}`;
};
/** Boolean expression that is TRUE when `valueExpr` is NOT a multiple of `divisor` (the error condition). */
export const multipleOfFailExpr = (valueExpr, divisor) => {
const { q, tol } = quotientTolerance(valueExpr, divisor);
return `Math.abs(${q} - Math.round(${q})) > ${tol}`;
const multipleOfFailExpr = (valueExpr, divisor) => {
const { q, tol } = quotientTolerance(valueExpr, divisor);
return `Math.abs(${q} - Math.round(${q})) > ${tol}`;
};
export {
multipleOfFailExpr,
multipleOfPassExpr
};

@@ -1,136 +0,94 @@

/**
* 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;
const 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 escapedTitle = sectionTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const sectionRegex = new RegExp(`${escapedHashes}\\s+${escapedTitle}\\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("\n");
let inTable = false;
for (const line of lines) {
if (line.includes("Field Name") || line.includes("---|")) {
inTable = true;
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 (line.startsWith("#")) {
inTable = false;
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.
// `sectionTitle` comes from the URL fragment, so escape any regex metacharacters
// before interpolating — otherwise a fragment containing `+`, `(`, etc. throws a
// SyntaxError that the outer catch then mislabels as a fetch failure.
const escapedHashes = headingHashes.replace(/#/g, '\\#');
const escapedTitle = sectionTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const sectionRegex = new RegExp(`${escapedHashes}\\s+${escapedTitle}\\s*\\n([\\s\\S]*?)(?=\\n${escapedHashes}\\s|$)`, 'i');
const sectionMatch = markdown.match(sectionRegex);
if (!sectionMatch) {
return null;
if (!inTable || !line.trim() || !line.includes("|")) {
continue;
}
const sectionContent = sectionMatch?.[1];
if (!sectionContent) {
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
};
}
}
// 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,
};
}
}
}
}
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) {
return {
title,
description,
properties,
title,
description,
properties: fallback.properties
};
}
}
catch (error) {
console.error('Failed to fetch OpenAPI documentation:', error);
return null;
}
return {
title,
description,
properties
};
} catch (error) {
console.error("Failed to fetch OpenAPI documentation:", error);
return null;
}
};
export {
parseDocumentation
};

@@ -1,19 +0,5 @@

// Characters that force the JSON.stringify escaping path below: quote,
// backslash, C0 controls, and the JS line separators. Anything else sits
// verbatim inside a double-quoted literal.
// biome-ignore lint/suspicious/noControlCharactersInRegex: the C0 range is exactly the set that must never appear raw in a generated string literal
const NEEDS_ESCAPING = /["\\\u0000-\u001f\u2028\u2029]/;
/**
* Emits `text` as a double-quoted JS string literal for generated code.
*
* This is the one escape-or-quote decision for schema-controlled text
* (property names, patterns, enum values) embedded in generated source — a key
* like `it's` or a pattern with a quote or newline must never break out of the
* literal or inject code, so anything carrying such a character goes through
* `JSON.stringify`. The common all-plain string skips the full escaper: code
* generators emit one literal per assertion/message, and the stringify calls
* were a measurable slice of generation time. Centralized so the next
* generator wanting the fast path cannot get the security-sensitive regex
* subtly wrong.
*/
export const quoteJsString = (text) => (NEEDS_ESCAPING.test(text) ? JSON.stringify(text) : `"${text}"`);
const quoteJsString = (text) => NEEDS_ESCAPING.test(text) ? JSON.stringify(text) : `"${text}"`;
export {
quoteJsString
};

@@ -1,108 +0,43 @@

/**
* 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 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();
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)}`;
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 = /* @__PURE__ */ 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 !== void 0 && 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 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;
const 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;
};
export {
refToFilename,
toKebabCase
};

@@ -1,55 +0,19 @@

import { refToFilename } from './ref-to-filename.js';
/**
* Converts a filename-like string to a PascalCase TypeScript identifier,
* appending an optional suffix.
*
* Word boundaries are *any* run of characters that cannot appear in a JS
* identifier — not just `-` — so a dotted key like `io.k8s.api.core.v1.Pod`
* becomes `IoK8sApiCoreV1Pod` instead of the invalid `Io.k8s.api.core.v1.pod`.
* A leading digit is prefixed with `_`, and an otherwise-empty result falls back
* to `_`, so the output is always a usable identifier.
*
* @example
* ```ts
* kebabToPascal('server-variable') // 'ServerVariable'
* kebabToPascal('channel') // 'Channel'
* kebabToPascal('channel', 'Object') // 'ChannelObject'
* kebabToPascal('io.k8s.api.core.v1.pod') // 'IoK8sApiCoreV1Pod'
* kebabToPascal('123abc') // '_123abc'
* ```
*/
import { refToFilename } from "./ref-to-filename.js";
const kebabToPascal = (kebab, suffix) => {
const words = kebab.split(/[^A-Za-z0-9]+/);
let pascalCase = '';
for (const word of words) {
if (word === '')
continue;
pascalCase += word.charAt(0).toUpperCase() + word.slice(1);
}
if (pascalCase === '')
pascalCase = '_';
else if (/^[0-9]/.test(pascalCase))
pascalCase = `_${pascalCase}`;
return pascalCase + suffix;
const words = kebab.split(/[^A-Za-z0-9]+/);
let pascalCase = "";
for (const word of words) {
if (word === "")
continue;
pascalCase += word.charAt(0).toUpperCase() + word.slice(1);
}
if (pascalCase === "")
pascalCase = "_";
else if (/^[0-9]/.test(pascalCase))
pascalCase = `_${pascalCase}`;
return pascalCase + suffix;
};
/**
* 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);
const refToName = (ref, suffix = "") => kebabToPascal(refToFilename(ref), suffix);
export {
refToName
};

@@ -1,69 +0,55 @@

/**
* 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).
*
* Walks both object properties and array elements, so a `$dynamicRef` nested
* inside a keyword whose value is an array of subschemas (`allOf`, `anyOf`,
* `oneOf`, `prefixItems`, …) is rewritten too. The build system generates a
* separate file per `$def`, so each schema walked here is relatively shallow.
*/
export const resolveDynamicRefs = (schema, dynamicRefMap) => {
if (typeof schema !== 'object' || schema === null) {
return schema;
const resolveDynamicRefs = (schema, dynamicRefMap) => {
if (typeof schema !== "object" || schema === null) {
return schema;
}
if (Object.keys(dynamicRefMap).length === 0) {
return schema;
}
if (!containsDynamicRef(schema)) {
return schema;
}
const clone = JSON.parse(JSON.stringify(schema));
const walk = (obj) => {
if (typeof obj !== "object" || obj === null) {
return;
}
// Skip if there are no dynamic refs to resolve
if (Object.keys(dynamicRefMap).length === 0) {
return schema;
if (Array.isArray(obj)) {
for (const item of obj)
walk(item);
return;
}
// Cheap read-only pre-scan: most subschemas carry no `$dynamicRef`, so returning
// the original untouched avoids a full deep clone (and its allocation) on every
// node of every generator's walk when the document as a whole *does* use them.
if (!containsDynamicRef(schema)) {
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"];
}
}
const clone = JSON.parse(JSON.stringify(schema));
const walk = (obj) => {
if (typeof obj !== 'object' || obj === null) {
return;
}
if (Array.isArray(obj)) {
for (const item of obj)
walk(item);
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;
for (const key in record) {
walk(record[key]);
}
};
walk(clone);
return clone;
};
/** True if `value` contains a `$dynamicRef` string anywhere in its subtree. */
const containsDynamicRef = (value) => {
if (typeof value !== 'object' || value === null)
return false;
if (Array.isArray(value)) {
for (const item of value)
if (containsDynamicRef(item))
return true;
return false;
}
const record = value;
if (typeof record['$dynamicRef'] === 'string')
if (typeof value !== "object" || value === null)
return false;
if (Array.isArray(value)) {
for (const item of value)
if (containsDynamicRef(item))
return true;
for (const key in record)
if (containsDynamicRef(record[key]))
return true;
return false;
}
const record = value;
if (typeof record["$dynamicRef"] === "string")
return true;
for (const key in record)
if (containsDynamicRef(record[key]))
return true;
return false;
};
export {
resolveDynamicRefs
};

@@ -1,77 +0,41 @@

/**
* 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;
}
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 void 0;
}
} else {
return void 0;
}
return current;
}
return current;
};
/**
* 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);
const 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 void 0;
const defsRecord = defs;
const base = defsRecord[baseUri];
if (typeof base !== "object" || base === null)
return void 0;
if (!fragment)
return base;
const normalizedFragment = fragment.replace(/^\/definitions\//, "/$defs/");
return navigatePointer(normalizedFragment, base);
};
export {
resolveRef
};

@@ -1,48 +0,21 @@

/**
* 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}`;
}
// Bracket keys are schema-controlled, so escape via JSON.stringify — a key like
// `it's` or `a']; evil(); //` would otherwise break or hijack the generated code.
const literal = JSON.stringify(key);
// Handle optional chaining: "input?" -> "input?.['key']"
if (variable.endsWith('?')) {
return `${variable}.[${literal}]`;
}
return `${variable}[${literal}]`;
const safeAccessor = (variable, key) => {
if (JS_IDENTIFIER.test(key)) {
return `${variable}.${key}`;
}
const literal = JSON.stringify(key);
if (variable.endsWith("?")) {
return `${variable}.[${literal}]`;
}
return `${variable}[${literal}]`;
};
/**
* 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;
}
// Schema-controlled keys must be escaped; a bare-quoted `it's` produces broken TS.
return JSON.stringify(key);
const safeKey = (key) => {
if (JS_IDENTIFIER.test(key)) {
return key;
}
return JSON.stringify(key);
};
export {
safeAccessor,
safeKey
};

@@ -1,175 +0,158 @@

/** Type guard to check if schema is not false */
export const isSchemaObject = (schema) => {
return typeof schema === 'object' && schema !== null && typeof schema !== 'boolean';
const 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';
const 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);
const 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);
const 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);
const 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;
const 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';
const 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';
const 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;
const 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);
const 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);
const 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);
const 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);
const 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);
const 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');
const 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;
const 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';
const 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';
const 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';
const 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';
const 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';
const 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';
const hasExclusiveMaximum = (schema) => {
return isSchemaObject(schema) && "exclusiveMaximum" in schema && typeof schema.exclusiveMaximum === "number";
};
/**
* Draft-04 expressed a strict lower bound as a boolean `exclusiveMinimum: true`
* paired with `minimum`, where draft-06+ uses a standalone numeric keyword. True
* only for that legacy boolean form, so callers can tighten the `minimum` compare.
*/
export const hasStrictExclusiveMinimum = (schema) => {
if (!isSchemaObject(schema))
return false;
const flag = schema.exclusiveMinimum;
return flag === true;
const hasStrictExclusiveMinimum = (schema) => {
if (!isSchemaObject(schema))
return false;
const flag = schema.exclusiveMinimum;
return flag === true;
};
/** Draft-04 boolean `exclusiveMaximum: true` paired with `maximum`. See {@link hasStrictExclusiveMinimum}. */
export const hasStrictExclusiveMaximum = (schema) => {
if (!isSchemaObject(schema))
return false;
const flag = schema.exclusiveMaximum;
return flag === true;
const hasStrictExclusiveMaximum = (schema) => {
if (!isSchemaObject(schema))
return false;
const flag = schema.exclusiveMaximum;
return flag === true;
};
/** Type guard to check if schema has multipleOf */
export const hasMultipleOf = (schema) => {
return isSchemaObject(schema) && 'multipleOf' in schema && typeof schema.multipleOf === 'number';
const 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';
const 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';
const 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';
const 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';
const 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';
const hasMaxProperties = (schema) => {
return isSchemaObject(schema) && "maxProperties" in schema && typeof schema.maxProperties === "number";
};
/** Type guard to check if schema has dependentRequired (2020-12). */
export const hasDependentRequired = (schema) => {
return (isSchemaObject(schema) &&
'dependentRequired' in schema &&
typeof schema.dependentRequired === 'object' &&
schema.dependentRequired !== null);
const hasDependentRequired = (schema) => {
return isSchemaObject(schema) && "dependentRequired" in schema && typeof schema.dependentRequired === "object" && schema.dependentRequired !== null;
};
/** Type guard to check if schema has a propertyNames subschema. */
export const hasPropertyNames = (schema) => {
return isSchemaObject(schema) && 'propertyNames' in schema;
const hasPropertyNames = (schema) => {
return isSchemaObject(schema) && "propertyNames" in schema;
};
/** Type guard to check if schema has dependentSchemas (2020-12). */
export const hasDependentSchemas = (schema) => {
return (isSchemaObject(schema) &&
'dependentSchemas' in schema &&
typeof schema.dependentSchemas === 'object' &&
schema.dependentSchemas !== null);
const hasDependentSchemas = (schema) => {
return isSchemaObject(schema) && "dependentSchemas" in schema && typeof schema.dependentSchemas === "object" && schema.dependentSchemas !== null;
};
/** Type guard to check if schema has patternProperties. */
export const hasPatternProperties = (schema) => {
return (isSchemaObject(schema) &&
'patternProperties' in schema &&
typeof schema.patternProperties === 'object' &&
schema.patternProperties !== null);
const hasPatternProperties = (schema) => {
return isSchemaObject(schema) && "patternProperties" in schema && typeof schema.patternProperties === "object" && schema.patternProperties !== null;
};
/** Type guard to check if schema has a `contains` subschema (array). */
export const hasContains = (schema) => {
return isSchemaObject(schema) && 'contains' in schema;
const hasContains = (schema) => {
return isSchemaObject(schema) && "contains" in schema;
};
/** Type guard to check if schema has a `not` subschema. */
export const hasNot = (schema) => {
return isSchemaObject(schema) && 'not' in schema;
const hasNot = (schema) => {
return isSchemaObject(schema) && "not" in schema;
};
/** Type guard to check if schema has an `if` subschema (with optional then/else). */
export const hasIf = (schema) => {
return isSchemaObject(schema) && 'if' in schema;
const hasIf = (schema) => {
return isSchemaObject(schema) && "if" in schema;
};
export { hasRef } from './has-ref.js';
import { hasRef } from "./has-ref.js";
export {
hasAdditionalProperties,
hasAllOf,
hasAnyOf,
hasConst,
hasContains,
hasDefault,
hasDependentRequired,
hasDependentSchemas,
hasEnum,
hasExamples,
hasExclusiveMaximum,
hasExclusiveMinimum,
hasFormat,
hasIf,
hasItems,
hasMaxItems,
hasMaxLength,
hasMaxProperties,
hasMaximum,
hasMinItems,
hasMinLength,
hasMinProperties,
hasMinimum,
hasMultipleOf,
hasNot,
hasOneOf,
hasPattern,
hasPatternProperties,
hasProperties,
hasPropertyNames,
hasRef,
hasRequired,
hasStrictExclusiveMaximum,
hasStrictExclusiveMinimum,
hasType,
hasUniqueItems,
isObjectSchema,
isSchemaObject
};

@@ -1,40 +0,22 @@

/**
* Default key-count cutoff below which an unknown-key sweep inlines `!==`
* comparisons instead of a hoisted `Set`. Chosen so typical schema objects stay
* on the faster inline path while pathologically wide objects fall back to the
* `Set`'s O(1) lookup.
*/
export const INLINE_KEY_LIMIT = 16;
/**
* Builds the "is this an undeclared key" test used by `additionalProperties:
* false` sweeps in the generated parsers and validators.
*
* For a small number of known keys V8 evaluates a chain of `!==` string
* comparisons faster than `Set.has` (which has to hash the string), and the
* inline form skips the per-module `Set` allocation — the same shape Ajv and
* TypeBox compile to. Above `inlineLimit` the chain grows long enough that the
* `Set`'s O(1) lookup wins, so a `Set` named `setName` is hoisted instead.
*
* @example
* const check = unknownKeyCheck(['id', 'name'], '_knownKeys0')
* check.declarations // []
* check.isUnknown('_k') // '_k !== "id" && _k !== "name"'
* check.isKnown('_k') // '_k === "id" || _k === "name"'
*/
export const unknownKeyCheck = (knownKeys, setName, inlineLimit = INLINE_KEY_LIMIT) => {
if (knownKeys.length === 0) {
return { declarations: [], isUnknown: () => 'true', isKnown: () => 'false' };
}
if (knownKeys.length <= inlineLimit) {
return {
declarations: [],
isUnknown: (keyVar) => knownKeys.map((key) => `${keyVar} !== ${JSON.stringify(key)}`).join(' && '),
isKnown: (keyVar) => knownKeys.map((key) => `${keyVar} === ${JSON.stringify(key)}`).join(' || '),
};
}
const INLINE_KEY_LIMIT = 16;
const unknownKeyCheck = (knownKeys, setName, inlineLimit = INLINE_KEY_LIMIT) => {
if (knownKeys.length === 0) {
return { declarations: [], isUnknown: () => "true", isKnown: () => "false" };
}
if (knownKeys.length <= inlineLimit) {
return {
declarations: [`const ${setName} = new Set(${JSON.stringify(knownKeys)})`],
isUnknown: (keyVar) => `!${setName}.has(${keyVar})`,
isKnown: (keyVar) => `${setName}.has(${keyVar})`,
declarations: [],
isUnknown: (keyVar) => knownKeys.map((key) => `${keyVar} !== ${JSON.stringify(key)}`).join(" && "),
isKnown: (keyVar) => knownKeys.map((key) => `${keyVar} === ${JSON.stringify(key)}`).join(" || ")
};
}
return {
declarations: [`const ${setName} = new Set(${JSON.stringify(knownKeys)})`],
isUnknown: (keyVar) => `!${setName}.has(${keyVar})`,
isKnown: (keyVar) => `${setName}.has(${keyVar})`
};
};
export {
INLINE_KEY_LIMIT,
unknownKeyCheck
};

@@ -1,160 +0,97 @@

/**
* 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.
*/
import { refToFilename, toKebabCase } from "./ref-to-filename.js";
const isDraft07Schema = (schema) => typeof schema["$schema"] === "string" && schema["$schema"].includes("draft-07");
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);
}
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;
}
return result;
};
/**
* 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);
}
const hoisted = {};
for (const [parentName, parentSchema] of Object.entries(defs)) {
if (typeof parentSchema !== "object" || parentSchema === null) {
hoisted[parentName] = parentSchema;
continue;
}
return hoisted;
};
/**
* 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 = /* @__PURE__ */ new Map();
for (const localName of Object.keys(nestedDefs)) {
const hoistedName = `${parentPrefix}-${toKebabCase(localName)}`;
localToHoisted.set(`#/$defs/${localName}`, `#/$defs/${hoistedName}`);
}
return {
...rest,
$defs: hoistedDefs,
};
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;
};
/**
* 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 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];
}
}
}
return {
...rest,
$defs: hoistedDefs
};
};
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);
}
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;
}
return result;
};
export {
isDraft07Schema,
upgradeDraft07Schema
};

@@ -1,33 +0,22 @@

/**
* Parses the items of an array with a parser function.
*
* When every element parses to the very same reference — the parser found it
* already valid and handed it back, as the generated item sub-parsers do for
* clean values — the input array itself is returned and nothing is allocated.
* The result array is only materialized (lazily, on the first element the
* parser actually replaced) when something was coerced or stripped, so the
* common all-clean parse costs no allocation at all. Callers already share
* element references either way; sharing the container mirrors the generated
* fast paths, which return the input array by reference too.
*/
export const validateArray = (input, parser) => {
if (!Array.isArray(input)) {
return [];
const validateArray = (input, parser) => {
if (!Array.isArray(input)) {
return [];
}
const len = input.length;
let result = null;
for (let i = 0; i < len; i++) {
const parsed = parser(input[i]);
if (result !== null) {
result[i] = parsed;
} else if (parsed !== input[i]) {
result = new Array(len);
for (let j = 0; j < i; j++)
result[j] = input[j];
result[i] = parsed;
}
const len = input.length;
let result = null;
for (let i = 0; i < len; i++) {
const parsed = parser(input[i]);
if (result !== null) {
result[i] = parsed;
}
else if (parsed !== input[i]) {
// First replaced element: materialize the copy and backfill the prefix.
result = new Array(len);
for (let j = 0; j < i; j++)
result[j] = input[j];
result[i] = parsed;
}
}
return result ?? input;
}
return result ?? input;
};
export {
validateArray
};

@@ -1,26 +0,20 @@

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 {};
import { isObject } from "./is-object.js";
const validateRecord = (input, parser) => {
if (!isObject(input)) {
return {};
}
const record = input;
const result = {};
for (const key in record) {
const value = parser(record[key]);
if (key === "__proto__") {
Object.defineProperty(result, key, { value, writable: true, enumerable: true, configurable: true });
} else {
result[key] = value;
}
const record = input;
const result = {};
for (const key in record) {
const value = parser(record[key]);
// A plain assignment of `__proto__` invokes the prototype setter and
// corrupts `result`'s prototype (a prototype-pollution vector for untrusted
// input). Define it as an own data property instead so it round-trips as a
// normal key, matching every other property.
if (key === '__proto__') {
Object.defineProperty(result, key, { value, writable: true, enumerable: true, configurable: true });
}
else {
result[key] = value;
}
}
return result;
}
return result;
};
export {
validateRecord
};

@@ -1,172 +0,127 @@

import { buildDynamicRefMap } from './build-dynamic-ref-map.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();
/**
* Keywords that give a schema a shape of its own. A root carrying any of these
* next to its `$ref` is a real composition, not a pure alias, and keeps the
* default root handling.
*/
import { buildDynamicRefMap } from "./build-dynamic-ref-map.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 = /* @__PURE__ */ new WeakMap();
const SHAPE_KEYWORDS = [
'properties',
'type',
'oneOf',
'anyOf',
'allOf',
'patternProperties',
'additionalProperties',
'enum',
'const',
'items',
'prefixItems',
'required',
'if',
'then',
'else',
'not',
"properties",
"type",
"oneOf",
"anyOf",
"allOf",
"patternProperties",
"additionalProperties",
"enum",
"const",
"items",
"prefixItems",
"required",
"if",
"then",
"else",
"not"
];
/** The `$ref` of a pure-alias root document (only `$ref` + metadata keys), or null. */
const getAliasRootRef = (schema) => {
if (typeof schema !== 'object' || schema === null)
return null;
const record = schema;
if (typeof record['$ref'] !== 'string')
return null;
return SHAPE_KEYWORDS.some((key) => key in record) ? null : record['$ref'];
if (typeof schema !== "object" || schema === null)
return null;
const record = schema;
if (typeof record["$ref"] !== "string")
return null;
return SHAPE_KEYWORDS.some((key) => key in record) ? null : record["$ref"];
};
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;
if (typeof rootSchema !== "object" || rootSchema === null) {
const upgraded2 = rootSchema;
return { upgraded: upgraded2, dynamicRefMap: {}, resolveRefCache: /* @__PURE__ */ new Map(), extractRefsCache: /* @__PURE__ */ new WeakMap() };
}
const existing = rootCaches.get(rootSchema);
if (existing)
return existing;
const upgraded = upgradeDraft07Schema(rootSchema);
const cache = {
upgraded,
dynamicRefMap: buildDynamicRefMap(upgraded),
resolveRefCache: /* @__PURE__ */ new Map(),
extractRefsCache: /* @__PURE__ */ 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;
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;
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);
// An alias root (a document that is just `$ref: '#/$defs/x'`) whose derived
// filename equals its target's would reserve the filename for a wrapper that
// re-exports the target — and the target, mapping to the same file, would
// then never be generated, leaving a file that imports itself. Generate the
// *target's* schema under the root's name instead, carrying the ref so
// generators can exclude the self-import.
let rootNodeSchema = upgraded;
let rootNodeRef;
const aliasRef = getAliasRootRef(upgraded);
if (aliasRef && refToFilename(aliasRef) === rootFilename) {
const resolved = cachedResolveRef(cache, aliasRef);
if (resolved) {
rootNodeSchema = resolved;
rootNodeRef = aliasRef;
}
const walkRefGraph = (rootSchema, rootTypeName, options, visit) => {
const typeSuffix = options.typeSuffix ?? "";
const cache = getRootCache(rootSchema);
const { upgraded, dynamicRefMap } = cache;
const processedRefs = /* @__PURE__ */ new Set();
const processedFilenames = /* @__PURE__ */ new Set();
const rootFilename = rootTypeName.toLowerCase();
processedFilenames.add(rootFilename);
let rootNodeSchema = upgraded;
let rootNodeRef;
const aliasRef = getAliasRootRef(upgraded);
if (aliasRef && refToFilename(aliasRef) === rootFilename) {
const resolved = cachedResolveRef(cache, aliasRef);
if (resolved) {
rootNodeSchema = resolved;
rootNodeRef = aliasRef;
}
visit({
ref: rootNodeRef,
typeName: rootTypeName,
filename: rootFilename,
schema: resolveDynamicRefs(rootNodeSchema, dynamicRefMap),
}
visit({
ref: rootNodeRef,
typeName: rootTypeName,
filename: rootFilename,
schema: resolveDynamicRefs(rootNodeSchema, dynamicRefMap),
rootSchema: upgraded,
isRoot: true
});
const queue = [...cachedExtractRefs(cache, upgraded), ...Object.values(dynamicRefMap)];
for (let head = 0; head < queue.length; head++) {
const ref = queue[head];
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: true,
});
// Seed dynamic-anchor targets from the memoized map instead of calling
// extractDynamicAnchorDefs (which would re-walk the whole document — the map
// already holds exactly those refs as its values).
const queue = [...cachedExtractRefs(cache, upgraded), ...Object.values(dynamicRefMap)];
// Advance a read cursor instead of `queue.shift()`, whose O(n) element move
// makes draining a large ref graph quadratic.
for (let head = 0; head < queue.length; head++) {
const ref = queue[head];
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);
}
isRoot: false
});
}
for (const nested of cachedExtractRefs(cache, resolved)) {
if (!processedRefs.has(nested))
queue.push(nested);
}
}
};
export {
walkRefGraph
};
{
"name": "@amritk/helpers",
"version": "0.13.2",
"version": "0.13.3",
"description": "Shared utilities for the mjst code generation ecosystem.",

@@ -31,3 +31,3 @@ "type": "module",

"scripts": {
"build": "tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f",
"build": "tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f && node ../../scripts/strip-comments.mjs",
"types:check": "tsgo -p . --noEmit",

@@ -34,0 +34,0 @@ "test": "NODE_ENV=production vitest run --root ../.. helpers"