@scalar/openapi-to-markdown 1.1.0 → 1.3.0
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/CHANGELOG.md +34 -0
- package/dist/get-markdown-examples.d.ts +12 -1
- package/dist/get-markdown-examples.d.ts.map +1 -1
- package/dist/get-markdown-examples.js +83 -4
- package/dist/load-document.d.ts.map +1 -1
- package/dist/load-document.js +120 -42
- package/dist/object-kinds.d.ts +14 -0
- package/dist/object-kinds.d.ts.map +1 -0
- package/dist/object-kinds.js +70 -0
- package/dist/render-document.d.ts.map +1 -1
- package/dist/render-document.js +6 -2
- package/dist/render-examples.d.ts +1 -1
- package/dist/render-examples.d.ts.map +1 -1
- package/dist/render-examples.js +32 -11
- package/dist/render-operation-details.d.ts +2 -2
- package/dist/render-operation-details.d.ts.map +1 -1
- package/dist/render-operation-details.js +5 -5
- package/dist/render-operation.d.ts.map +1 -1
- package/dist/render-operation.js +10 -11
- package/dist/render-schema.d.ts +20 -2
- package/dist/render-schema.d.ts.map +1 -1
- package/dist/render-schema.js +175 -67
- package/dist/restore-boolean-schemas.d.ts +8 -1
- package/dist/restore-boolean-schemas.d.ts.map +1 -1
- package/dist/restore-boolean-schemas.js +42 -38
- package/dist/select-document.d.ts +3 -3
- package/dist/select-document.d.ts.map +1 -1
- package/dist/select-document.js +56 -35
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @scalar/openapi-to-markdown
|
|
2
2
|
|
|
3
|
+
## 1.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#10192](https://github.com/scalar/scalar/pull/10192): Mock-server XML response bytes now use the shared schema-aware serializer instead of `json2xml`, including attributes, namespaces, and root naming. Existing XML response snapshots may need updating. Supplied serialized XML remains unchanged.
|
|
8
|
+
|
|
9
|
+
Generate XML examples from schema metadata, preserving attributes, namespaces, array wrappers, repeated elements, and OpenAPI 3.2 text and CDATA nodes. Use the same XML serialization for request bodies, code snippets, response examples, mock responses, and Markdown documentation. Preserve serialized media examples and escape schema string examples as element text.
|
|
10
|
+
|
|
11
|
+
Explain XML generation failures in response example panels, including the serialized-example escape hatch for large payloads. Expose XML generation failures in mock response headers with `X-Scalar-XML-Error`, containing the first error diagnostic code. Report diagnostics to other consumers through a callback or the developer console, and format element-only descendants within mixed content without changing text values.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- [#10356](https://github.com/scalar/scalar/pull/10356): Load densely cross-linked API descriptions with far less memory. References are now linked after coercion, so TypeBox no longer copies the reference graph each time it checks a union or array. Stripe's API description loads in about 2 seconds with a peak of about 190 MB, down from about 17 seconds and 1.4 GB, so it now loads and renders with a 768 MB heap limit.
|
|
16
|
+
|
|
17
|
+
Coercion now checks each reference target in its own position, not through every reference that points to it. Before, a target that failed a strict schema check (for example, a schema with `oneOf` references) could turn the reference into an empty schema and drop sibling `x-` extensions. Those references and extensions are now kept, so affected response schemas render in full.
|
|
18
|
+
|
|
19
|
+
References to targets that coercion drops, such as a root-level `definitions` block, now link to a copy cast as the schema, parameter, response or other object the reference stands in for, and their anchors resolve against the resource that contains them. A `$ref: '#'` inside a schema with an `$id` now links to that schema.
|
|
20
|
+
|
|
21
|
+
Coerce reference targets stored in extension data and schema siblings on fallback references before rendering, avoiding crashes from malformed fields while preserving recursive links.
|
|
22
|
+
|
|
23
|
+
- [#10192](https://github.com/scalar/scalar/pull/10192): Apply edited XML bodies instead of their original serialized or data examples, render schema-free XML examples in Markdown, and share reference decoding and response provenance selection.
|
|
24
|
+
|
|
25
|
+
## 1.2.0
|
|
26
|
+
|
|
27
|
+
### Minor Changes
|
|
28
|
+
|
|
29
|
+
- [#10186](https://github.com/scalar/scalar/pull/10186): Support document-defined additional operations in Markdown operation, webhook, and tag selections. Preserve custom method spelling and inherited parameters, servers, and security when copying an operation as Markdown.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- [#10329](https://github.com/scalar/scalar/pull/10329): Render each shared schema once per page. The first reference to a structured schema expands it and labels it with its name (for example `schema: Account`). Later references print "Schema `Account` is shown above." instead of expanding it again. Model sections for schemas already expanded on the page refer back to them too. True cycles still print `[Circular Reference]`. This keeps Markdown output proportional to the schema graph, rather than to the number of paths through it. Before this change, densely linked descriptions such as Stripe's ran out of memory when rendering a single operation.
|
|
34
|
+
|
|
35
|
+
A node budget per operation and model section adds a visible `[Schema output truncated]` marker as a last resort. Generated examples that would exceed 10,000 values are omitted with a note. References past the depth limit now point to the model's own section instead of being cut off.
|
|
36
|
+
|
|
3
37
|
## 1.1.0
|
|
4
38
|
|
|
5
39
|
### Minor Changes
|
|
@@ -15,8 +15,19 @@ type MarkdownExample = {
|
|
|
15
15
|
externalValue: string;
|
|
16
16
|
} | {
|
|
17
17
|
serializedValue: string;
|
|
18
|
+
} | {
|
|
19
|
+
omitted: true;
|
|
20
|
+
} | {
|
|
21
|
+
dataValue: unknown;
|
|
22
|
+
} | {
|
|
23
|
+
error: string;
|
|
18
24
|
});
|
|
25
|
+
/**
|
|
26
|
+
* Estimate how many values an example generated from this schema contains, stopping just past the limit.
|
|
27
|
+
* Counts are cached per schema and level, so this is linear in the size of the schema graph.
|
|
28
|
+
*/
|
|
29
|
+
export declare const countGeneratedExampleValues: (root: unknown, limit?: number) => number;
|
|
19
30
|
/** Preserve supplied values; generate a fallback only when examples are not supplied. */
|
|
20
|
-
export declare const getMarkdownExamples: (source: ExampleSource, mediaType: string, mode?: "read" | "write", openapiVersion?: string) => MarkdownExample[];
|
|
31
|
+
export declare const getMarkdownExamples: (source: ExampleSource, mediaType: string, mode?: "read" | "write", openapiVersion?: string, schemaOpenapiVersion?: string) => MarkdownExample[];
|
|
21
32
|
export {};
|
|
22
33
|
//# sourceMappingURL=get-markdown-examples.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-markdown-examples.d.ts","sourceRoot":"","sources":["../src/get-markdown-examples.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"get-markdown-examples.d.ts","sourceRoot":"","sources":["../src/get-markdown-examples.ts"],"names":[],"mappings":"AAMA,4FAA4F;AAC5F,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC,CAAA;AAED,gFAAgF;AAChF,KAAK,eAAe,GAAG;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,GAAG,CACA;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAClB;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,eAAe,EAAE,MAAM,CAAA;CAAE,GAC3B;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GACjB;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,GACtB;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CACpB,CAAA;AAYD;;;GAGG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,OAAO,EAAE,cAAoC,KAAG,MA2CjG,CAAA;AAED,yFAAyF;AACzF,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,aAAa,EACrB,WAAW,MAAM,EACjB,OAAO,MAAM,GAAG,OAAO,EACvB,uBAAwB,EAExB,6BAAqC,KACpC,eAAe,EAoCjB,CAAA"}
|
|
@@ -1,8 +1,77 @@
|
|
|
1
|
+
import { isXmlMediaType } from '@scalar/helpers/http/is-xml-media-type';
|
|
1
2
|
import { isObject } from '@scalar/helpers/object/is-object';
|
|
2
3
|
import { getResolvedRef, mergeSiblingReferences } from '@scalar/workspace-store/helpers/get-resolved-ref';
|
|
3
|
-
import { getExampleFromSchema } from '@scalar/workspace-store/request-example';
|
|
4
|
+
import { getExampleFromSchema, getXmlBodyExample } from '@scalar/workspace-store/request-example';
|
|
5
|
+
/** Mirrors the depth at which `getExampleFromSchema` stops following nested schemas. */
|
|
6
|
+
const EXAMPLE_DEPTH = 10;
|
|
7
|
+
/**
|
|
8
|
+
* Generated examples repeat every shared schema at each place it is used, up to ten levels deep.
|
|
9
|
+
* A densely shared schema graph therefore produces billions of values, so larger examples are skipped.
|
|
10
|
+
* The largest generated examples in the Stripe, GitHub, and Cloudflare descriptions have about 7,600 values.
|
|
11
|
+
*/
|
|
12
|
+
const MAX_GENERATED_EXAMPLE_VALUES = 10_000;
|
|
13
|
+
/**
|
|
14
|
+
* Estimate how many values an example generated from this schema contains, stopping just past the limit.
|
|
15
|
+
* Counts are cached per schema and level, so this is linear in the size of the schema graph.
|
|
16
|
+
*/
|
|
17
|
+
export const countGeneratedExampleValues = (root, limit = MAX_GENERATED_EXAMPLE_VALUES) => {
|
|
18
|
+
const counts = new WeakMap();
|
|
19
|
+
const count = (input, level) => {
|
|
20
|
+
if (level > EXAMPLE_DEPTH || !isObject(input))
|
|
21
|
+
return 1;
|
|
22
|
+
const cached = counts.get(input)?.get(level);
|
|
23
|
+
if (cached !== undefined)
|
|
24
|
+
return cached;
|
|
25
|
+
const schema = getResolvedRef(input, mergeSiblingReferences);
|
|
26
|
+
if (!isObject(schema))
|
|
27
|
+
return 1;
|
|
28
|
+
// Supplied examples bypass schema expansion in the generator too.
|
|
29
|
+
if (schema.example !== undefined || (Array.isArray(schema.examples) && schema.examples.length > 0))
|
|
30
|
+
return 1;
|
|
31
|
+
let total = 1;
|
|
32
|
+
const add = (child) => {
|
|
33
|
+
if (total <= limit)
|
|
34
|
+
total += count(child, level + 1);
|
|
35
|
+
};
|
|
36
|
+
if (isObject(schema.properties))
|
|
37
|
+
Object.values(schema.properties).forEach(add);
|
|
38
|
+
if (isObject(schema.patternProperties))
|
|
39
|
+
Object.values(schema.patternProperties).forEach(add);
|
|
40
|
+
if (isObject(schema.additionalProperties))
|
|
41
|
+
add(schema.additionalProperties);
|
|
42
|
+
if (schema.items !== undefined)
|
|
43
|
+
add(schema.items);
|
|
44
|
+
if (Array.isArray(schema.prefixItems))
|
|
45
|
+
schema.prefixItems.forEach(add);
|
|
46
|
+
if (Array.isArray(schema.allOf))
|
|
47
|
+
schema.allOf.forEach(add);
|
|
48
|
+
const variants = Array.isArray(schema.oneOf) ? schema.oneOf : Array.isArray(schema.anyOf) ? schema.anyOf : [];
|
|
49
|
+
// Object and array generation can use the first variant, while other unions skip null.
|
|
50
|
+
// A discriminator can choose any variant, so bound the largest candidate in that case.
|
|
51
|
+
const nonNull = variants.find((variant) => {
|
|
52
|
+
const resolved = getResolvedRef(variant, mergeSiblingReferences);
|
|
53
|
+
return isObject(resolved) && resolved.type !== 'null';
|
|
54
|
+
});
|
|
55
|
+
const candidates = isObject(schema.discriminator) && schema.discriminator.defaultMapping !== undefined
|
|
56
|
+
? variants
|
|
57
|
+
: [variants[0], nonNull];
|
|
58
|
+
let variantCount = 0;
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
if (candidate !== undefined && total <= limit && variantCount <= limit)
|
|
61
|
+
variantCount = Math.max(variantCount, count(candidate, level + 1));
|
|
62
|
+
}
|
|
63
|
+
total += variantCount;
|
|
64
|
+
const levels = counts.get(input) ?? new Map();
|
|
65
|
+
levels.set(level, total);
|
|
66
|
+
counts.set(input, levels);
|
|
67
|
+
return total;
|
|
68
|
+
};
|
|
69
|
+
return count(root, 0);
|
|
70
|
+
};
|
|
4
71
|
/** Preserve supplied values; generate a fallback only when examples are not supplied. */
|
|
5
|
-
export const getMarkdownExamples = (source, mediaType, mode, openapiVersion = '3.2.0'
|
|
72
|
+
export const getMarkdownExamples = (source, mediaType, mode, openapiVersion = '3.2.0',
|
|
73
|
+
// Schema metadata can be upgraded while example fields still follow the original version.
|
|
74
|
+
schemaOpenapiVersion = openapiVersion) => {
|
|
6
75
|
if (source.example !== undefined)
|
|
7
76
|
return [{ value: source.example }];
|
|
8
77
|
if (source.examples && Object.keys(source.examples).length) {
|
|
@@ -22,15 +91,25 @@ export const getMarkdownExamples = (source, mediaType, mode, openapiVersion = '3
|
|
|
22
91
|
if (typeof example.externalValue === 'string')
|
|
23
92
|
return [{ ...metadata, externalValue: example.externalValue }];
|
|
24
93
|
if (/^3\.2\./.test(openapiVersion) && example.dataValue !== undefined)
|
|
25
|
-
return [{ ...metadata,
|
|
94
|
+
return [{ ...metadata, dataValue: example.dataValue }];
|
|
26
95
|
return [];
|
|
27
96
|
});
|
|
28
97
|
}
|
|
29
98
|
const schema = getResolvedRef(source.schema);
|
|
30
99
|
if (!isObject(schema))
|
|
31
100
|
return [];
|
|
101
|
+
if (countGeneratedExampleValues(source.schema) > MAX_GENERATED_EXAMPLE_VALUES)
|
|
102
|
+
return [{ omitted: true }];
|
|
103
|
+
if (isXmlMediaType(mediaType)) {
|
|
104
|
+
const result = getXmlBodyExample(source.schema, undefined, {
|
|
105
|
+
mode,
|
|
106
|
+
openapiVersion: schemaOpenapiVersion,
|
|
107
|
+
});
|
|
108
|
+
return [
|
|
109
|
+
result.xml === undefined ? { error: 'Unable to generate an XML example.' } : { serializedValue: result.xml },
|
|
110
|
+
];
|
|
111
|
+
}
|
|
32
112
|
const value = getExampleFromSchema(getResolvedRef(source.schema, mergeSiblingReferences), {
|
|
33
|
-
xml: mediaType.includes('xml'),
|
|
34
113
|
mode,
|
|
35
114
|
});
|
|
36
115
|
return value === undefined ? [] : [{ value }];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-document.d.ts","sourceRoot":"","sources":["../src/load-document.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,eAAe,
|
|
1
|
+
{"version":3,"file":"load-document.d.ts","sourceRoot":"","sources":["../src/load-document.ts"],"names":[],"mappings":"AAWA,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,8DAA8D,CAAA;AA6LrE,8EAA8E;AAC9E,eAAO,MAAM,YAAY,GACvB,OAAO,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,KACxD,OAAO,CAAC,eAAe,CAgGzB,CAAA"}
|
package/dist/load-document.js
CHANGED
|
@@ -9,21 +9,99 @@ import { getRaw } from '@scalar/json-magic/magic-proxy';
|
|
|
9
9
|
import { upgrade } from '@scalar/openapi-upgrader';
|
|
10
10
|
import { deepClone } from '@scalar/workspace-store/helpers/deep-clone';
|
|
11
11
|
import { coerceValue } from '@scalar/workspace-store/schemas/typebox-coerce';
|
|
12
|
-
import { OpenAPIDocumentSchema, } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
|
|
13
|
-
import {
|
|
12
|
+
import { OpenAPIDocumentSchema, SchemaObjectSchema, } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
|
|
13
|
+
import { getChildKind, referenceTargetSchemas } from './object-kinds.js';
|
|
14
|
+
import { getChildPosition, restoreBooleanSchemas } from './restore-boolean-schemas.js';
|
|
14
15
|
/**
|
|
15
16
|
* Link references in a private, bundled document without proxies or expanded copies.
|
|
16
17
|
* JSON Magic owns `$id` and anchor indexing, which keeps local-reference behavior
|
|
17
18
|
* aligned with the external-reference bundler without retaining a second resolver.
|
|
18
19
|
* Non-enumerable links preserve serialization and share recursive schema targets.
|
|
19
|
-
*
|
|
20
|
+
* With `detectOnly`, nothing is linked and the pass only looks for external references.
|
|
20
21
|
* Returns whether external references remain and require bundling.
|
|
21
22
|
*/
|
|
22
|
-
const attachRefValues = (document,
|
|
23
|
-
//
|
|
23
|
+
const attachRefValues = (document, schemas = getSchemas(document), { detectOnly = false, fallback } = {}) => {
|
|
24
|
+
// Fallback targets are linked outside the walked tree and can be reached again from it,
|
|
25
|
+
// so one set keeps every object to a single visit.
|
|
24
26
|
const seen = new WeakSet();
|
|
27
|
+
// Large descriptions repeat the same reference strings thousands of times.
|
|
28
|
+
const targets = new Map();
|
|
29
|
+
// Share one cast copy between every reference of the same kind to the same dropped target.
|
|
30
|
+
const castFallbacks = new WeakMap();
|
|
25
31
|
let hasExternalReferences = false;
|
|
26
|
-
const
|
|
32
|
+
const lookUp = (path, ref) => {
|
|
33
|
+
// An empty path is a resource root, unless the fragment is the pointer to the empty key.
|
|
34
|
+
const segments = path === '' && !ref.includes('#/') ? [] : parseJsonPointerSegments(`/${path}`);
|
|
35
|
+
const linked = getValueByPath(document, segments);
|
|
36
|
+
let position = 'document';
|
|
37
|
+
let kind = 'document';
|
|
38
|
+
let parent = document;
|
|
39
|
+
for (const key of segments) {
|
|
40
|
+
kind = kind && getChildKind(kind, key);
|
|
41
|
+
position = Array.isArray(parent) ? position : position && getChildPosition(position, key);
|
|
42
|
+
// Arbitrary extension and example data has not passed an OpenAPI object schema.
|
|
43
|
+
if (position === 'document' && kind === undefined)
|
|
44
|
+
position = undefined;
|
|
45
|
+
parent = getValueByPath(parent, [key]).value;
|
|
46
|
+
}
|
|
47
|
+
const schema = getTargetSchema(position, kind);
|
|
48
|
+
if ((linked.value !== undefined && schema !== undefined) || fallback === undefined) {
|
|
49
|
+
return { ...linked, linked: true, schema };
|
|
50
|
+
}
|
|
51
|
+
// The context is the base of the resource that contains the fallback target.
|
|
52
|
+
return { ...getValueByPath(fallback.source, segments), linked: false };
|
|
53
|
+
};
|
|
54
|
+
const resolve = (ref, base) => {
|
|
55
|
+
const key = `${base}\u0000${ref}`;
|
|
56
|
+
if (targets.has(key)) {
|
|
57
|
+
return targets.get(key);
|
|
58
|
+
}
|
|
59
|
+
// JSON Magic has no entry for an empty fragment, which names the root of the current resource.
|
|
60
|
+
const path = ref === '#' ? (schemas.get(base) ?? '') : convertToLocalRef(ref, base, schemas);
|
|
61
|
+
const resolved = path === undefined ? undefined : lookUp(path, ref);
|
|
62
|
+
targets.set(key, resolved);
|
|
63
|
+
return resolved;
|
|
64
|
+
};
|
|
65
|
+
// Cast dropped targets like the objects they stand in for, so the renderer never sees uncoerced shapes.
|
|
66
|
+
const castFallback = (value, schema, position) => {
|
|
67
|
+
// OpenAPI 3.1 keeps boolean schemas, and a nested reference is linked once it is walked.
|
|
68
|
+
if (value === undefined || (fallback?.booleanSchemas && position === 'schema' && typeof value === 'boolean')) {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
if (!isObject(value)) {
|
|
72
|
+
return coerceValue(schema, value);
|
|
73
|
+
}
|
|
74
|
+
const casts = castFallbacks.get(value) ?? new Map();
|
|
75
|
+
castFallbacks.set(value, casts);
|
|
76
|
+
if (!casts.has(schema)) {
|
|
77
|
+
// Cast schema siblings without losing the reference. Other Reference Objects only
|
|
78
|
+
// support summary and description; casting them as their targets would add defaults.
|
|
79
|
+
// Path Item Objects declare their own $ref field, so their siblings are cast normally.
|
|
80
|
+
const reference = typeof value.$ref === 'string' ? value.$ref : undefined;
|
|
81
|
+
const cast = reference === undefined || schema === referenceTargetSchemas.pathItem
|
|
82
|
+
? coerceValue(schema, value)
|
|
83
|
+
: position === 'schema'
|
|
84
|
+
? { ...coerceValue(SchemaObjectSchema, value), $ref: reference }
|
|
85
|
+
: {
|
|
86
|
+
$ref: reference,
|
|
87
|
+
...(typeof value.summary === 'string' ? { summary: value.summary } : {}),
|
|
88
|
+
...(typeof value.description === 'string' ? { description: value.description } : {}),
|
|
89
|
+
};
|
|
90
|
+
if (fallback?.booleanSchemas) {
|
|
91
|
+
restoreBooleanSchemas(value, cast, position);
|
|
92
|
+
}
|
|
93
|
+
casts.set(schema, cast);
|
|
94
|
+
}
|
|
95
|
+
return casts.get(schema);
|
|
96
|
+
};
|
|
97
|
+
// Schemas are recognized by their position, and every other object by the field that holds it.
|
|
98
|
+
const getTargetSchema = (position, kind) => {
|
|
99
|
+
if (position === 'schema') {
|
|
100
|
+
return SchemaObjectSchema;
|
|
101
|
+
}
|
|
102
|
+
return typeof kind === 'string' ? referenceTargetSchemas[kind] : undefined;
|
|
103
|
+
};
|
|
104
|
+
const visit = (node, context, position, kind) => {
|
|
27
105
|
if (node === null || typeof node !== 'object' || seen.has(node)) {
|
|
28
106
|
return;
|
|
29
107
|
}
|
|
@@ -32,45 +110,40 @@ const attachRefValues = (document, enumerable = false, schemas = getSchemas(docu
|
|
|
32
110
|
const base = getId(node) ?? context;
|
|
33
111
|
if (isObject(node) && typeof node.$ref === 'string') {
|
|
34
112
|
const ref = node.$ref;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (path === undefined && ref.split('#')[0]) {
|
|
40
|
-
hasExternalReferences = true;
|
|
41
|
-
}
|
|
42
|
-
if (enumerable) {
|
|
43
|
-
const followed = new WeakSet();
|
|
44
|
-
let resolved = target;
|
|
45
|
-
// TypeBox needs the final target, so collapse a pre-existing reference chain safely.
|
|
46
|
-
while (isObject(resolved) && '$ref-value' in resolved && !followed.has(resolved)) {
|
|
47
|
-
followed.add(resolved);
|
|
48
|
-
resolved = resolved['$ref-value'];
|
|
113
|
+
if (detectOnly) {
|
|
114
|
+
// JSON Magic only needs to bundle references outside this document's resource index.
|
|
115
|
+
if (ref.split('#')[0] && convertToLocalRef(ref, base, schemas) === undefined) {
|
|
116
|
+
hasExternalReferences = true;
|
|
49
117
|
}
|
|
50
|
-
|
|
118
|
+
}
|
|
119
|
+
else if (getTargetSchema(position, kind)) {
|
|
120
|
+
const resolved = resolve(ref, base);
|
|
121
|
+
const expectedSchema = getTargetSchema(position, kind);
|
|
122
|
+
const schema = resolved?.schema === expectedSchema ? undefined : expectedSchema;
|
|
123
|
+
// Point to the target instead of copying it into every reference.
|
|
124
|
+
const target = schema ? castFallback(resolved?.value, schema, position ?? 'document') : resolved?.value;
|
|
125
|
+
if (target !== undefined) {
|
|
51
126
|
Object.defineProperty(node, '$ref-value', {
|
|
52
|
-
value:
|
|
53
|
-
enumerable,
|
|
127
|
+
value: target,
|
|
128
|
+
enumerable: false,
|
|
54
129
|
configurable: true,
|
|
55
130
|
writable: true,
|
|
56
131
|
});
|
|
57
132
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
enumerable,
|
|
63
|
-
configurable: true,
|
|
64
|
-
writable: true,
|
|
65
|
-
});
|
|
133
|
+
// Fallback targets live outside the walked tree, so link their own references too.
|
|
134
|
+
if (resolved && (!resolved.linked || schema)) {
|
|
135
|
+
visit(target, resolved.context, position, kind);
|
|
136
|
+
}
|
|
66
137
|
}
|
|
67
138
|
}
|
|
68
139
|
// Preserve the nearest resource base while walking into child schemas.
|
|
69
|
-
for (const child of Object.
|
|
70
|
-
|
|
140
|
+
for (const [key, child] of Object.entries(node)) {
|
|
141
|
+
const childKind = kind && getChildKind(kind, key);
|
|
142
|
+
const childPosition = Array.isArray(node) ? position : position && getChildPosition(position, key);
|
|
143
|
+
visit(child, base, childPosition === 'document' && childKind === undefined ? undefined : childPosition, childKind);
|
|
71
144
|
}
|
|
72
145
|
};
|
|
73
|
-
visit(document);
|
|
146
|
+
visit(document, getId(document) ?? '', 'document', 'document');
|
|
74
147
|
return hasExternalReferences;
|
|
75
148
|
};
|
|
76
149
|
/** Coerce one plain document and keep references linked to shared targets. */
|
|
@@ -108,7 +181,7 @@ export const loadDocument = async (input) => {
|
|
|
108
181
|
const declaredOpenapiVersion = typeof raw.openapi === 'string' ? raw.openapi : '2.0';
|
|
109
182
|
const { document: upgraded } = upgrade(raw, '3.2', { onIncompatible: 'collect' });
|
|
110
183
|
const upgradedSchemas = getSchemas(upgraded);
|
|
111
|
-
const hasExternalReferences = attachRefValues(upgraded,
|
|
184
|
+
const hasExternalReferences = attachRefValues(upgraded, upgradedSchemas, { detectOnly: true });
|
|
112
185
|
let document = upgraded;
|
|
113
186
|
let schemas = upgradedSchemas;
|
|
114
187
|
// Resolve external targets only when the initial local-reference pass finds them.
|
|
@@ -130,17 +203,21 @@ export const loadDocument = async (input) => {
|
|
|
130
203
|
throw new Error(errors.join('\n'));
|
|
131
204
|
}
|
|
132
205
|
schemas = getSchemas(document);
|
|
133
|
-
attachRefValues(document, false, schemas);
|
|
134
206
|
}
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
207
|
+
// Cast the document before linking references. TypeBox clones every value that already
|
|
208
|
+
// matches a union or array schema, and its clone follows all own properties, including
|
|
209
|
+
// non-enumerable `$ref-value` links. On densely linked descriptions, each clone would copy
|
|
210
|
+
// most of the reference graph again, which costs gigabytes of transient memory.
|
|
211
|
+
// Reference branches accept a missing `$ref-value`, and every target is cast in its own
|
|
212
|
+
// position, so one target that fails a strict check can no longer replace the reference.
|
|
138
213
|
const coerced = coerceValue(OpenAPIDocumentSchema, document);
|
|
139
214
|
// Rendering must use the declared version for features added after OpenAPI 3.1.
|
|
140
215
|
coerced['x-original-oas-version'] = declaredOpenapiVersion;
|
|
141
216
|
// Boolean schemas were introduced in OpenAPI 3.1; older descriptions retain their existing coercion.
|
|
142
|
-
|
|
217
|
+
const booleanSchemas = /^3\.[12]\./.test(declaredOpenapiVersion);
|
|
218
|
+
if (booleanSchemas) {
|
|
143
219
|
restoreBooleanSchemas(document, coerced);
|
|
220
|
+
}
|
|
144
221
|
// Keep extension resources that local and bundled references can target.
|
|
145
222
|
for (const [key, value] of Object.entries(document)) {
|
|
146
223
|
if (key.startsWith('x-') && !(key in coerced)) {
|
|
@@ -152,7 +229,8 @@ export const loadDocument = async (input) => {
|
|
|
152
229
|
});
|
|
153
230
|
}
|
|
154
231
|
}
|
|
155
|
-
//
|
|
156
|
-
|
|
232
|
+
// Link references to coerced targets. Targets that coercion dropped fall back to source values,
|
|
233
|
+
// which are cast according to the object kind expected at the reference.
|
|
234
|
+
attachRefValues(coerced, schemas, { fallback: { source: document, booleanSchemas } });
|
|
157
235
|
return coerced;
|
|
158
236
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { coerceValue } from '@scalar/workspace-store/schemas/typebox-coerce';
|
|
2
|
+
/** The OpenAPI objects outside of schemas that the linking pass tells apart. */
|
|
3
|
+
export type ObjectKind = 'document' | 'components' | 'pathItem' | 'operation' | 'callback' | 'response' | 'requestBody' | 'mediaType' | 'encoding' | 'parameter' | 'header' | 'example' | 'link' | 'securityScheme';
|
|
4
|
+
/** A value is either one object of a kind, or a map or array whose values all have that kind. */
|
|
5
|
+
export type KindPosition = ObjectKind | {
|
|
6
|
+
values: ObjectKind;
|
|
7
|
+
};
|
|
8
|
+
type TargetSchema = Parameters<typeof coerceValue>[0];
|
|
9
|
+
/** Returns the kind of the value stored under `key`, or `undefined` outside the known structure. */
|
|
10
|
+
export declare const getChildKind: (position: KindPosition, key: string) => KindPosition | undefined;
|
|
11
|
+
/** Strict schemas for the objects that a Reference Object can stand in for. */
|
|
12
|
+
export declare const referenceTargetSchemas: Partial<Record<ObjectKind, TargetSchema>>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=object-kinds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"object-kinds.d.ts","sourceRoot":"","sources":["../src/object-kinds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gDAAgD,CAAA;AAejF,gFAAgF;AAChF,MAAM,MAAM,UAAU,GAClB,UAAU,GACV,YAAY,GACZ,UAAU,GACV,WAAW,GACX,UAAU,GACV,UAAU,GACV,aAAa,GACb,WAAW,GACX,UAAU,GACV,WAAW,GACX,QAAQ,GACR,SAAS,GACT,MAAM,GACN,gBAAgB,CAAA;AAEpB,iGAAiG;AACjG,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAAA;AAE9D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAA;AAmDrD,oGAAoG;AACpG,eAAO,MAAM,YAAY,GAAI,UAAU,YAAY,EAAE,KAAK,MAAM,KAAG,YAAY,GAAG,SAMjF,CAAA;AAED,+EAA+E;AAC/E,eAAO,MAAM,sBAAsB,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,CAY5E,CAAA"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CallbackObjectSchema, ExampleObjectSchema, HeaderObjectSchema, LinkObjectSchema, MediaTypeObjectSchema, OperationObjectSchema, ParameterObjectSchema, PathItemObjectSchema, RequestBodyObjectSchema, ResponseObjectSchema, SecuritySchemeObjectSchema, } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
|
|
2
|
+
const methods = ['get', 'put', 'post', 'delete', 'patch', 'options', 'head', 'trace', 'connect', 'query'];
|
|
3
|
+
/**
|
|
4
|
+
* Mirrors the OpenAPI 3.2 structure of the strict workspace schemas, so a reference knows which
|
|
5
|
+
* object it stands in for. A `*` key applies to every field, such as the expressions of a callback.
|
|
6
|
+
*/
|
|
7
|
+
const childKinds = {
|
|
8
|
+
document: {
|
|
9
|
+
paths: { values: 'pathItem' },
|
|
10
|
+
webhooks: { values: 'pathItem' },
|
|
11
|
+
components: 'components',
|
|
12
|
+
},
|
|
13
|
+
components: {
|
|
14
|
+
responses: { values: 'response' },
|
|
15
|
+
parameters: { values: 'parameter' },
|
|
16
|
+
examples: { values: 'example' },
|
|
17
|
+
requestBodies: { values: 'requestBody' },
|
|
18
|
+
headers: { values: 'header' },
|
|
19
|
+
securitySchemes: { values: 'securityScheme' },
|
|
20
|
+
links: { values: 'link' },
|
|
21
|
+
callbacks: { values: 'callback' },
|
|
22
|
+
pathItems: { values: 'pathItem' },
|
|
23
|
+
mediaTypes: { values: 'mediaType' },
|
|
24
|
+
},
|
|
25
|
+
pathItem: {
|
|
26
|
+
...Object.fromEntries(methods.map((method) => [method, 'operation'])),
|
|
27
|
+
additionalOperations: { values: 'operation' },
|
|
28
|
+
parameters: { values: 'parameter' },
|
|
29
|
+
},
|
|
30
|
+
operation: {
|
|
31
|
+
parameters: { values: 'parameter' },
|
|
32
|
+
requestBody: 'requestBody',
|
|
33
|
+
responses: { values: 'response' },
|
|
34
|
+
callbacks: { values: 'callback' },
|
|
35
|
+
},
|
|
36
|
+
callback: { '*': 'pathItem' },
|
|
37
|
+
response: { headers: { values: 'header' }, content: { values: 'mediaType' }, links: { values: 'link' } },
|
|
38
|
+
requestBody: { content: { values: 'mediaType' } },
|
|
39
|
+
mediaType: {
|
|
40
|
+
examples: { values: 'example' },
|
|
41
|
+
encoding: { values: 'encoding' },
|
|
42
|
+
prefixEncoding: { values: 'encoding' },
|
|
43
|
+
itemEncoding: 'encoding',
|
|
44
|
+
},
|
|
45
|
+
encoding: { headers: { values: 'header' } },
|
|
46
|
+
parameter: { examples: { values: 'example' }, content: { values: 'mediaType' } },
|
|
47
|
+
header: { examples: { values: 'example' }, content: { values: 'mediaType' } },
|
|
48
|
+
};
|
|
49
|
+
/** Returns the kind of the value stored under `key`, or `undefined` outside the known structure. */
|
|
50
|
+
export const getChildKind = (position, key) => {
|
|
51
|
+
if (typeof position !== 'string') {
|
|
52
|
+
return position.values;
|
|
53
|
+
}
|
|
54
|
+
const children = childKinds[position];
|
|
55
|
+
return children?.[key] ?? children?.['*'];
|
|
56
|
+
};
|
|
57
|
+
/** Strict schemas for the objects that a Reference Object can stand in for. */
|
|
58
|
+
export const referenceTargetSchemas = {
|
|
59
|
+
pathItem: PathItemObjectSchema,
|
|
60
|
+
operation: OperationObjectSchema,
|
|
61
|
+
callback: CallbackObjectSchema,
|
|
62
|
+
response: ResponseObjectSchema,
|
|
63
|
+
requestBody: RequestBodyObjectSchema,
|
|
64
|
+
mediaType: MediaTypeObjectSchema,
|
|
65
|
+
parameter: ParameterObjectSchema,
|
|
66
|
+
header: HeaderObjectSchema,
|
|
67
|
+
example: ExampleObjectSchema,
|
|
68
|
+
link: LinkObjectSchema,
|
|
69
|
+
securityScheme: SecuritySchemeObjectSchema,
|
|
70
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-document.d.ts","sourceRoot":"","sources":["../src/render-document.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAmB,MAAM,8DAA8D,CAAA;AAepH,2FAA2F;AAC3F,eAAO,MAAM,sBAAsB,QAAO,CAAC,CAAC,QAAQ,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"render-document.d.ts","sourceRoot":"","sources":["../src/render-document.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAmB,MAAM,8DAA8D,CAAA;AAepH,2FAA2F;AAC3F,eAAO,MAAM,sBAAsB,QAAO,CAAC,CAAC,QAAQ,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,CA6IxF,CAAA"}
|
package/dist/render-document.js
CHANGED
|
@@ -13,9 +13,11 @@ const serializer = unified().use(remarkGfm).use(remarkStringify, { bullet: '-' }
|
|
|
13
13
|
/** Build Markdown directly, retaining caches only for this immutable document snapshot. */
|
|
14
14
|
export const createDocumentRenderer = () => {
|
|
15
15
|
const descriptions = createDescriptionParser();
|
|
16
|
-
const
|
|
16
|
+
const schemaRenderer = createSchemaRenderer();
|
|
17
17
|
return async (document) => {
|
|
18
18
|
const description = descriptions();
|
|
19
|
+
// Each page expands a shared schema once, then refers back to it.
|
|
20
|
+
const schemas = schemaRenderer.forDocument(document.components?.schemas);
|
|
19
21
|
const { info } = document;
|
|
20
22
|
const metadata = [
|
|
21
23
|
field('OpenAPI Version', inlineCode(document.openapi)),
|
|
@@ -85,6 +87,7 @@ export const createDocumentRenderer = () => {
|
|
|
85
87
|
nodes.push(heading(2, text(group.title)));
|
|
86
88
|
hasOperations = true;
|
|
87
89
|
}
|
|
90
|
+
schemas.beginSection();
|
|
88
91
|
nodes.push(...(await renderOperation(document, path, method, pathItem, operation, group.webhook, {
|
|
89
92
|
description,
|
|
90
93
|
schemas,
|
|
@@ -97,12 +100,13 @@ export const createDocumentRenderer = () => {
|
|
|
97
100
|
if (models.length)
|
|
98
101
|
nodes.push(heading(2, text('Schemas')));
|
|
99
102
|
for (const [name, schema] of models) {
|
|
103
|
+
schemas.beginSection();
|
|
100
104
|
const view = schemas.view(schema);
|
|
101
105
|
nodes.push(heading(3, text(view.title ?? name)), list([
|
|
102
106
|
view.type
|
|
103
107
|
? field('Type', inlineCode(Array.isArray(view.type) ? view.type.join(' | ') : view.type))
|
|
104
108
|
: item(paragraph(strong(text('Type:')))),
|
|
105
|
-
]), ...(await description(view.description)), ...schemas.render(schema, 0, [], { hideDescription: true }));
|
|
109
|
+
]), ...(await description(view.description)), ...schemas.render(schema, 0, [], { hideDescription: true, name }));
|
|
106
110
|
if (view.type === 'object')
|
|
107
111
|
nodes.push(...(await renderExamples({ schema }, description, 'application/json', undefined, document['x-original-oas-version'] ?? document.openapi)));
|
|
108
112
|
flush();
|
|
@@ -2,5 +2,5 @@ import type { RootContent } from 'mdast';
|
|
|
2
2
|
import { type ExampleSource } from './get-markdown-examples.js';
|
|
3
3
|
import type { DescriptionParser } from './parse-description.js';
|
|
4
4
|
/** Render supplied examples before considering a schema-generated fallback. */
|
|
5
|
-
export declare const renderExamples: (source: ExampleSource, description: DescriptionParser, mediaType?: string, mode?: "read" | "write", openapiVersion?: string) => Promise<RootContent[]>;
|
|
5
|
+
export declare const renderExamples: (source: ExampleSource, description: DescriptionParser, mediaType?: string, mode?: "read" | "write", openapiVersion?: string, schemaOpenapiVersion?: string) => Promise<RootContent[]>;
|
|
6
6
|
//# sourceMappingURL=render-examples.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-examples.d.ts","sourceRoot":"","sources":["../src/render-examples.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"render-examples.d.ts","sourceRoot":"","sources":["../src/render-examples.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAExC,OAAO,EAAE,KAAK,aAAa,EAAuB,MAAM,yBAAyB,CAAA;AAEjF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE5D,+EAA+E;AAC/E,eAAO,MAAM,cAAc,GACzB,QAAQ,aAAa,EACrB,aAAa,iBAAiB,EAC9B,kBAA8B,EAC9B,OAAO,MAAM,GAAG,OAAO,EACvB,uBAAwB,EAExB,6BAAqC,KACpC,OAAO,CAAC,WAAW,EAAE,CAmDvB,CAAA"}
|
package/dist/render-examples.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isXmlMediaType } from '@scalar/helpers/http/is-xml-media-type';
|
|
2
|
+
import { getXmlBodyExample } from '@scalar/workspace-store/request-example';
|
|
2
3
|
import { getMarkdownExamples } from './get-markdown-examples.js';
|
|
3
4
|
import { link, paragraph, strong, text } from './markdown-nodes.js';
|
|
4
5
|
/** Render supplied examples before considering a schema-generated fallback. */
|
|
5
|
-
export const renderExamples = async (source, description, mediaType = 'application/json', mode, openapiVersion = '3.2.0'
|
|
6
|
+
export const renderExamples = async (source, description, mediaType = 'application/json', mode, openapiVersion = '3.2.0',
|
|
7
|
+
// Schema metadata can be upgraded while example fields still follow the original version.
|
|
8
|
+
schemaOpenapiVersion = openapiVersion) => {
|
|
6
9
|
const nodes = [];
|
|
7
|
-
for (const example of getMarkdownExamples(source, mediaType, mode, openapiVersion)) {
|
|
10
|
+
for (const example of getMarkdownExamples(source, mediaType, mode, openapiVersion, schemaOpenapiVersion)) {
|
|
8
11
|
nodes.push(paragraph(strong(text(example.name ? `Example: ${example.name}` : 'Example:'))));
|
|
12
|
+
if ('omitted' in example) {
|
|
13
|
+
nodes.push(paragraph(text('[Generated example omitted because it is too large]')));
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
9
16
|
if (example.summary)
|
|
10
17
|
nodes.push(paragraph(text(example.summary)));
|
|
11
18
|
nodes.push(...(await description(example.description)));
|
|
19
|
+
if ('error' in example) {
|
|
20
|
+
nodes.push(paragraph(text(example.error)));
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
12
23
|
if ('externalValue' in example) {
|
|
13
24
|
nodes.push(paragraph(strong(text('External value:')), text(' '), link(example.externalValue, example.externalValue)));
|
|
14
25
|
continue;
|
|
@@ -16,21 +27,31 @@ export const renderExamples = async (source, description, mediaType = 'applicati
|
|
|
16
27
|
if ('serializedValue' in example) {
|
|
17
28
|
nodes.push({
|
|
18
29
|
type: 'code',
|
|
19
|
-
lang: mediaType
|
|
30
|
+
lang: isXmlMediaType(mediaType) ? 'xml' : mediaType.includes('json') ? 'json' : 'text',
|
|
20
31
|
value: example.serializedValue,
|
|
21
32
|
});
|
|
22
33
|
continue;
|
|
23
34
|
}
|
|
24
|
-
const xml = mediaType
|
|
35
|
+
const xml = isXmlMediaType(mediaType);
|
|
36
|
+
const value = 'dataValue' in example ? example.dataValue : example.value;
|
|
37
|
+
if (xml && !source.schema && 'value' in example && (value === null || typeof value !== 'object')) {
|
|
38
|
+
nodes.push({ type: 'code', lang: 'xml', value: String(value) });
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const result = xml
|
|
42
|
+
? getXmlBodyExample(source.schema, example, {
|
|
43
|
+
mode,
|
|
44
|
+
openapiVersion: schemaOpenapiVersion,
|
|
45
|
+
})
|
|
46
|
+
: undefined;
|
|
47
|
+
if (xml && result?.xml === undefined) {
|
|
48
|
+
nodes.push(paragraph(text('Unable to generate an XML example.')));
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
25
51
|
nodes.push({
|
|
26
52
|
type: 'code',
|
|
27
53
|
lang: xml ? 'xml' : 'json',
|
|
28
|
-
|
|
29
|
-
value: xml
|
|
30
|
-
? example.value !== null && typeof example.value === 'object'
|
|
31
|
-
? json2xml(example.value)
|
|
32
|
-
: String(example.value)
|
|
33
|
-
: (JSON.stringify(example.value, null, 2) ?? ''),
|
|
54
|
+
value: result?.xml ?? JSON.stringify(value, null, 2) ?? '',
|
|
34
55
|
});
|
|
35
56
|
}
|
|
36
57
|
return nodes;
|