@kubb/ast 5.0.0-beta.30 → 5.0.0-beta.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -201,60 +201,6 @@ const mediaTypes = {
201
201
  videoMp4: "video/mp4"
202
202
  };
203
203
  //#endregion
204
- //#region src/dialect.ts
205
- /**
206
- * Identity helper that types a {@link SchemaDialect} for an adapter. Like
207
- * `defineParser`, it adds no runtime behavior — it pins the dialect's type for
208
- * inference and gives adapter authors a discoverable anchor.
209
- *
210
- * @example
211
- * ```ts
212
- * export const oasDialect = defineSchemaDialect({
213
- * name: 'oas',
214
- * isNullable,
215
- * isReference,
216
- * isDiscriminator,
217
- * isBinary: (schema) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
218
- * resolveRef,
219
- * })
220
- * ```
221
- */
222
- function defineSchemaDialect(dialect) {
223
- return dialect;
224
- }
225
- //#endregion
226
- //#region src/dispatch.ts
227
- /**
228
- * Walks an ordered list of {@link DispatchRule}s and returns the first node produced.
229
- *
230
- * This is the shared backbone for spec adapters (OpenAPI today, AsyncAPI and others later).
231
- * The contract an adapter follows is intentionally minimal:
232
- *
233
- * context → [rule.match → rule.convert] → node
234
- *
235
- * An adapter derives a context from a source spec node, then declares an ordered table of
236
- * rules mapping spec shapes onto Kubb AST nodes. To add support for a new spec, write a new
237
- * context type and a new rules table — the traversal here is reused unchanged.
238
- *
239
- * Order is significant: earlier rules win, so list higher-precedence or more specific shapes
240
- * first (e.g. composition keywords before plain `type`). A rule whose `match` returns `true`
241
- * may still `convert` to `null` to defer to later rules. When no rule produces a node this
242
- * returns `null`, leaving the caller to apply its own fallback.
243
- *
244
- * @example
245
- * ```ts
246
- * const node = dispatch(schemaRules, schemaContext) ?? createSchema({ type: fallbackType })
247
- * ```
248
- */
249
- function dispatch(rules, context) {
250
- for (const rule of rules) {
251
- if (!rule.match(context)) continue;
252
- const node = rule.convert(context);
253
- if (node !== null && node !== void 0) return node;
254
- }
255
- return null;
256
- }
257
- //#endregion
258
204
  //#region ../../internals/utils/src/casing.ts
259
205
  /**
260
206
  * Shared implementation for camelCase and PascalCase conversion.
@@ -2115,6 +2061,277 @@ function createJsx(value) {
2115
2061
  };
2116
2062
  }
2117
2063
  //#endregion
2064
+ //#region src/signature.ts
2065
+ /**
2066
+ * The shape-affecting flags shared by every node kind: base primitive, format, and `nullable`.
2067
+ * Documentation and usage-slot flags (`optional`/`nullish`/`readOnly`/`writeOnly`) are
2068
+ * intentionally excluded — they describe the property slot, not the type.
2069
+ */
2070
+ function flagsDescriptor(node) {
2071
+ return `${node.primitive ?? ""};${node.format ?? ""};${node.nullable ? 1 : 0}`;
2072
+ }
2073
+ function refTargetName(node) {
2074
+ if (node.ref) return extractRefName(node.ref);
2075
+ return node.name ?? "";
2076
+ }
2077
+ /**
2078
+ * Builds the local, shape-only descriptor for a node: its kind, flags, constraints, and its
2079
+ * children's signatures. {@link signatureOf} hashes this string; children contribute their
2080
+ * fixed-length signature rather than their own full descriptor, which keeps the result bounded.
2081
+ */
2082
+ function describeShape(node, signatures) {
2083
+ const flags = flagsDescriptor(node);
2084
+ switch (node.type) {
2085
+ case "object": {
2086
+ const props = (node.properties ?? []).map((prop) => `${prop.name}${prop.required ? "!" : "?"}${signatureOf(prop.schema, signatures)}`).join(",");
2087
+ let additional = "";
2088
+ if (typeof node.additionalProperties === "boolean") additional = `ab:${node.additionalProperties}`;
2089
+ else if (node.additionalProperties) additional = `as:${signatureOf(node.additionalProperties, signatures)}`;
2090
+ const pattern = node.patternProperties ? Object.keys(node.patternProperties).sort().map((key) => `${key}=${signatureOf(node.patternProperties[key], signatures)}`).join(",") : "";
2091
+ return `object|${flags}|p[${props}]|${additional}|pp[${pattern}]|mn:${node.minProperties ?? ""}|mx:${node.maxProperties ?? ""}`;
2092
+ }
2093
+ case "array":
2094
+ case "tuple": {
2095
+ const items = (node.items ?? []).map((item) => signatureOf(item, signatures)).join(",");
2096
+ const rest = node.rest ? signatureOf(node.rest, signatures) : "";
2097
+ return `${node.type}|${flags}|i[${items}]|r:${rest}|mn:${node.min ?? ""}|mx:${node.max ?? ""}|u:${node.unique ? 1 : 0}`;
2098
+ }
2099
+ case "union": {
2100
+ const members = (node.members ?? []).map((member) => signatureOf(member, signatures)).join(",");
2101
+ return `union|${flags}|s:${node.strategy ?? ""}|d:${node.discriminatorPropertyName ?? ""}|m[${members}]`;
2102
+ }
2103
+ case "intersection": return `intersection|${flags}|m[${(node.members ?? []).map((member) => signatureOf(member, signatures)).join(",")}]`;
2104
+ case "enum": {
2105
+ let values = "";
2106
+ if (node.namedEnumValues?.length) values = node.namedEnumValues.map((entry) => `${entry.name}=${entry.primitive}:${String(entry.value)}`).join(",");
2107
+ else if (node.enumValues?.length) values = node.enumValues.map((value) => `${value === null ? "null" : typeof value}:${String(value)}`).join(",");
2108
+ return `enum|${flags}|v[${values}]`;
2109
+ }
2110
+ case "ref": return `ref|${flags}|->${refTargetName(node)}`;
2111
+ case "string": return `string|${flags}|mn:${node.min ?? ""}|mx:${node.max ?? ""}|pt:${node.pattern ?? ""}`;
2112
+ case "number":
2113
+ case "integer":
2114
+ case "bigint": return `${node.type}|${flags}|mn:${node.min ?? ""}|mx:${node.max ?? ""}|emn:${node.exclusiveMinimum ?? ""}|emx:${node.exclusiveMaximum ?? ""}|mo:${node.multipleOf ?? ""}`;
2115
+ case "url": return `url|${flags}|path:${node.path ?? ""}|mn:${node.min ?? ""}|mx:${node.max ?? ""}`;
2116
+ case "uuid":
2117
+ case "email": return `${node.type}|${flags}|mn:${node.min ?? ""}|mx:${node.max ?? ""}`;
2118
+ case "datetime": return `datetime|${flags}|o:${node.offset ? 1 : 0}|l:${node.local ? 1 : 0}`;
2119
+ case "date":
2120
+ case "time": return `${node.type}|${flags}|rep:${node.representation}`;
2121
+ default: return `${node.type}|${flags}`;
2122
+ }
2123
+ }
2124
+ /**
2125
+ * Hash-consing: each node's signature is a fixed-length digest of its local shape plus its
2126
+ * children's digests (a Merkle hash). Children contribute their 64-char hash instead of their
2127
+ * full nested descriptor, so a signature stays bounded regardless of subtree depth, and the
2128
+ * digest is identical across calls because it depends only on content — never on traversal
2129
+ * order. This keeps the keys built during planning consistent with the ones recomputed later
2130
+ * during streaming. `signatures` memoizes node → digest within a single computation.
2131
+ */
2132
+ function signatureOf(node, signatures) {
2133
+ const cached = signatures.get(node);
2134
+ if (cached !== void 0) return cached;
2135
+ const signature = createHash("sha256").update(describeShape(node, signatures)).digest("hex");
2136
+ signatures.set(node, signature);
2137
+ return signature;
2138
+ }
2139
+ /**
2140
+ * Computes a deterministic, shape-only signature (a fixed-length content hash) for a schema node.
2141
+ *
2142
+ * Two schemas share a signature when they are structurally identical, ignoring
2143
+ * documentation (`name`, `title`, `description`, `example`, `default`, `deprecated`)
2144
+ * and usage-slot flags (`optional`, `nullish`, `readOnly`, `writeOnly`). `nullable`
2145
+ * is kept because it changes the produced type. `ref` nodes compare by target name,
2146
+ * which also keeps the algorithm terminating on circular shapes.
2147
+ *
2148
+ * @example Two enums with different descriptions share a signature
2149
+ * ```ts
2150
+ * schemaSignature(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'], description: 'x' })) ===
2151
+ * schemaSignature(createSchema({ type: 'enum', primitive: 'string', enumValues: ['a', 'b'] }))
2152
+ * ```
2153
+ */
2154
+ function schemaSignature(node) {
2155
+ return signatureOf(node, /* @__PURE__ */ new Map());
2156
+ }
2157
+ /**
2158
+ * Returns `true` when two schema nodes are structurally identical under shape-only equality.
2159
+ *
2160
+ * @example
2161
+ * ```ts
2162
+ * isSchemaEqual(a, b) // a and b produce the same TypeScript type
2163
+ * ```
2164
+ */
2165
+ function isSchemaEqual(a, b) {
2166
+ return schemaSignature(a) === schemaSignature(b);
2167
+ }
2168
+ //#endregion
2169
+ //#region src/dedupe.ts
2170
+ /**
2171
+ * Builds the shared `ref` replacement for a duplicate occurrence, carrying the
2172
+ * usage-slot and documentation fields that are not part of the canonical type.
2173
+ */
2174
+ function createRefNode(node, canonical) {
2175
+ return createSchema({
2176
+ type: "ref",
2177
+ name: canonical.name,
2178
+ ref: canonical.ref,
2179
+ optional: node.optional,
2180
+ nullish: node.nullish,
2181
+ readOnly: node.readOnly,
2182
+ writeOnly: node.writeOnly,
2183
+ deprecated: node.deprecated,
2184
+ description: node.description,
2185
+ default: node.default,
2186
+ example: node.example
2187
+ });
2188
+ }
2189
+ function applyDedupe(node, canonicalBySignature, skipRootMatch = false) {
2190
+ if (canonicalBySignature.size === 0) return node;
2191
+ const signatures = /* @__PURE__ */ new Map();
2192
+ const root = node;
2193
+ return transform(node, { schema(schemaNode) {
2194
+ const signature = signatureOf(schemaNode, signatures);
2195
+ if (skipRootMatch && schemaNode === root) return void 0;
2196
+ const canonical = canonicalBySignature.get(signature);
2197
+ if (!canonical) return void 0;
2198
+ return createRefNode(schemaNode, canonical);
2199
+ } });
2200
+ }
2201
+ /**
2202
+ * Strips usage-slot flags from a hoisted definition and applies its canonical name.
2203
+ * A standalone definition is never optional, so `optional`/`nullish` are cleared.
2204
+ */
2205
+ function cleanDefinition(node, name) {
2206
+ return {
2207
+ ...node,
2208
+ name,
2209
+ optional: void 0,
2210
+ nullish: void 0
2211
+ };
2212
+ }
2213
+ /**
2214
+ * Scans a forest of schema and operation nodes and produces a {@link DedupePlan}.
2215
+ *
2216
+ * A shape that occurs at least `minOccurrences` times is deduplicated: if any occurrence
2217
+ * is a named top-level schema, that name becomes the canonical (so other top-level duplicates
2218
+ * and inline copies turn into references to it); otherwise a new definition is hoisted using
2219
+ * `nameFor`. The plan is then applied per node with {@link applyDedupe}.
2220
+ *
2221
+ * @example
2222
+ * ```ts
2223
+ * const plan = buildDedupePlan([...schemaNodes, ...operationNodes], {
2224
+ * isCandidate: (node) => node.type === 'enum' || node.type === 'object',
2225
+ * nameFor: (node) => node.name ?? null,
2226
+ * refFor: (name) => `#/components/schemas/${name}`,
2227
+ * })
2228
+ * ```
2229
+ */
2230
+ function buildDedupePlan(roots, options) {
2231
+ const { isCandidate, nameFor, refFor, minOccurrences = 2 } = options;
2232
+ const signatures = /* @__PURE__ */ new Map();
2233
+ const topLevelNodes = /* @__PURE__ */ new Set();
2234
+ const groups = /* @__PURE__ */ new Map();
2235
+ function record(schemaNode) {
2236
+ const signature = signatureOf(schemaNode, signatures);
2237
+ if (!isCandidate(schemaNode)) return;
2238
+ const isTopLevel = topLevelNodes.has(schemaNode) && !!schemaNode.name;
2239
+ const group = groups.get(signature);
2240
+ if (group) {
2241
+ group.count++;
2242
+ if (isTopLevel && !group.topLevelName) group.topLevelName = schemaNode.name;
2243
+ } else groups.set(signature, {
2244
+ count: 1,
2245
+ representative: schemaNode,
2246
+ topLevelName: isTopLevel ? schemaNode.name : void 0
2247
+ });
2248
+ }
2249
+ for (const root of roots) {
2250
+ if (root.kind === "Schema") topLevelNodes.add(root);
2251
+ for (const schemaNode of collectLazy(root, { schema: (node) => node })) record(schemaNode);
2252
+ }
2253
+ const canonicalBySignature = /* @__PURE__ */ new Map();
2254
+ const pendingHoists = [];
2255
+ for (const [signature, group] of groups) {
2256
+ if (group.count < minOccurrences) continue;
2257
+ if (group.topLevelName) {
2258
+ canonicalBySignature.set(signature, {
2259
+ name: group.topLevelName,
2260
+ ref: refFor(group.topLevelName)
2261
+ });
2262
+ continue;
2263
+ }
2264
+ const name = nameFor(group.representative, signature);
2265
+ if (!name) continue;
2266
+ canonicalBySignature.set(signature, {
2267
+ name,
2268
+ ref: refFor(name)
2269
+ });
2270
+ pendingHoists.push({
2271
+ name,
2272
+ representative: group.representative
2273
+ });
2274
+ }
2275
+ return {
2276
+ canonicalBySignature,
2277
+ hoisted: pendingHoists.map(({ name, representative }) => cleanDefinition(applyDedupe(representative, canonicalBySignature, true), name))
2278
+ };
2279
+ }
2280
+ //#endregion
2281
+ //#region src/dialect.ts
2282
+ /**
2283
+ * Identity helper that types a {@link SchemaDialect} for an adapter. Like
2284
+ * `defineParser`, it adds no runtime behavior — it pins the dialect's type for
2285
+ * inference and gives adapter authors a discoverable anchor.
2286
+ *
2287
+ * @example
2288
+ * ```ts
2289
+ * export const oasDialect = defineSchemaDialect({
2290
+ * name: 'oas',
2291
+ * isNullable,
2292
+ * isReference,
2293
+ * isDiscriminator,
2294
+ * isBinary: (schema) => schema.type === 'string' && schema.contentMediaType === 'application/octet-stream',
2295
+ * resolveRef,
2296
+ * })
2297
+ * ```
2298
+ */
2299
+ function defineSchemaDialect(dialect) {
2300
+ return dialect;
2301
+ }
2302
+ //#endregion
2303
+ //#region src/dispatch.ts
2304
+ /**
2305
+ * Walks an ordered list of {@link DispatchRule}s and returns the first node produced.
2306
+ *
2307
+ * This is the shared backbone for spec adapters (OpenAPI today, AsyncAPI and others later).
2308
+ * The contract an adapter follows is intentionally minimal:
2309
+ *
2310
+ * context → [rule.match → rule.convert] → node
2311
+ *
2312
+ * An adapter derives a context from a source spec node, then declares an ordered table of
2313
+ * rules mapping spec shapes onto Kubb AST nodes. To add support for a new spec, write a new
2314
+ * context type and a new rules table — the traversal here is reused unchanged.
2315
+ *
2316
+ * Order is significant: earlier rules win, so list higher-precedence or more specific shapes
2317
+ * first (e.g. composition keywords before plain `type`). A rule whose `match` returns `true`
2318
+ * may still `convert` to `null` to defer to later rules. When no rule produces a node this
2319
+ * returns `null`, leaving the caller to apply its own fallback.
2320
+ *
2321
+ * @example
2322
+ * ```ts
2323
+ * const node = dispatch(schemaRules, schemaContext) ?? createSchema({ type: fallbackType })
2324
+ * ```
2325
+ */
2326
+ function dispatch(rules, context) {
2327
+ for (const rule of rules) {
2328
+ if (!rule.match(context)) continue;
2329
+ const node = rule.convert(context);
2330
+ if (node !== null && node !== void 0) return node;
2331
+ }
2332
+ return null;
2333
+ }
2334
+ //#endregion
2118
2335
  //#region src/printer.ts
2119
2336
  /**
2120
2337
  * Defines a schema printer: a function that takes a `SchemaNode` and emits
@@ -2331,6 +2548,6 @@ function setEnumName(propNode, parentName, propName, enumSuffix) {
2331
2548
  return propNode;
2332
2549
  }
2333
2550
  //#endregion
2334
- export { caseParams, childName, collect, collectImports, collectLazy, collectReferencedSchemaNames, collectUsedSchemaNames, containsCircularRef, createArrowFunction, createBreak, createConst, createContent, createDiscriminantNode, createExport, createFile, createFunction, createFunctionParameter, createFunctionParameters, createImport, createInput, createJsx, createOperation, createOperationParams, createOutput, createParameter, createParameterGroup, createParamsType, createPrinterFactory, createProperty, createRequestBody, createResponse, createSchema, createSource, createStreamInput, createText, createType, definePrinter, defineSchemaDialect, dispatch, enumPropName, extractRefName, extractStringsFromNodes, findCircularSchemas, findDiscriminator, httpMethods, isHttpOperationNode, isInputNode, isOperationNode, isOutputNode, isScalarPrimitive, isSchemaNode, isStringType, mediaTypes, mergeAdjacentObjects, mergeAdjacentObjectsLazy, narrowSchema, nodeKinds, resolveRefName, schemaTypes, setDiscriminatorEnum, setEnumName, simplifyUnion, syncOptionality, syncSchemaRef, transform, update, walk };
2551
+ export { applyDedupe, buildDedupePlan, caseParams, childName, collect, collectImports, collectLazy, collectReferencedSchemaNames, collectUsedSchemaNames, containsCircularRef, createArrowFunction, createBreak, createConst, createContent, createDiscriminantNode, createExport, createFile, createFunction, createFunctionParameter, createFunctionParameters, createImport, createInput, createJsx, createOperation, createOperationParams, createOutput, createParameter, createParameterGroup, createParamsType, createPrinterFactory, createProperty, createRequestBody, createResponse, createSchema, createSource, createStreamInput, createText, createType, definePrinter, defineSchemaDialect, dispatch, enumPropName, extractRefName, extractStringsFromNodes, findCircularSchemas, findDiscriminator, httpMethods, isHttpOperationNode, isInputNode, isOperationNode, isOutputNode, isScalarPrimitive, isSchemaEqual, isSchemaNode, isStringType, mediaTypes, mergeAdjacentObjects, mergeAdjacentObjectsLazy, narrowSchema, nodeKinds, resolveRefName, schemaSignature, schemaTypes, setDiscriminatorEnum, setEnumName, simplifyUnion, syncOptionality, syncSchemaRef, transform, update, walk };
2335
2552
 
2336
2553
  //# sourceMappingURL=index.js.map