@kubb/adapter-oas 5.0.0-beta.11 → 5.0.0-beta.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/adapter-oas",
3
- "version": "5.0.0-beta.11",
3
+ "version": "5.0.0-beta.13",
4
4
  "description": "OpenAPI and Swagger adapter for Kubb. Parses and validates OAS 2.0/3.x specifications into a @kubb/ast RootNode for downstream code generation plugins.",
5
5
  "keywords": [
6
6
  "adapter",
@@ -47,7 +47,7 @@
47
47
  "oas": "^32.1.18",
48
48
  "oas-normalize": "^16.0.4",
49
49
  "swagger2openapi": "^7.0.8",
50
- "@kubb/core": "5.0.0-beta.11"
50
+ "@kubb/core": "5.0.0-beta.13"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/swagger2openapi": "^7.0.4",
@@ -68,6 +68,7 @@
68
68
  "release": "pnpm publish --no-git-check",
69
69
  "release:canary": "bash ../../.github/canary.sh && node ../../scripts/build.js canary && pnpm publish --no-git-check",
70
70
  "start": "tsdown --watch",
71
+ "bench": "vitest bench",
71
72
  "test": "vitest --passWithNoTests",
72
73
  "typecheck": "tsc -p ./tsconfig.json --noEmit --emitDeclarationOnly false"
73
74
  }
@@ -0,0 +1,35 @@
1
+ import path from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { bench, describe } from 'vitest'
4
+ import { parseDocument } from './factory.ts'
5
+ import { parseOas } from './parser.ts'
6
+ import type { Document } from './types.ts'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = path.dirname(__filename)
10
+
11
+ // TODO: replace with stripe.json once the parser handles its circular-reference
12
+ // expansion without exhausting heap (the full Stripe spec triggers exponential
13
+ // sub-tree duplication because resolvingRefs only prevents recursion within a
14
+ // single resolution chain, not repeated resolution of the same schema).
15
+ const petStorePath = path.resolve(__dirname, '../mocks/petStore.yaml')
16
+
17
+ let document: Document | undefined
18
+
19
+ async function getDocument(): Promise<Document> {
20
+ if (!document) {
21
+ document = await parseDocument(petStorePath)
22
+ }
23
+ return document
24
+ }
25
+
26
+ describe('parseOas() performance', () => {
27
+ bench(
28
+ 'petStore spec',
29
+ async () => {
30
+ const doc = await getDocument()
31
+ parseOas(doc)
32
+ },
33
+ { iterations: 5, warmupIterations: 1 },
34
+ )
35
+ })
package/src/adapter.ts CHANGED
@@ -44,10 +44,8 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
44
44
  emptySchemaType = unknownType || DEFAULT_PARSER_OPTIONS.emptySchemaType,
45
45
  } = options
46
46
 
47
- // Let-binding so parse() can replace it with a simple reassignment (no clear+loop).
48
47
  let nameMapping = new Map<string, string>()
49
48
  let parsedDocument: Document | null
50
- let inputNode: ast.InputNode | null
51
49
 
52
50
  return {
53
51
  name: adapterOasName,
@@ -69,9 +67,6 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
69
67
  get document() {
70
68
  return parsedDocument
71
69
  },
72
- get inputNode() {
73
- return inputNode
74
- },
75
70
  async validate(input, options) {
76
71
  const document = await parseDocument(input)
77
72
  await validateDocument(document, options)
@@ -114,7 +109,7 @@ export const adapterOas = createAdapter<AdapterOas>((options) => {
114
109
  // Expose the raw document so consumers (e.g. plugin-redoc) can access it.
115
110
  parsedDocument = document
116
111
 
117
- inputNode = ast.createInput({
112
+ const inputNode = ast.createInput({
118
113
  ...node,
119
114
  meta: {
120
115
  title: document.info?.title,
package/src/parser.ts CHANGED
@@ -89,6 +89,20 @@ function createSchemaParser(ctx: OasParserContext) {
89
89
  */
90
90
  const resolvingRefs = new Set<string>()
91
91
 
92
+ /**
93
+ * Cache of already-resolved `$ref` schemas within this parser instance.
94
+ *
95
+ * Without this, the same referenced schema (e.g. `customer`) is fully re-expanded
96
+ * every time it appears as a `$ref` in a different parent schema. In heavily
97
+ * cross-referenced specs like Stripe (~1 400 schemas), this causes exponential
98
+ * blowup — `customer` alone may be referenced from dozens of top-level schemas,
99
+ * each triggering a fresh recursive expansion of its entire sub-tree.
100
+ *
101
+ * Memoising by `$ref` path reduces the overall work from O(2^depth) to O(N)
102
+ * where N is the number of unique schema names.
103
+ */
104
+ const resolvedRefCache = new Map<string, ast.SchemaNode | undefined>()
105
+
92
106
  /**
93
107
  * Converts a `$ref` schema into a `RefSchemaNode`.
94
108
  *
@@ -101,15 +115,20 @@ function createSchemaParser(ctx: OasParserContext) {
101
115
  let resolvedSchema: ast.SchemaNode | undefined
102
116
  const refPath = schema.$ref
103
117
  if (refPath && !resolvingRefs.has(refPath)) {
104
- try {
105
- const referenced = resolveRef<SchemaObject>(document, refPath)
106
- if (referenced) {
107
- resolvingRefs.add(refPath)
108
- resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
109
- resolvingRefs.delete(refPath)
118
+ if (resolvedRefCache.has(refPath)) {
119
+ resolvedSchema = resolvedRefCache.get(refPath)
120
+ } else {
121
+ try {
122
+ const referenced = resolveRef<SchemaObject>(document, refPath)
123
+ if (referenced) {
124
+ resolvingRefs.add(refPath)
125
+ resolvedSchema = parseSchema({ schema: referenced }, rawOptions)
126
+ resolvingRefs.delete(refPath)
127
+ }
128
+ } catch {
129
+ // Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
110
130
  }
111
- } catch {
112
- // Ref cannot be resolved in this document (e.g. unit tests with minimal documents).
131
+ resolvedRefCache.set(refPath, resolvedSchema)
113
132
  }
114
133
  }
115
134