use @scalar/openapi-parser replace @apidevtools/swagger-parser

This commit is contained in:
Curry Yang committed 2026-11-02 16:27:58 +08:00
1 parent f6f3849594
commit 79bfc947ef
5 files changed
+246 -61

No files matched your search

+157 -46
View File
@@ -74,22 +74,6 @@
"ajv": ">=8"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz",
"integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==",
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.15",
"js-yaml": "^4.1.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/philsturgeon"
}
},
"node_modules/@apidevtools/openapi-schemas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
@@ -105,23 +89,6 @@
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
"license": "MIT"
},
"node_modules/@apidevtools/swagger-parser": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz",
"integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==",
"license": "MIT",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "14.0.1",
"@apidevtools/openapi-schemas": "^2.1.0",
"@apidevtools/swagger-methods": "^3.0.2",
"ajv": "^8.17.1",
"ajv-draft-04": "^1.0.0",
"call-me-maybe": "^1.0.2"
},
"peerDependencies": {
"openapi-types": ">=7"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.6.tgz",
@@ -6961,6 +6928,141 @@
"win32"
]
},
"node_modules/@scalar/helpers": {
"version": "0.16.4",
"resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.16.4.tgz",
"integrity": "sha512-9eILWzUcAkHEuYMMvijpSIy69t+Vkq+NudLB/7lgARFTWqXfQpIxW1L3VH7ogKyRie5+E4CNZ3PSvlw6IIfLGw==",
"license": "MIT",
"engines": {
"node": ">=22"
}
},
"node_modules/@scalar/json-magic": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@scalar/json-magic/-/json-magic-0.17.0.tgz",
"integrity": "sha512-YmJAO9NoGCcU/vx7P4Dy9DGv+Vntp+ny3Fh99slHOOh/IZxvbdK5hWeZdzQ3x+ezab9XJNscFoX8H/P6W+H8lA==",
"license": "MIT",
"dependencies": {
"@scalar/helpers": "0.16.4",
"pathe": "^2.0.3",
"undici": "8.12.3",
"yaml": "^2.9.1"
},
"engines": {
"node": ">=22.19.0"
}
},
"node_modules/@scalar/json-magic/node_modules/undici": {
"version": "8.12.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-8.12.3.tgz",
"integrity": "sha512-+fdndUTwBP86cVjihTp21hZHo7d6oj1c/47XPA3lL8mM0XHs+C4EkcjmgRsFnFk2f/UONGDy9ItBItmXsLOQHw==",
"license": "MIT",
"engines": {
"node": ">=22.19.0"
}
},
"node_modules/@scalar/json-schema-validator": {
"version": "0.1.17",
"resolved": "https://registry.npmjs.org/@scalar/json-schema-validator/-/json-schema-validator-0.1.17.tgz",
"integrity": "sha512-4Kmbvpng0V0kgPyqDWUZ1qstKjtDfz7seUCK1/inHjeQ+EJIcXPhoW47sOXf9yQBPbSjp42Wt8a7MZvizyoCPg==",
"license": "MIT",
"dependencies": {
"@scalar/helpers": "0.16.4",
"@scalar/types": "0.26.0",
"ajv": "^8.20.0",
"ajv-draft-04": "^1.0.0",
"ajv-formats": "^3.0.1",
"yaml": "^2.9.1"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@scalar/openapi-parser": {
"version": "0.29.19",
"resolved": "https://registry.npmjs.org/@scalar/openapi-parser/-/openapi-parser-0.29.19.tgz",
"integrity": "sha512-ajtjKM9sQQOxEKP/VCtLbM9kQCAa0M4XWKsfadVnPvga1p7t1wPtIW+uDph8sHe+nKyydqwYxlZ8Q6RluxhbXw==",
"license": "MIT",
"dependencies": {
"@scalar/helpers": "0.16.4",
"@scalar/json-magic": "0.17.0",
"@scalar/openapi-types": "0.9.8",
"@scalar/openapi-upgrader": "0.4.6",
"@scalar/openapi-validator": "0.1.17",
"@scalar/types": "0.26.0",
"yaml": "^2.9.1"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@scalar/openapi-types": {
"version": "0.9.8",
"resolved": "https://registry.npmjs.org/@scalar/openapi-types/-/openapi-types-0.9.8.tgz",
"integrity": "sha512-pGcpkZObsGBC7nRKYte+6gstoe0C4hp7B2UWQYjg5To1wE68s3OHVvin+muL3tn3ZAoahXeJAkn1+aEcMi802w==",
"license": "MIT",
"engines": {
"node": ">=22"
}
},
"node_modules/@scalar/openapi-upgrader": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/@scalar/openapi-upgrader/-/openapi-upgrader-0.4.6.tgz",
"integrity": "sha512-wkTbowGZAlDA5AhCAnBVft6y3veoE17P+okH/zrwM/BZWBSzPghGpa4BPTCg3+IzRmRP/8ZeaYO7LfxCDAEmcA==",
"license": "MIT",
"dependencies": {
"@scalar/helpers": "0.16.4",
"@scalar/openapi-types": "0.9.8"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@scalar/openapi-validator": {
"version": "0.1.17",
"resolved": "https://registry.npmjs.org/@scalar/openapi-validator/-/openapi-validator-0.1.17.tgz",
"integrity": "sha512-vBZZgD5k5mp5ii3kNdAti3bZZO0kEvUWYbr9E+Dkjdb67LAdgD5y4Ym0pNIik7prN6KUuUVUu1UMFGFvu1Udqw==",
"license": "MIT",
"dependencies": {
"@scalar/helpers": "0.16.4",
"@scalar/json-schema-validator": "0.1.17",
"@scalar/types": "0.26.0",
"yaml": "^2.9.1"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@scalar/types": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.26.0.tgz",
"integrity": "sha512-T78Dj1Q+W0E1RsMyPoydfR+L+p9IUIkntBCRYlWaRibM98jAxABRYGQoU/qDHX0PR1IAFdE+d7ZqslypZbJzcg==",
"license": "MIT",
"dependencies": {
"@scalar/helpers": "0.16.4",
"nanoid": "^5.1.16",
"type-fest": "^5.10.0",
"zod": "^4.6.7"
},
"engines": {
"node": ">=22"
}
},
"node_modules/@scalar/types/node_modules/type-fest": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.10.0.tgz",
"integrity": "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA==",
"license": "(MIT OR CC0-1.0)",
"dependencies": {
"tagged-tag": "^1.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@scarf/scarf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
@@ -9766,7 +9868,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
@@ -19247,10 +19348,9 @@
}
},
"node_modules/nanoid": {
"version": "5.1.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz",
"integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==",
"devOptional": true,
"version": "5.1.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz",
"integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==",
"funding": [
{
"type": "github",
@@ -24083,6 +24183,18 @@
"node": ">= 6"
}
},
"node_modules/tagged-tag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tailwind-merge": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
@@ -26364,9 +26476,9 @@
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
"integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
@@ -26505,10 +26617,9 @@
}
},
"node_modules/zod": {
"version": "4.6.5",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz",
"integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==",
"devOptional": true,
"version": "4.6.7",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz",
"integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
@@ -26542,7 +26653,6 @@
"license": "Apache-2.0",
"dependencies": {
"@apideck/better-ajv-errors": "^0.3.6",
"@apidevtools/swagger-parser": "12.1.0",
"@bufbuild/protobuf": "^2.16.0",
"@connectrpc/connect": "^2.2.0",
"@connectrpc/connect-node": "^2.2.0",
@@ -26562,6 +26672,7 @@
"@rjsf/core": "6.8.0",
"@rjsf/utils": "6.8.0",
"@rjsf/validator-ajv8": "6.8.0",
"@scalar/openapi-parser": "0.29.19",
"@sentry/electron": "^7.17.0",
"@stoplight/spectral-core": "^1.22.0",
"@stoplight/spectral-formats": "^1.8.2",
+1 -1
View File
@@ -50,7 +50,6 @@
},
"dependencies": {
"@apideck/better-ajv-errors": "^0.3.6",
"@apidevtools/swagger-parser": "12.1.0",
"@bufbuild/protobuf": "^2.16.0",
"@connectrpc/connect": "^2.2.0",
"@connectrpc/connect-node": "^2.2.0",
@@ -70,6 +69,7 @@
"@rjsf/core": "6.8.0",
"@rjsf/utils": "6.8.0",
"@rjsf/validator-ajv8": "6.8.0",
"@scalar/openapi-parser": "0.29.19",
"@sentry/electron": "^7.17.0",
"@stoplight/spectral-core": "^1.22.0",
"@stoplight/spectral-formats": "^1.8.2",
@@ -495,4 +495,57 @@ describe('openapi-3', () => {
expect(anyPetContentTypeHeader?.value).toBe('application/json');
});
});
describe('circular $ref', () => {
it('should import a schema that references itself without recursing forever', async () => {
const openApiDoc: OpenAPIV3.Document = {
openapi: '3.0.3',
info: { title: 'Tree API', version: '1.0.0' },
servers: [{ url: 'https://api.example.com' }],
paths: {
'/nodes': {
post: {
requestBody: {
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Node' },
},
},
},
responses: { 200: { description: 'ok' } },
},
},
},
components: {
schemas: {
Node: {
type: 'object',
properties: {
name: { type: 'string' },
parent: { $ref: '#/components/schemas/Node' },
children: { type: 'array', items: { $ref: '#/components/schemas/Node' } },
},
},
},
},
};
const result = await convert(JSON.stringify(openApiDoc));
expect(result).not.toBeNull();
const request = result?.find(item => item._type === 'request');
const body = JSON.parse(request.body.text);
expect(body.name).toBe('string');
expect(Array.isArray(body.children)).toBe(true);
// the cycle is cut off with null instead of expanding forever
let node = body;
let depth = 0;
while (node && depth < 10) {
node = node.parent;
depth++;
}
expect(node).toBeNull();
expect(depth).toBeLessThan(10);
});
});
});
@@ -1,7 +1,7 @@
import crypto from 'node:crypto';
import { parse as urlParse } from 'node:url';
import SwaggerParser from '@apidevtools/swagger-parser';
import { dereference, validate } from '@scalar/openapi-parser';
import type { OpenAPIV2, OpenAPIV3 } from 'openapi-types';
import YAML from 'yaml';
@@ -554,7 +554,7 @@ const convertParameters = (parameters: OpenAPIV3.ParameterObject[] = []) => {
* Returns example / default value of the parameter, if any of those are defined. If not, returns value based on parameter type.
*/
// @ts-expect-error -- ran out of time during TypeScript conversion to handle this particular recursion
const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string) => {
const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string, ancestors: unknown[] = []) => {
const typeExamples = {
'string': () => 'string',
'string_email': () => 'user@example.com',
@@ -571,7 +571,10 @@ const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string) => {
if (properties) {
for (const propertyName of Object.keys(properties)) {
example[propertyName] = generateParameterExample(properties[propertyName] as OpenAPIV3.SchemaObject);
example[propertyName] = generateParameterExample(properties[propertyName] as OpenAPIV3.SchemaObject, [
...ancestors,
schema,
]);
}
}
@@ -580,7 +583,7 @@ const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string) => {
// @ts-expect-error -- ran out of time during TypeScript conversion to handle this particular recursion
'array': (schema: OpenAPIV2.ItemsObject) => {
// @ts-expect-error -- ran out of time during TypeScript conversion to handle this particular recursion
const value = generateParameterExample(schema.items);
const value = generateParameterExample(schema.items, [...ancestors, schema]);
if (schema.collectionFormat === 'csv') {
return value;
@@ -595,6 +598,11 @@ const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string) => {
}
if (schema instanceof Object) {
// dereferenced circular $refs resolve to the same object, so stop when a schema contains itself
if (ancestors.includes(schema)) {
return null;
}
const {
type,
format,
@@ -626,7 +634,7 @@ const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string) => {
if (allOf && Array.isArray(allOf)) {
const mergedExample: Record<string, unknown> = {};
for (const subSchema of allOf) {
const subExample = generateParameterExample(subSchema as OpenAPIV3.SchemaObject);
const subExample = generateParameterExample(subSchema as OpenAPIV3.SchemaObject, [...ancestors, schema]);
if (subExample && typeof subExample === 'object' && !Array.isArray(subExample)) {
Object.assign(mergedExample, subExample);
}
@@ -636,12 +644,12 @@ const generateParameterExample = (schema: OpenAPIV3.SchemaObject | string) => {
// Handle oneOf by using the first schema (validates against exactly one)
if (oneOf && Array.isArray(oneOf) && oneOf.length > 0) {
return generateParameterExample(oneOf[0] as OpenAPIV3.SchemaObject);
return generateParameterExample(oneOf[0] as OpenAPIV3.SchemaObject, [...ancestors, schema]);
}
// Handle anyOf by using the first schema (validates against any/one or more)
if (anyOf && Array.isArray(anyOf) && anyOf.length > 0) {
return generateParameterExample(anyOf[0] as OpenAPIV3.SchemaObject);
return generateParameterExample(anyOf[0] as OpenAPIV3.SchemaObject, [...ancestors, schema]);
}
// @ts-expect-error -- ran out of time during TypeScript conversion to handle this particular recursion
@@ -810,11 +818,16 @@ export const convert: Converter = async rawData => {
}
try {
apiDocument = (await SwaggerParser.validate(apiDocument, {
dereference: {
circular: 'ignore',
},
})) as OpenAPIV3.Document;
const input = apiDocument as unknown as Record<string, unknown>;
const { valid, errors } = await validate(input);
if (!valid) {
throw errors;
}
const { schema, errors: dereferenceErrors } = await dereference(input);
if (dereferenceErrors?.length) {
throw dereferenceErrors;
}
apiDocument = schema as unknown as OpenAPIV3.Document;
} catch (err) {
console.log('[openapi-3] Import file validation failed', err);
}
@@ -1,6 +1,6 @@
import crypto from 'node:crypto';
import SwaggerParser from '@apidevtools/swagger-parser';
import { dereference, validate } from '@scalar/openapi-parser';
import type { OpenAPIV2 } from 'openapi-types';
import YAML from 'yaml';
@@ -502,7 +502,15 @@ export const convert: Converter = async rawData => {
} // Await here so we catch any exceptions
try {
api = await SwaggerParser.validate(api);
const { valid, errors } = await validate(api);
if (!valid) {
throw errors;
}
const { schema, errors: dereferenceErrors } = await dereference(api);
if (dereferenceErrors?.length) {
throw dereferenceErrors;
}
api = schema as unknown as OpenAPIV2.Document;
} catch (err) {
// We already know it's a Swagger doc so we will try to import it anyway instead
// of bailing out here.