@mintlify/scraping 4.0.1072 → 4.0.1074

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.
@@ -0,0 +1,286 @@
1
+ import { generateOpenApiPages as generateEditorPages } from '@mintlify/common';
2
+ import type { OpenAPIV3_2 } from '@mintlify/validation';
3
+ import type { OpenAPI } from 'openapi-types';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ import { generateOpenApiPages } from '../src/openapi/generateOpenApiPages.js';
7
+ import { generateOpenApiPagesForDocsConfig } from '../src/openapi/generateOpenApiPagesForDocsConfig.js';
8
+
9
+ const endpoint = (tags: string[], summary: string) => ({
10
+ get: { tags, summary, responses: { '200': { description: 'OK' } } },
11
+ });
12
+
13
+ type Tag = { name: string; parent?: string; summary?: string; kind?: string };
14
+
15
+ function document() {
16
+ const tags: Tag[] = [
17
+ { name: 'books', parent: 'products', summary: 'Books' },
18
+ { name: 'products', parent: 'store', summary: 'Products' },
19
+ { name: 'store', summary: 'Store' },
20
+ ];
21
+ return {
22
+ openapi: '3.2.0',
23
+ info: { title: 'Store', version: '1' },
24
+ tags,
25
+ paths: {
26
+ '/books': endpoint(['books'], 'List books'),
27
+ '/products': endpoint(['products'], 'List products'),
28
+ },
29
+ webhooks: {
30
+ bookCreated: {
31
+ post: {
32
+ tags: ['books'],
33
+ summary: 'Book created',
34
+ responses: { '200': { description: 'OK' } },
35
+ },
36
+ },
37
+ },
38
+ };
39
+ }
40
+
41
+ describe('3.2 methods', () => {
42
+ it('generates pages for query and additionalOperations with the wire method in the meta tag', async () => {
43
+ const base = document();
44
+ const spec: OpenAPIV3_2.Document = {
45
+ ...base,
46
+ paths: {
47
+ ...base.paths,
48
+ '/books': {
49
+ ...base.paths['/books'],
50
+ query: {
51
+ tags: ['books'],
52
+ summary: 'Search books',
53
+ responses: { '200': { description: 'OK' } },
54
+ },
55
+ additionalOperations: {
56
+ PURGE: {
57
+ tags: ['books'],
58
+ summary: 'Purge books',
59
+ responses: { '204': { description: 'Purged' } },
60
+ },
61
+ },
62
+ },
63
+ },
64
+ };
65
+ const { pagesAcc } = await generateOpenApiPagesForDocsConfig(spec as OpenAPI.Document, {
66
+ writeFiles: false,
67
+ outDir: 'api-reference',
68
+ });
69
+ expect(Object.values(pagesAcc).map((page) => [page.href, page.openapi])).toEqual(
70
+ expect.arrayContaining([
71
+ ['/api-reference/books/search-books', 'query /books'],
72
+ ['/api-reference/books/purge-books', 'PURGE /books'],
73
+ ])
74
+ );
75
+ });
76
+ });
77
+
78
+ describe.each([
79
+ ['docs.json', generateOpenApiPagesForDocsConfig],
80
+ ['mint.json', generateOpenApiPages],
81
+ ] as const)('%s tag hierarchy', (_name, generate) => {
82
+ it('generates nested paths and webhooks with matching decorated navigation and stable URLs', async () => {
83
+ const spec = document();
84
+ const result = await generate(spec, {
85
+ writeFiles: false,
86
+ outDir: 'api-reference',
87
+ });
88
+ expect(result.nav).toEqual([
89
+ {
90
+ group: 'Store',
91
+ pages: [
92
+ {
93
+ group: 'Products',
94
+ pages: [
95
+ {
96
+ group: 'Books',
97
+ pages: ['api-reference/books/list-books', 'api-reference/books/book-created'],
98
+ },
99
+ 'api-reference/products/list-products',
100
+ ],
101
+ },
102
+ ],
103
+ },
104
+ ]);
105
+ expect(result.decoratedNav).toMatchObject([
106
+ {
107
+ group: 'Store',
108
+ pages: [
109
+ {
110
+ group: 'Products',
111
+ pages: [
112
+ {
113
+ group: 'Books',
114
+ pages: [
115
+ { href: '/api-reference/books/list-books' },
116
+ { href: '/api-reference/books/book-created' },
117
+ ],
118
+ },
119
+ { href: '/api-reference/products/list-products' },
120
+ ],
121
+ },
122
+ ],
123
+ },
124
+ ]);
125
+ const flat = await generate(
126
+ { ...spec, tags: spec.tags.map(({ name }) => ({ name })), openapi: '3.1.0' },
127
+ { writeFiles: false, outDir: 'api-reference' }
128
+ );
129
+ expect(Object.keys(result.pagesAcc)).toEqual(Object.keys(flat.pagesAcc));
130
+ expect(
131
+ generateEditorPages({ spec, specFilePath: 'openapi.yaml' })
132
+ .map((page) => page.href)
133
+ .sort()
134
+ ).toEqual(
135
+ Object.values(result.pagesAcc)
136
+ .map((page) => page.href)
137
+ .sort()
138
+ );
139
+ });
140
+
141
+ it('does not merge distinct tags with identical labels', async () => {
142
+ const spec = {
143
+ ...document(),
144
+ webhooks: {},
145
+ tags: [
146
+ { name: 'books', parent: 'store', summary: 'Items' },
147
+ { name: 'products', parent: 'store', summary: 'Items' },
148
+ { name: 'store' },
149
+ ],
150
+ };
151
+ const { nav } = await generate(spec, { writeFiles: false });
152
+ expect(nav).toEqual([
153
+ {
154
+ group: 'store',
155
+ pages: [
156
+ { group: 'Items', pages: ['books/list-books'] },
157
+ { group: 'Items', pages: ['products/list-products'] },
158
+ ],
159
+ },
160
+ ]);
161
+ });
162
+
163
+ it('keeps filenames unique when different tag names produce the same slug', async () => {
164
+ const spec = {
165
+ ...document(),
166
+ webhooks: {},
167
+ tags: [{ name: 'book.s' }, { name: 'books' }],
168
+ paths: {
169
+ '/first': endpoint(['book.s'], 'List'),
170
+ '/second': endpoint(['books'], 'List'),
171
+ },
172
+ };
173
+ const { pagesAcc } = await generate(spec, {
174
+ writeFiles: false,
175
+ outDir: 'api-reference',
176
+ });
177
+ expect(Object.keys(pagesAcc)).toEqual([
178
+ 'api-reference/books/list',
179
+ 'api-reference/books/list-1',
180
+ ]);
181
+ expect(
182
+ generateEditorPages({ spec, specFilePath: 'openapi.yaml' }).map((page) => page.href)
183
+ ).toEqual(['/api-reference/books/list', '/api-reference/books/list-1']);
184
+ });
185
+
186
+ it('omits groups containing only hidden or excluded operations', async () => {
187
+ const spec = document();
188
+ const hiddenSpec = {
189
+ ...spec,
190
+ webhooks: {},
191
+ paths: {
192
+ '/books': { get: { ...spec.paths['/books'].get, 'x-hidden': true } },
193
+ '/products': { get: { ...spec.paths['/products'].get, 'x-excluded': true } },
194
+ },
195
+ };
196
+ const { nav, pagesAcc } = await generate(hiddenSpec, {
197
+ writeFiles: false,
198
+ });
199
+ expect(nav).toEqual([]);
200
+ expect(Object.keys(pagesAcc)).toEqual(['books/list-books']);
201
+ });
202
+
203
+ it('supports webhook-only specs and rejects specs with no operations', async () => {
204
+ const { paths: _paths, ...spec } = document();
205
+ const result = await generate(spec, { writeFiles: false });
206
+ expect(Object.keys(result.pagesAcc)).toEqual(['books/book-created']);
207
+ await expect(generate({ ...spec, webhooks: {} }, { writeFiles: false })).rejects.toThrow(
208
+ 'No paths or webhooks'
209
+ );
210
+ });
211
+
212
+ it('turns kind: badge tags into the page tag pill without grouping by them', async () => {
213
+ const spec = document();
214
+ spec.tags.push(
215
+ { name: 'beta', kind: 'badge', summary: 'Beta' },
216
+ { name: 'internal', kind: 'badge', summary: 'Internal: staff only' }
217
+ );
218
+ spec.paths['/books'].get.tags = ['books', 'beta'];
219
+ spec.paths['/products'].get.tags = ['products', 'internal', 'beta'];
220
+ spec.webhooks.bookCreated.post.tags = ['books', 'beta'];
221
+ const written: Record<string, string> = {};
222
+ const result = await generate(spec, {
223
+ writeFiles: true,
224
+ writeFile: (path, contents) => {
225
+ written[path] = contents;
226
+ },
227
+ outDir: 'api-reference',
228
+ });
229
+ expect(result.pagesAcc['api-reference/books/list-books']?.tag).toBe('Beta');
230
+ expect(result.pagesAcc['api-reference/products/list-products']?.tag).toBe(
231
+ 'Internal: staff only'
232
+ );
233
+ expect(result.pagesAcc['api-reference/books/book-created']?.tag).toBe('Beta');
234
+ expect(written['api-reference/books/list-books.mdx']).toContain('\ntag: Beta\n');
235
+ expect(written['api-reference/products/list-products.mdx']).toContain(
236
+ "\ntag: 'Internal: staff only'\n"
237
+ );
238
+ expect(result.nav[0]).toMatchObject({ group: 'Store' });
239
+ expect(JSON.stringify(result.nav)).not.toContain('Beta');
240
+
241
+ const editorPages = generateEditorPages({
242
+ spec,
243
+ specFilePath: 'openapi.yaml',
244
+ outputDirectory: 'api-reference',
245
+ });
246
+ expect(editorPages.map((page) => page.xMintMetadata?.tag)).toEqual([
247
+ 'Beta',
248
+ 'Internal: staff only',
249
+ 'Beta',
250
+ ]);
251
+ });
252
+
253
+ it('files a page under its navigation tag when a badge tag is listed first', async () => {
254
+ const spec = document();
255
+ spec.tags.push({ name: 'beta', kind: 'badge', summary: 'Beta' });
256
+ spec.paths['/books'].get.tags = ['beta', 'books'];
257
+ spec.webhooks.bookCreated.post.tags = ['beta', 'books'];
258
+ const result = await generate(spec, { writeFiles: false, outDir: 'api-reference' });
259
+ expect(Object.keys(result.pagesAcc)).toEqual(
260
+ expect.arrayContaining(['api-reference/books/list-books', 'api-reference/books/book-created'])
261
+ );
262
+
263
+ const editorPages = generateEditorPages({
264
+ spec,
265
+ specFilePath: 'openapi.yaml',
266
+ outputDirectory: 'api-reference',
267
+ });
268
+ expect(editorPages.map((page) => page.href)).toEqual(
269
+ expect.arrayContaining([
270
+ '/api-reference/books/list-books',
271
+ '/api-reference/books/book-created',
272
+ ])
273
+ );
274
+ });
275
+
276
+ it('lets x-mint metadata tag override a badge tag', async () => {
277
+ const spec = document();
278
+ spec.tags.push({ name: 'beta', kind: 'badge' });
279
+ Object.assign(spec.paths['/books'].get, {
280
+ tags: ['books', 'beta'],
281
+ 'x-mint': { metadata: { tag: 'PREVIEW' } },
282
+ });
283
+ const result = await generate(spec, { writeFiles: false });
284
+ expect(result.pagesAcc['books/list-books']?.tag).toBe('PREVIEW');
285
+ });
286
+ });
@@ -1,4 +1,4 @@
1
- import { type LoadedOverlay } from '@mintlify/common';
1
+ import { createOpenApiBadgeResolver, createOpenApiTagResolver, type LoadedOverlay } from '@mintlify/common';
2
2
  import type { DecoratedNavigationPage, PageMetaTags } from '@mintlify/models';
3
3
  import { XMint } from '@mintlify/validation';
4
4
  import { OpenAPI, OpenAPIV3 } from 'openapi-types';
@@ -35,6 +35,8 @@ export type GenerateOpenApiPagesOptions = {
35
35
  mountPath?: string;
36
36
  overwrite?: boolean;
37
37
  localSchema?: boolean;
38
+ resolveTags?: ReturnType<typeof createOpenApiTagResolver>;
39
+ resolveBadge?: ReturnType<typeof createOpenApiBadgeResolver>;
38
40
  writeFile?: (filename: string, content: string) => void | Promise<void>;
39
41
  overlays?: readonly LoadedOverlay[];
40
42
  };
@@ -45,8 +47,8 @@ export type OpenApiPageGenerationResult<N, DN> = {
45
47
  pagesAcc: Record<string, DecoratedNavigationPage>;
46
48
  isUrl: boolean;
47
49
  };
48
- export declare function processOpenApiPath<N, DN>(path: string, pathItemObject: OpenAPIV3.PathItemObject<OpenApiExtensions>, schema: OpenAPI.Document, nav: N, decoratedNav: DN, writePromises: Promise<void>[], pagesAcc: Record<string, DecoratedNavigationPage>, options: GenerateOpenApiPagesOptions, findNavGroup: (nav: any, groupName?: string) => any): string[];
49
- export declare function processOpenApiWebhook<N, DN>(webhook: string, webhookObject: OpenAPIV3.PathItemObject<OpenApiExtensions>, schema: OpenAPI.Document, nav: N, decoratedNav: DN, writePromises: Promise<void>[], pagesAcc: Record<string, DecoratedNavigationPage>, options: GenerateOpenApiPagesOptions, findNavGroup: (nav: any, groupName?: string) => any): string[];
50
+ export declare function processOpenApiPath<N, DN>(path: string, pathItemObject: OpenAPIV3.PathItemObject<OpenApiExtensions>, schema: OpenAPI.Document, nav: N, decoratedNav: DN, writePromises: Promise<void>[], pagesAcc: Record<string, DecoratedNavigationPage>, options: GenerateOpenApiPagesOptions, findNavGroup: (nav: any, groupName?: string, tagKey?: string) => any): string[];
51
+ export declare function processOpenApiWebhook<N, DN>(webhook: string, webhookObject: OpenAPIV3.PathItemObject<OpenApiExtensions>, schema: OpenAPI.Document, nav: N, decoratedNav: DN, writePromises: Promise<void>[], pagesAcc: Record<string, DecoratedNavigationPage>, options: GenerateOpenApiPagesOptions, findNavGroup: (nav: any, groupName?: string, tagKey?: string) => any): string[];
50
52
  export declare const getXMintGroups: ({ pathObject, operationObject, }: {
51
53
  pathObject: OpenAPIV3.PathItemObject<OpenApiExtensions>;
52
54
  operationObject: OpenAPIV3.OperationObject<OpenApiExtensions> | undefined;
@@ -1,9 +1,9 @@
1
- import { applyOverlays, getOpenApiTitleAndDescription, optionallyAddLeadingSlash, optionallyRemoveLeadingSlash, slugToTitle, isAllowedLocalSchemaUrl, buildOpenApiMetaTag, registerXMintContent, prepareStringToBeValidFilename, generateUniqueFilenameWithoutExtension, getTagDisplayName, mergePageGroups, } from '@mintlify/common';
1
+ import { applyOverlays, getOpenApiTitleAndDescription, optionallyAddLeadingSlash, optionallyRemoveLeadingSlash, slugToTitle, isAllowedLocalSchemaUrl, buildOpenApiMetaTag, registerXMintContent, prepareStringToBeValidFilename, generateUniqueFilenameWithoutExtension, createOpenApiBadgeResolver, createOpenApiTagResolver, findOpenApiNavGroup, isOpenApi32, mergePageGroups, } from '@mintlify/common';
2
+ import { getPathItemOperations } from '@mintlify/validation';
2
3
  import { outputFile } from 'fs-extra';
3
4
  import fse from 'fs-extra';
4
5
  import fs from 'fs/promises';
5
6
  import yaml from 'js-yaml';
6
- import { OpenAPIV3 } from 'openapi-types';
7
7
  import path, { join, parse, posix } from 'path';
8
8
  import { fetchOpenApi } from '../utils/network.js';
9
9
  export const getOpenApiDefinition = async (pathOrDocumentOrUrl, localSchema, overlays) => {
@@ -59,7 +59,8 @@ export const createOpenApiFrontmatterContent = ({ openApiMetaTag, version, depre
59
59
  Object.entries(metadata)
60
60
  .filter(([k]) => !reserved.has(k))
61
61
  .forEach(([key, value]) => {
62
- frontmatter += `\n${key}: ${value}`;
62
+ const serialized = typeof value === 'string' ? yaml.dump(value, { lineWidth: -1 }).trimEnd() : value;
63
+ frontmatter += `\n${key}: ${serialized}`;
63
64
  });
64
65
  }
65
66
  frontmatter += `\n---`;
@@ -81,94 +82,99 @@ findNavGroup) {
81
82
  ? optionallyAddLeadingSlash(options.openApiFilePath)
82
83
  : undefined;
83
84
  const generatedSlugs = [];
84
- Object.values(OpenAPIV3.HttpMethods).forEach((method) => {
85
- if (method in pathItemObject) {
86
- const operation = pathItemObject[method];
87
- if (isExcludedOperation(operation)) {
88
- return;
89
- }
90
- const xMint = operation?.['x-mint'];
91
- const xMintGroups = getXMintGroups({
92
- pathObject: pathItemObject,
93
- operationObject: operation,
94
- });
95
- if (xMint?.href) {
96
- xMint.href = optionallyAddLeadingSlash(xMint.href);
97
- }
98
- const tagName = operation?.tags?.[0];
99
- const groupName = getTagDisplayName(tagName, schema);
100
- let title = prepareStringToBeValidFilename(operation?.summary) ??
101
- `${method}-${prepareStringToBeValidFilename(path)}`;
102
- let folder = prepareStringToBeValidFilename(tagName) ?? '';
103
- let base = posix.join(options.outDir ?? '', folder, title);
104
- if (xMint?.href) {
105
- const slug = optionallyRemoveLeadingSlash(xMint.href);
106
- title = posix.parse(slug).name;
107
- folder = posix.parse(slug).dir;
108
- base = posix.join(options.mountPath ?? '', folder, title);
109
- }
110
- const navGroup = findNavGroup(nav, groupName);
111
- const decoratedNavGroup = findNavGroup(decoratedNav, groupName);
112
- const filenameWithoutExtension = generateUniqueFilenameWithoutExtension(navGroup, base);
113
- const openapiMetaTag = buildOpenApiMetaTag({
114
- filePath: openApiFilePathFromRoot,
115
- method,
116
- path,
117
- });
118
- const { title: titleTag, description } = getOpenApiTitleAndDescription([
119
- {
120
- filename: options.openApiFilePath
121
- ? parse(options.openApiFilePath).name
122
- : 'filler-filename',
123
- spec: schema,
124
- originalFileLocation: openApiFilePathFromRoot,
125
- },
126
- ], openapiMetaTag);
127
- let xMintMetadata = xMint?.metadata;
128
- if (xMintGroups.length > 0) {
129
- xMintMetadata = {
130
- ...xMintMetadata,
131
- groups: mergePageGroups(xMintMetadata?.groups, xMintGroups),
132
- };
133
- }
134
- const slugTitle = slugToTitle(filenameWithoutExtension);
135
- const page = {
136
- title: titleTag ?? slugTitle,
137
- description,
138
- deprecated: operation?.deprecated,
139
- version: options.version,
140
- // When a file-path spec has x-mint.href overriding the path, use the slug-derived
141
- // title for the sidebar so the nav shows the user-chosen name rather than the
142
- // OpenAPI summary. Only for openApiFilePath specs to avoid changing URL-based behavior.
143
- ...(xMint?.href && titleTag && options.openApiFilePath ? { sidebarTitle: slugTitle } : {}),
85
+ const resolveTags = options.resolveTags ?? createOpenApiTagResolver(schema);
86
+ const resolveBadge = options.resolveBadge ?? createOpenApiBadgeResolver(schema);
87
+ const nestedTags = isOpenApi32(schema);
88
+ getPathItemOperations(pathItemObject).forEach(([method, operation]) => {
89
+ if (isExcludedOperation(operation)) {
90
+ return;
91
+ }
92
+ const xMint = operation?.['x-mint'];
93
+ const xMintGroups = getXMintGroups({
94
+ pathObject: pathItemObject,
95
+ operationObject: operation,
96
+ });
97
+ if (xMint?.href) {
98
+ xMint.href = optionallyAddLeadingSlash(xMint.href);
99
+ }
100
+ const tagPath = resolveTags(operation?.tags);
101
+ const tagName = tagPath.at(-1)?.name;
102
+ let title = prepareStringToBeValidFilename(operation?.summary) ??
103
+ `${method.toLowerCase()}-${prepareStringToBeValidFilename(path)}`;
104
+ let folder = prepareStringToBeValidFilename(tagName) ?? '';
105
+ let base = posix.join(options.outDir ?? '', folder, title);
106
+ if (xMint?.href) {
107
+ const slug = optionallyRemoveLeadingSlash(xMint.href);
108
+ title = posix.parse(slug).name;
109
+ folder = posix.parse(slug).dir;
110
+ base = posix.join(options.mountPath ?? '', folder, title);
111
+ }
112
+ const hidden = isHiddenOperation(operation);
113
+ const navGroup = hidden && nestedTags ? [] : findOpenApiNavGroup(nav, tagPath, findNavGroup);
114
+ const decoratedNavGroup = hidden && nestedTags ? [] : findOpenApiNavGroup(decoratedNav, tagPath, findNavGroup);
115
+ const filenameWithoutExtension = generateUniqueFilenameWithoutExtension(nestedTags ? pagesAcc : navGroup, base);
116
+ const openapiMetaTag = buildOpenApiMetaTag({
117
+ filePath: openApiFilePathFromRoot,
118
+ method,
119
+ path,
120
+ });
121
+ const { title: titleTag, description } = getOpenApiTitleAndDescription([
122
+ {
123
+ filename: options.openApiFilePath
124
+ ? parse(options.openApiFilePath).name
125
+ : 'filler-filename',
126
+ spec: schema,
127
+ originalFileLocation: openApiFilePathFromRoot,
128
+ },
129
+ ], openapiMetaTag);
130
+ let xMintMetadata = xMint?.metadata;
131
+ if (xMintGroups.length > 0) {
132
+ xMintMetadata = {
144
133
  ...xMintMetadata,
145
- openapi: openapiMetaTag,
146
- href: posix.resolve('/', filenameWithoutExtension),
134
+ groups: mergePageGroups(xMintMetadata?.groups, xMintGroups),
147
135
  };
148
- if (!isHiddenOperation(operation)) {
149
- navGroup.push(filenameWithoutExtension);
150
- decoratedNavGroup.push(page);
151
- }
152
- pagesAcc[filenameWithoutExtension] = page;
153
- generatedSlugs.push(filenameWithoutExtension);
154
- if (!options.writeFiles) {
155
- registerXMintContent(filenameWithoutExtension, xMint?.content);
156
- }
157
- const targetPath = options.outDirBasePath
158
- ? join(options.outDirBasePath, `${filenameWithoutExtension}.mdx`)
159
- : `${filenameWithoutExtension}.mdx`;
160
- if (options.writeFiles && (!fse.pathExistsSync(targetPath) || options.overwrite)) {
161
- const input = {
162
- openApiMetaTag: openapiMetaTag,
163
- version: options.version,
164
- deprecated: operation?.deprecated,
165
- metadata: xMintMetadata,
166
- extraContent: xMint?.content,
167
- };
168
- writePromises.push(options.writeFile
169
- ? Promise.resolve(options.writeFile(targetPath, createOpenApiFrontmatterContent(input)))
170
- : createOpenApiFrontmatter({ filename: targetPath, ...input }));
171
- }
136
+ }
137
+ const badge = resolveBadge(operation?.tags);
138
+ if (badge !== undefined && xMintMetadata?.tag === undefined) {
139
+ xMintMetadata = { ...xMintMetadata, tag: badge };
140
+ }
141
+ const slugTitle = slugToTitle(filenameWithoutExtension);
142
+ const page = {
143
+ title: titleTag ?? slugTitle,
144
+ description,
145
+ deprecated: operation?.deprecated,
146
+ version: options.version,
147
+ // When a file-path spec has x-mint.href overriding the path, use the slug-derived
148
+ // title for the sidebar so the nav shows the user-chosen name rather than the
149
+ // OpenAPI summary. Only for openApiFilePath specs to avoid changing URL-based behavior.
150
+ ...(xMint?.href && titleTag && options.openApiFilePath ? { sidebarTitle: slugTitle } : {}),
151
+ ...xMintMetadata,
152
+ openapi: openapiMetaTag,
153
+ href: posix.resolve('/', filenameWithoutExtension),
154
+ };
155
+ if (!isHiddenOperation(operation)) {
156
+ navGroup.push(filenameWithoutExtension);
157
+ decoratedNavGroup.push(page);
158
+ }
159
+ pagesAcc[filenameWithoutExtension] = page;
160
+ generatedSlugs.push(filenameWithoutExtension);
161
+ if (!options.writeFiles) {
162
+ registerXMintContent(filenameWithoutExtension, xMint?.content);
163
+ }
164
+ const targetPath = options.outDirBasePath
165
+ ? join(options.outDirBasePath, `${filenameWithoutExtension}.mdx`)
166
+ : `${filenameWithoutExtension}.mdx`;
167
+ if (options.writeFiles && (!fse.pathExistsSync(targetPath) || options.overwrite)) {
168
+ const input = {
169
+ openApiMetaTag: openapiMetaTag,
170
+ version: options.version,
171
+ deprecated: operation?.deprecated,
172
+ metadata: xMintMetadata,
173
+ extraContent: xMint?.content,
174
+ };
175
+ writePromises.push(options.writeFile
176
+ ? Promise.resolve(options.writeFile(targetPath, createOpenApiFrontmatterContent(input)))
177
+ : createOpenApiFrontmatter({ filename: targetPath, ...input }));
172
178
  }
173
179
  });
174
180
  return generatedSlugs;
@@ -180,69 +186,74 @@ findNavGroup) {
180
186
  ? optionallyAddLeadingSlash(options.openApiFilePath)
181
187
  : undefined;
182
188
  const generatedSlugs = [];
183
- Object.values(OpenAPIV3.HttpMethods).forEach((method) => {
184
- if (method in webhookObject) {
185
- const operation = webhookObject[method];
186
- if (isExcludedOperation(operation)) {
187
- return;
188
- }
189
- const xMint = operation?.['x-mint'];
190
- if (xMint?.href) {
191
- xMint.href = optionallyAddLeadingSlash(xMint.href);
192
- }
193
- const tagName = operation?.tags?.[0];
194
- const groupName = getTagDisplayName(tagName, schema);
195
- let title = prepareStringToBeValidFilename(operation?.summary) ??
196
- `${prepareStringToBeValidFilename(webhook)}`;
197
- let folder = prepareStringToBeValidFilename(tagName) ?? '';
198
- let base = posix.join(options.outDir ?? '', folder, title);
199
- if (xMint?.href) {
200
- const slug = optionallyRemoveLeadingSlash(xMint.href);
201
- title = posix.parse(slug).name;
202
- folder = posix.parse(slug).dir;
203
- base = posix.join(options.mountPath ?? '', folder, title);
204
- }
205
- const navGroup = findNavGroup(nav, groupName);
206
- const decoratedNavGroup = findNavGroup(decoratedNav, groupName);
207
- const filenameWithoutExtension = generateUniqueFilenameWithoutExtension(navGroup, base);
208
- const openapiMetaTag = buildOpenApiMetaTag({
209
- filePath: openApiFilePathFromRoot,
210
- method: 'webhook',
211
- path: webhook,
212
- });
213
- const page = {
214
- title: slugToTitle(filenameWithoutExtension),
215
- description: operation?.description,
189
+ const resolveTags = options.resolveTags ?? createOpenApiTagResolver(schema);
190
+ const resolveBadge = options.resolveBadge ?? createOpenApiBadgeResolver(schema);
191
+ const nestedTags = isOpenApi32(schema);
192
+ getPathItemOperations(webhookObject).forEach(([, operation]) => {
193
+ if (isExcludedOperation(operation)) {
194
+ return;
195
+ }
196
+ const xMint = operation?.['x-mint'];
197
+ if (xMint?.href) {
198
+ xMint.href = optionallyAddLeadingSlash(xMint.href);
199
+ }
200
+ const tagPath = resolveTags(operation?.tags);
201
+ const tagName = tagPath.at(-1)?.name;
202
+ let title = prepareStringToBeValidFilename(operation?.summary) ??
203
+ `${prepareStringToBeValidFilename(webhook)}`;
204
+ let folder = prepareStringToBeValidFilename(tagName) ?? '';
205
+ let base = posix.join(options.outDir ?? '', folder, title);
206
+ if (xMint?.href) {
207
+ const slug = optionallyRemoveLeadingSlash(xMint.href);
208
+ title = posix.parse(slug).name;
209
+ folder = posix.parse(slug).dir;
210
+ base = posix.join(options.mountPath ?? '', folder, title);
211
+ }
212
+ const hidden = isHiddenOperation(operation);
213
+ const navGroup = hidden && nestedTags ? [] : findOpenApiNavGroup(nav, tagPath, findNavGroup);
214
+ const decoratedNavGroup = hidden && nestedTags ? [] : findOpenApiNavGroup(decoratedNav, tagPath, findNavGroup);
215
+ const filenameWithoutExtension = generateUniqueFilenameWithoutExtension(nestedTags ? pagesAcc : navGroup, base);
216
+ const openapiMetaTag = buildOpenApiMetaTag({
217
+ filePath: openApiFilePathFromRoot,
218
+ method: 'webhook',
219
+ path: webhook,
220
+ });
221
+ const badge = resolveBadge(operation?.tags);
222
+ const xMintMetadata = badge !== undefined && xMint?.metadata?.tag === undefined
223
+ ? { ...xMint?.metadata, tag: badge }
224
+ : xMint?.metadata;
225
+ const page = {
226
+ title: slugToTitle(filenameWithoutExtension),
227
+ description: operation?.description,
228
+ version: options.version,
229
+ deprecated: operation?.deprecated,
230
+ ...xMintMetadata,
231
+ openapi: openapiMetaTag,
232
+ href: posix.resolve('/', filenameWithoutExtension),
233
+ };
234
+ if (!isHiddenOperation(operation)) {
235
+ navGroup.push(filenameWithoutExtension);
236
+ decoratedNavGroup.push(page);
237
+ }
238
+ pagesAcc[filenameWithoutExtension] = page;
239
+ generatedSlugs.push(filenameWithoutExtension);
240
+ if (!options.writeFiles) {
241
+ registerXMintContent(filenameWithoutExtension, xMint?.content);
242
+ }
243
+ const targetPath = options.outDirBasePath
244
+ ? join(options.outDirBasePath, `${filenameWithoutExtension}.mdx`)
245
+ : `${filenameWithoutExtension}.mdx`;
246
+ if (options.writeFiles && (!fse.pathExistsSync(targetPath) || options.overwrite)) {
247
+ const input = {
248
+ openApiMetaTag: openapiMetaTag,
216
249
  version: options.version,
217
250
  deprecated: operation?.deprecated,
218
- ...xMint?.metadata,
219
- openapi: openapiMetaTag,
220
- href: posix.resolve('/', filenameWithoutExtension),
251
+ metadata: xMintMetadata,
252
+ extraContent: xMint?.content,
221
253
  };
222
- if (!isHiddenOperation(operation)) {
223
- navGroup.push(filenameWithoutExtension);
224
- decoratedNavGroup.push(page);
225
- }
226
- pagesAcc[filenameWithoutExtension] = page;
227
- generatedSlugs.push(filenameWithoutExtension);
228
- if (!options.writeFiles) {
229
- registerXMintContent(filenameWithoutExtension, xMint?.content);
230
- }
231
- const targetPath = options.outDirBasePath
232
- ? join(options.outDirBasePath, `${filenameWithoutExtension}.mdx`)
233
- : `${filenameWithoutExtension}.mdx`;
234
- if (options.writeFiles && (!fse.pathExistsSync(targetPath) || options.overwrite)) {
235
- const input = {
236
- openApiMetaTag: openapiMetaTag,
237
- version: options.version,
238
- deprecated: operation?.deprecated,
239
- metadata: xMint?.metadata,
240
- extraContent: xMint?.content,
241
- };
242
- writePromises.push(options.writeFile
243
- ? Promise.resolve(options.writeFile(targetPath, createOpenApiFrontmatterContent(input)))
244
- : createOpenApiFrontmatter({ filename: targetPath, ...input }));
245
- }
254
+ writePromises.push(options.writeFile
255
+ ? Promise.resolve(options.writeFile(targetPath, createOpenApiFrontmatterContent(input)))
256
+ : createOpenApiFrontmatter({ filename: targetPath, ...input }));
246
257
  }
247
258
  });
248
259
  return generatedSlugs;