@scalar/openapi-to-markdown 1.4.0 → 1.5.1

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +66 -5
  3. package/dist/create-markdown-from-openapi.d.ts.map +1 -1
  4. package/dist/create-markdown-from-openapi.js +3 -2
  5. package/dist/document-anchors.d.ts +8 -0
  6. package/dist/document-anchors.d.ts.map +1 -0
  7. package/dist/document-anchors.js +25 -0
  8. package/dist/document-examples.d.ts +14 -0
  9. package/dist/document-examples.d.ts.map +1 -0
  10. package/dist/document-examples.js +54 -0
  11. package/dist/get-markdown-examples.d.ts +2 -0
  12. package/dist/get-markdown-examples.d.ts.map +1 -1
  13. package/dist/get-markdown-examples.js +11 -12
  14. package/dist/markdown-nodes.d.ts +22 -1
  15. package/dist/markdown-nodes.d.ts.map +1 -1
  16. package/dist/markdown-nodes.js +4 -0
  17. package/dist/parse-description.d.ts +2 -0
  18. package/dist/parse-description.d.ts.map +1 -1
  19. package/dist/parse-description.js +28 -1
  20. package/dist/render-document.d.ts +2 -2
  21. package/dist/render-document.d.ts.map +1 -1
  22. package/dist/render-document.js +217 -84
  23. package/dist/render-examples.d.ts +9 -1
  24. package/dist/render-examples.d.ts.map +1 -1
  25. package/dist/render-examples.js +26 -8
  26. package/dist/render-operation-details.d.ts +3 -2
  27. package/dist/render-operation-details.d.ts.map +1 -1
  28. package/dist/render-operation-details.js +8 -5
  29. package/dist/render-operation.d.ts +16 -1
  30. package/dist/render-operation.d.ts.map +1 -1
  31. package/dist/render-operation.js +168 -42
  32. package/dist/render-schema.d.ts +30 -3
  33. package/dist/render-schema.d.ts.map +1 -1
  34. package/dist/render-schema.js +474 -111
  35. package/dist/render-security.d.ts +7 -3
  36. package/dist/render-security.d.ts.map +1 -1
  37. package/dist/render-security.js +61 -12
  38. package/dist/select-document.d.ts +34 -3
  39. package/dist/select-document.d.ts.map +1 -1
  40. package/dist/select-document.js +58 -30
  41. package/package.json +18 -9
@@ -1,6 +1,10 @@
1
1
  import type { OpenApiDocument } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
2
- import type { RootContent } from 'mdast';
2
+ import type { Heading, RootContent } from 'mdast';
3
3
  import type { DescriptionParser } from './parse-description.js';
4
- /** Preserve OR between requirements and AND between schemes within a requirement. */
5
- export declare const renderSecurity: (requirements: OpenApiDocument["security"], schemes: NonNullable<OpenApiDocument["components"]>["securitySchemes"], description: DescriptionParser) => Promise<RootContent[]>;
4
+ /**
5
+ * Preserve OR between requirements and AND between schemes within a requirement.
6
+ * Without declared requirements there is nothing to say: only an explicit empty list means that
7
+ * no authentication is required.
8
+ */
9
+ export declare const renderSecurity: (requirements: OpenApiDocument["security"], schemes: NonNullable<OpenApiDocument["components"]>["securitySchemes"], description: DescriptionParser, level?: Heading["depth"]) => Promise<RootContent[]>;
6
10
  //# sourceMappingURL=render-security.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"render-security.d.ts","sourceRoot":"","sources":["../src/render-security.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8DAA8D,CAAA;AACnG,OAAO,KAAK,EAAY,WAAW,EAAE,MAAM,OAAO,CAAA;AAGlD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAE5D,qFAAqF;AACrF,eAAO,MAAM,cAAc,GACzB,cAAc,eAAe,CAAC,UAAU,CAAC,EACzC,SAAS,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC,iBAAiB,CAAC,EACtE,aAAa,iBAAiB,KAC7B,OAAO,CAAC,WAAW,EAAE,CA0BvB,CAAA"}
1
+ {"version":3,"file":"render-security.d.ts","sourceRoot":"","sources":["../src/render-security.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8DAA8D,CAAA;AACnG,OAAO,KAAK,EAAE,OAAO,EAA6B,WAAW,EAAE,MAAM,OAAO,CAAA;AAG5E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAsD5D;;;;GAIG;AACH,eAAO,MAAM,cAAc,GACzB,cAAc,eAAe,CAAC,UAAU,CAAC,EACzC,SAAS,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC,iBAAiB,CAAC,EACtE,aAAa,iBAAiB,EAC9B,QAAO,OAAO,CAAC,OAAO,CAAK,KAC1B,OAAO,CAAC,WAAW,EAAE,CAsBvB,CAAA"}
@@ -1,10 +1,61 @@
1
1
  import { getResolvedRef } from '@scalar/workspace-store/helpers/get-resolved-ref';
2
- import { heading, item, list, paragraph, strong, text } from './markdown-nodes.js';
3
- /** Preserve OR between requirements and AND between schemes within a requirement. */
4
- export const renderSecurity = async (requirements, schemes, description) => {
2
+ import { heading, inlineCode, item, list, paragraph, strong, text } from './markdown-nodes.js';
3
+ const flowNames = {
4
+ implicit: 'implicit',
5
+ password: 'password',
6
+ clientCredentials: 'client credentials',
7
+ authorizationCode: 'authorization code',
8
+ deviceAuthorization: 'device authorization',
9
+ };
10
+ /** Summarize a security scheme on one line, for example "API key in header `X-Api-Key`". */
11
+ const summarizeScheme = (scheme) => {
12
+ const value = scheme;
13
+ switch (value.type) {
14
+ case 'apiKey':
15
+ return [text(`API key in ${String(value.in ?? 'header')} `), inlineCode(value.name)];
16
+ case 'http': {
17
+ const name = typeof value.scheme === 'string' ? value.scheme.toLowerCase() : '';
18
+ return [
19
+ text(`HTTP ${name || 'authentication'}`),
20
+ ...(typeof value.bearerFormat === 'string' ? [text(' ('), inlineCode(value.bearerFormat), text(')')] : []),
21
+ ];
22
+ }
23
+ case 'oauth2': {
24
+ const flows = Object.entries(typeof value.flows === 'object' && value.flows ? value.flows : {});
25
+ const nodes = [text('OAuth 2.0')];
26
+ for (const [index, [flow, settings]] of flows.entries()) {
27
+ const urls = settings;
28
+ nodes.push(text(`${index ? '; ' : ': '}${flowNames[flow] ?? flow}`));
29
+ for (const [key, label] of [
30
+ ['authorizationUrl', 'authorize'],
31
+ ['deviceAuthorizationUrl', 'device authorization'],
32
+ ['tokenUrl', 'token'],
33
+ ['refreshUrl', 'refresh'],
34
+ ]) {
35
+ // Coercion fills in optional URLs as empty strings.
36
+ if (typeof urls[key] === 'string' && urls[key])
37
+ nodes.push(text(`, ${label} `), inlineCode(urls[key]));
38
+ }
39
+ }
40
+ return nodes;
41
+ }
42
+ case 'openIdConnect':
43
+ return [text('OpenID Connect '), inlineCode(value.openIdConnectUrl)];
44
+ case 'mutualTLS':
45
+ return [text('Mutual TLS')];
46
+ default:
47
+ return [inlineCode(value.type)];
48
+ }
49
+ };
50
+ /**
51
+ * Preserve OR between requirements and AND between schemes within a requirement.
52
+ * Without declared requirements there is nothing to say: only an explicit empty list means that
53
+ * no authentication is required.
54
+ */
55
+ export const renderSecurity = async (requirements, schemes, description, level = 4) => {
5
56
  if (!requirements)
6
57
  return [];
7
- const nodes = [heading(4, text('Authentication'))];
58
+ const nodes = [heading(level, text('Authentication'))];
8
59
  if (!requirements.length)
9
60
  nodes.push(paragraph(text('No authentication required.')));
10
61
  for (const [index, requirement] of requirements.entries()) {
@@ -17,15 +68,13 @@ export const renderSecurity = async (requirements, schemes, description) => {
17
68
  }
18
69
  const entriesNodes = [];
19
70
  for (const [name, scopes] of entries) {
20
- const blocks = [
21
- paragraph(strong(text(name)), ...(scopes?.length ? [text(` Scopes: ${scopes.join(', ')}`)] : [])),
22
- ];
23
71
  const scheme = getResolvedRef(schemes?.[name]);
24
- if (scheme) {
25
- blocks.push({ type: 'code', value: JSON.stringify(scheme, null, 2) });
26
- blocks.push(...(await description(scheme.description)));
27
- }
28
- entriesNodes.push(item(...blocks));
72
+ const line = [strong(text(name))];
73
+ if (scheme)
74
+ line.push(text(': '), ...summarizeScheme(scheme));
75
+ if (scopes?.length)
76
+ line.push(text(', scopes: '), inlineCode(scopes.join(', ')));
77
+ entriesNodes.push(item(paragraph(...line), ...(await description(scheme?.description))));
29
78
  }
30
79
  nodes.push(list(entriesNodes));
31
80
  }
@@ -1,5 +1,5 @@
1
1
  import type { OperationMethod } from '@scalar/workspace-store/schemas/navigation';
2
- import type { OpenApiDocument } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
2
+ import type { OpenApiDocument, PathItemObject } from '@scalar/workspace-store/schemas/v3.2/strict/openapi-document';
3
3
  /** Identify one operation by path and method, operation ID, or JSON pointer. */
4
4
  export type OperationSelector = {
5
5
  path: string;
@@ -23,6 +23,11 @@ export type SchemaReferenceOptions = {
23
23
  ref: string;
24
24
  name: string;
25
25
  }) => string | undefined;
26
+ /**
27
+ * Render references to primitive, enum and `const` schemas, and to aliases of them, in place
28
+ * instead of linking them. Their whole definition fits on one line. Defaults to `true`.
29
+ */
30
+ inlinePrimitives?: boolean;
26
31
  };
27
32
  };
28
33
  type PageSelectors = {
@@ -35,7 +40,33 @@ type PageSelectors = {
35
40
  };
36
41
  introduction: true;
37
42
  };
38
- /** Scope after resolving references and migrating older documents. */
39
- export declare const selectDocument: (document: OpenApiDocument, options?: OpenApiRenderOptions) => OpenApiDocument;
43
+ type OperationMatch = {
44
+ path: string;
45
+ method: OperationMethod;
46
+ };
47
+ type TaggedPathItem = {
48
+ path: string;
49
+ pathItem: PathItemObject;
50
+ methods: string[];
51
+ };
52
+ /**
53
+ * Lookups that every selection from one document shares, built on first use.
54
+ * The document must not change while its lookup is in use.
55
+ */
56
+ type DocumentLookup = {
57
+ /** Operations by operation ID, in document order. */
58
+ operationsById: () => ReadonlyMap<string, OperationMatch[]>;
59
+ /** Path items with the methods that carry each tag, in document order. */
60
+ operationsByTag: () => ReadonlyMap<string, TaggedPathItem[]>;
61
+ /** The position of each schema in `components.schemas`. */
62
+ schemaPositions: () => ReadonlyMap<string, number>;
63
+ };
64
+ /** Index a document once so that each selection costs time in proportion to what it selects. */
65
+ export declare const createDocumentLookup: (document: OpenApiDocument) => DocumentLookup;
66
+ /**
67
+ * Scope after resolving references and migrating older documents.
68
+ * Pass the same lookup to every selection from one document to reuse its indexes.
69
+ */
70
+ export declare const selectDocument: (document: OpenApiDocument, options?: OpenApiRenderOptions, lookup?: DocumentLookup) => OpenApiDocument;
40
71
  export {};
41
72
  //# sourceMappingURL=select-document.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"select-document.d.ts","sourceRoot":"","sources":["../src/select-document.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAA;AACjF,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,8DAA8D,CAAA;AAEnH,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GACzB;IACE,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,eAAe,CAAA;CACxB,GACD;IACE,WAAW,EAAE,MAAM,CAAA;CACpB,GACD;IACE,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AACL,2EAA2E;AAC3E,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,GACvD,CACI;KACG,GAAG,IAAI,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,aAAa,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,GACrF,IAAI,CAAC,aAAa,EAAE,GAAG,CAAC;CAC3B,CAAC,MAAM,aAAa,CAAC,GACtB,OAAO,CAAC,MAAM,CAAC,MAAM,aAAa,EAAE,KAAK,CAAC,CAAC,CAC9C,CAAA;AAEH,mFAAmF;AACnF,MAAM,MAAM,sBAAsB,GAAG;IACnC,4GAA4G;IAC5G,gBAAgB,CAAC,EAAE;QACjB,IAAI,EAAE,QAAQ,CAAA;QACd,oFAAoF;QACpF,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,KAAK,MAAM,GAAG,SAAS,CAAA;KAC9E,CAAA;CACF,CAAA;AAED,KAAK,aAAa,GAAG;IACnB,SAAS,EAAE,iBAAiB,CAAA;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,eAAe,CAAA;KAAE,CAAA;IAClD,YAAY,EAAE,IAAI,CAAA;CACnB,CAAA;AAyKD,sEAAsE;AACtE,eAAO,MAAM,cAAc,GAAI,UAAU,eAAe,EAAE,UAAS,oBAAyB,KAAG,eA2N9F,CAAA"}
1
+ {"version":3,"file":"select-document.d.ts","sourceRoot":"","sources":["../src/select-document.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4CAA4C,CAAA;AACjF,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,8DAA8D,CAAA;AAEnH,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GACzB;IACE,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,eAAe,CAAA;CACxB,GACD;IACE,WAAW,EAAE,MAAM,CAAA;CACpB,GACD;IACE,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AACL,2EAA2E;AAC3E,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,GACvD,CACI;KACG,GAAG,IAAI,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,aAAa,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,GACrF,IAAI,CAAC,aAAa,EAAE,GAAG,CAAC;CAC3B,CAAC,MAAM,aAAa,CAAC,GACtB,OAAO,CAAC,MAAM,CAAC,MAAM,aAAa,EAAE,KAAK,CAAC,CAAC,CAC9C,CAAA;AAEH,mFAAmF;AACnF,MAAM,MAAM,sBAAsB,GAAG;IACnC,4GAA4G;IAC5G,gBAAgB,CAAC,EAAE;QACjB,IAAI,EAAE,QAAQ,CAAA;QACd,oFAAoF;QACpF,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,KAAK,MAAM,GAAG,SAAS,CAAA;QAC7E;;;WAGG;QACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAC3B,CAAA;CACF,CAAA;AAED,KAAK,aAAa,GAAG;IACnB,SAAS,EAAE,iBAAiB,CAAA;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,eAAe,CAAA;KAAE,CAAA;IAClD,YAAY,EAAE,IAAI,CAAA;CACnB,CAAA;AACD,KAAK,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAAA;AA+G/D,KAAK,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,cAAc,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAEnF;;;GAGG;AACH,KAAK,cAAc,GAAG;IACpB,qDAAqD;IACrD,cAAc,EAAE,MAAM,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC,CAAA;IAC3D,0EAA0E;IAC1E,eAAe,EAAE,MAAM,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC,CAAA;IAC5D,2DAA2D;IAC3D,eAAe,EAAE,MAAM,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACnD,CAAA;AA8BD,gGAAgG;AAChG,eAAO,MAAM,oBAAoB,GAAI,UAAU,eAAe,KAAG,cAWhE,CAAA;AAwDD;;;GAGG;AACH,eAAO,MAAM,cAAc,GACzB,UAAU,eAAe,EACzB,UAAS,oBAAyB,EAClC,SAAQ,cAA+C,KACtD,eA2NF,CAAA"}
@@ -78,16 +78,43 @@ const findOperationByPathAndMethod = (document, selector) => {
78
78
  method,
79
79
  };
80
80
  };
81
- const findOperationsByOperationId = (document, operationId) => getPathEntries(document).flatMap(([path, pathItem]) => {
82
- const matches = [];
83
- forEachPathItemOperation(pathItem, (method, operation) => {
84
- if (getResolvedRef(operation)?.operationId === operationId) {
85
- matches.push({ path, method });
86
- }
87
- });
88
- return matches;
89
- });
90
- const resolveOperationMatch = (document, selector) => {
81
+ const indexOperations = (document) => {
82
+ const byId = new Map();
83
+ const byTag = new Map();
84
+ for (const [path, pathItem] of getPathEntries(document)) {
85
+ forEachPathItemOperation(pathItem, (method, operationRef) => {
86
+ const operation = getResolvedRef(operationRef);
87
+ if (typeof operation?.operationId === 'string') {
88
+ const matches = byId.get(operation.operationId) ?? [];
89
+ matches.push({ path, method });
90
+ byId.set(operation.operationId, matches);
91
+ }
92
+ for (const tag of new Set(operation?.tags)) {
93
+ const entries = byTag.get(tag) ?? [];
94
+ const last = entries.at(-1);
95
+ if (last?.path === path) {
96
+ last.methods.push(method);
97
+ }
98
+ else {
99
+ entries.push({ path, pathItem, methods: [method] });
100
+ }
101
+ byTag.set(tag, entries);
102
+ }
103
+ });
104
+ }
105
+ return { byId, byTag };
106
+ };
107
+ /** Index a document once so that each selection costs time in proportion to what it selects. */
108
+ export const createDocumentLookup = (document) => {
109
+ let operations;
110
+ let schemaPositions;
111
+ return {
112
+ operationsById: () => (operations ??= indexOperations(document)).byId,
113
+ operationsByTag: () => (operations ??= indexOperations(document)).byTag,
114
+ schemaPositions: () => (schemaPositions ??= new Map(Object.keys(document.components?.schemas ?? {}).map((name, position) => [name, position]))),
115
+ };
116
+ };
117
+ const resolveOperationMatch = (document, selector, lookup) => {
91
118
  if ('pointer' in selector) {
92
119
  const match = getOperationSelectorFromPointer(selector.pointer);
93
120
  if (!getPathItemOperation(document.paths?.[match.path], match.method)) {
@@ -96,7 +123,7 @@ const resolveOperationMatch = (document, selector) => {
96
123
  return match;
97
124
  }
98
125
  if ('operationId' in selector) {
99
- const matches = findOperationsByOperationId(document, selector.operationId);
126
+ const matches = lookup.operationsById().get(selector.operationId) ?? [];
100
127
  if (!matches.length) {
101
128
  throw new Error(`Operation with operationId "${selector.operationId}" was not found`);
102
129
  }
@@ -108,9 +135,9 @@ const resolveOperationMatch = (document, selector) => {
108
135
  }
109
136
  return findOperationByPathAndMethod(document, selector);
110
137
  };
111
- const filterDocumentByOperation = (document, selector) => {
112
- const match = resolveOperationMatch(document, selector);
113
- const pathItem = getPathEntries(document).find(([path]) => path === match.path)?.[1];
138
+ const filterDocumentByOperation = (document, selector, lookup) => {
139
+ const match = resolveOperationMatch(document, selector, lookup);
140
+ const pathItem = getResolvedPathItem(document.paths?.[match.path]);
114
141
  if (!pathItem) {
115
142
  throw new Error(`Operation not found for path "${match.path}" and method "${match.method.toUpperCase()}"`);
116
143
  }
@@ -121,8 +148,11 @@ const filterDocumentByOperation = (document, selector) => {
121
148
  },
122
149
  };
123
150
  };
124
- /** Scope after resolving references and migrating older documents. */
125
- export const selectDocument = (document, options = {}) => {
151
+ /**
152
+ * Scope after resolving references and migrating older documents.
153
+ * Pass the same lookup to every selection from one document to reuse its indexes.
154
+ */
155
+ export const selectDocument = (document, options = {}, lookup = createDocumentLookup(document)) => {
126
156
  if (!isObject(options)) {
127
157
  throw new Error('Render options must be an object');
128
158
  }
@@ -159,7 +189,7 @@ export const selectDocument = (document, options = {}) => {
159
189
  const selected = { ...document, paths: {}, webhooks: {}, tags: [] };
160
190
  const modelRoots = [];
161
191
  if (options.operation) {
162
- selected.paths = filterDocumentByOperation(document, options.operation).paths;
192
+ selected.paths = filterDocumentByOperation(document, options.operation, lookup).paths;
163
193
  }
164
194
  if (options.tag !== undefined) {
165
195
  const metadata = document.tags?.filter((tag) => tag.name === options.tag) ?? [];
@@ -167,16 +197,8 @@ export const selectDocument = (document, options = {}) => {
167
197
  throw new Error(`Multiple tags found for "${options.tag}"`);
168
198
  }
169
199
  selected.tags = metadata.length ? metadata : [{ name: options.tag }];
170
- for (const [path, item] of getPathEntries(document)) {
171
- const methods = [];
172
- forEachPathItemOperation(item, (method, operation) => {
173
- if (getResolvedRef(operation)?.tags?.includes(options.tag)) {
174
- methods.push(method);
175
- }
176
- });
177
- if (methods.length) {
178
- selected.paths[path] = filterPathItemOperations(item, methods);
179
- }
200
+ for (const { path, pathItem, methods } of lookup.operationsByTag().get(options.tag) ?? []) {
201
+ selected.paths[path] = filterPathItemOperations(pathItem, methods);
180
202
  }
181
203
  if (!metadata.length && !Object.keys(selected.paths ?? {}).length) {
182
204
  throw new Error(`Tag "${options.tag}" was not found`);
@@ -231,8 +253,9 @@ export const selectDocument = (document, options = {}) => {
231
253
  parameters.set(`${parameter.in}:${parameter.name}`, ref);
232
254
  }
233
255
  }
234
- const security = operation.security ?? document.security ?? [];
235
- for (const requirement of security) {
256
+ // Undeclared security stays undeclared: an empty list would claim that no authentication is required.
257
+ const security = operation.security ?? document.security;
258
+ for (const requirement of security ?? []) {
236
259
  for (const name of Object.keys(requirement)) {
237
260
  securityNames.add(name);
238
261
  }
@@ -326,9 +349,14 @@ export const selectDocument = (document, options = {}) => {
326
349
  visit(root);
327
350
  }
328
351
  }
352
+ const positions = lookup.schemaPositions();
329
353
  selected.components = {
330
354
  ...document.components,
331
- schemas: Object.fromEntries(Object.entries(schemas).filter(([name]) => needed.has(name))),
355
+ // Keep the document's schema order, which decides the order of the page's model sections.
356
+ schemas: Object.fromEntries([...needed]
357
+ .filter((name) => positions.has(name))
358
+ .sort((a, b) => positions.get(a) - positions.get(b))
359
+ .map((name) => [name, schemas[name]])),
332
360
  securitySchemes: Object.fromEntries(Object.entries(document.components?.securitySchemes ?? {}).filter(([name]) => securityNames.has(name))),
333
361
  };
334
362
  return selected;
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  "llm",
17
17
  "swagger"
18
18
  ],
19
- "version": "1.4.0",
19
+ "version": "1.5.1",
20
20
  "engines": {
21
21
  "node": ">=22"
22
22
  },
@@ -40,11 +40,11 @@
40
40
  "CHANGELOG.md"
41
41
  ],
42
42
  "dependencies": {
43
- "@scalar/code-highlight": "0.4.7",
43
+ "@scalar/code-highlight": "0.4.8",
44
44
  "@scalar/helpers": "0.16.0",
45
- "@scalar/json-magic": "0.15.3",
45
+ "@scalar/json-magic": "0.15.4",
46
46
  "@scalar/openapi-upgrader": "0.4.1",
47
- "@scalar/workspace-store": "0.67.1",
47
+ "@scalar/workspace-store": "0.68.0",
48
48
  "rehype-parse": "^9.0.1",
49
49
  "rehype-remark": "^10.0.1",
50
50
  "rehype-sanitize": "^6.0.0",
@@ -54,16 +54,25 @@
54
54
  "unified": "^11.0.5"
55
55
  },
56
56
  "devDependencies": {
57
- "@hono/node-server": "^2.1.1",
57
+ "@scalar/components": "0.30.6",
58
58
  "@scalar/galaxy": "0.7.1",
59
+ "@scalar/themes": "0.18.1",
60
+ "@tailwindcss/vite": "^4.3.3",
59
61
  "@types/mdast": "^4.0.4",
60
- "hono": "^4.13.8",
61
- "vitest": "4.1.10"
62
+ "@vitejs/plugin-vue": "^6.0.8",
63
+ "marked": "^14.0.0",
64
+ "rehype-stringify": "^10.0.1",
65
+ "tailwindcss": "^4.3.3",
66
+ "tsx": "4.19.4",
67
+ "vite": "8.1.5",
68
+ "vitest": "4.1.10",
69
+ "vue": "^3.5.40",
70
+ "vue-tsc": "^3.3.8"
62
71
  },
63
72
  "scripts": {
64
73
  "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json",
65
- "dev": "tsx watch playground/index.ts",
74
+ "dev": "cd playground && vite",
66
75
  "test": "vitest --run",
67
- "types:check": "tsgo --noEmit"
76
+ "types:check": "tsgo --noEmit && vue-tsc --noEmit -p playground/tsconfig.json"
68
77
  }
69
78
  }