docusaurus-plugin-openapi-docs 0.0.0-351 → 0.0.0-352

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/lib/index.js CHANGED
@@ -25,7 +25,7 @@ function pluginOpenAPI(context, options) {
25
25
  const contentPath = path_1.default.resolve(siteDir, specPath);
26
26
  try {
27
27
  const openapiFiles = await (0, openapi_1.readOpenapiFiles)(contentPath, {});
28
- const loadedApi = await (0, openapi_1.processOpenapiFiles)(openapiFiles);
28
+ const [loadedApi, tags] = await (0, openapi_1.processOpenapiFiles)(openapiFiles);
29
29
  if (!fs_1.default.existsSync(outputDir)) {
30
30
  try {
31
31
  fs_1.default.mkdirSync(outputDir, { recursive: true });
@@ -38,7 +38,7 @@ function pluginOpenAPI(context, options) {
38
38
  // TODO: figure out better way to set default
39
39
  if (Object.keys(sidebarOptions !== null && sidebarOptions !== void 0 ? sidebarOptions : {}).length > 0) {
40
40
  const sidebarSlice = (0, sidebars_1.default)(sidebarOptions, // TODO: find a better way to handle null
41
- options, loadedApi);
41
+ options, loadedApi, tags);
42
42
  const sidebarSliceTemplate = template
43
43
  ? fs_1.default.readFileSync(template).toString()
44
44
  : `module.exports = {{{slice}}};`;
@@ -59,7 +59,12 @@ function pluginOpenAPI(context, options) {
59
59
  ? fs_1.default.readFileSync(template).toString()
60
60
  : `---
61
61
  id: {{{id}}}
62
+ {{^api}}
63
+ sidebar_label: Introduction
64
+ {{/api}}
65
+ {{#api}}
62
66
  sidebar_label: {{{title}}}
67
+ {{/api}}
63
68
  {{^api}}
64
69
  sidebar_position: 0
65
70
  {{/api}}
@@ -76,6 +81,23 @@ sidebar_class_name: "{{{api.method}}} api-method"
76
81
  ---
77
82
 
78
83
  {{{markdown}}}
84
+ `;
85
+ const infoMdTemplate = template
86
+ ? fs_1.default.readFileSync(template).toString()
87
+ : `---
88
+ id: {{{id}}}
89
+ sidebar_label: {{{title}}}
90
+ hide_title: true
91
+ ---
92
+
93
+ {{{markdown}}}
94
+
95
+ \`\`\`mdx-code-block
96
+ import DocCardList from '@theme/DocCardList';
97
+ import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
98
+
99
+ <DocCardList items={useCurrentSidebarCategory().items}/>
100
+ \`\`\`
79
101
  `;
80
102
  loadedApi.map(async (item) => {
81
103
  const markdown = item.type === "api" ? (0, markdown_1.createApiPageMD)(item) : (0, markdown_1.createInfoPageMD)(item);
@@ -84,6 +106,7 @@ sidebar_class_name: "{{{api.method}}} api-method"
84
106
  item.json = JSON.stringify(item.api);
85
107
  }
86
108
  const view = (0, mustache_1.render)(mdTemplate, item);
109
+ const utils = (0, mustache_1.render)(infoMdTemplate, item);
87
110
  if (item.type === "api") {
88
111
  if (!fs_1.default.existsSync(`${outputDir}/${item.id}.api.mdx`)) {
89
112
  try {
@@ -97,13 +120,15 @@ sidebar_class_name: "{{{api.method}}} api-method"
97
120
  }
98
121
  // TODO: determine if we actually want/need this
99
122
  if (item.type === "info") {
100
- if (!fs_1.default.existsSync(`${outputDir}/index.api.mdx`)) {
123
+ if (!fs_1.default.existsSync(`${outputDir}/${item.id}.info.mdx`)) {
101
124
  try {
102
- fs_1.default.writeFileSync(`${outputDir}/index.api.mdx`, view, "utf8");
103
- console.log(chalk_1.default.green(`Successfully created "${outputDir}/index.api.mdx"`));
125
+ (sidebarOptions === null || sidebarOptions === void 0 ? void 0 : sidebarOptions.useInfoAsCategoryLink)
126
+ ? fs_1.default.writeFileSync(`${outputDir}/${item.id}.info.mdx`, utils, "utf8")
127
+ : fs_1.default.writeFileSync(`${outputDir}/${item.id}.info.mdx`, view, "utf8");
128
+ console.log(chalk_1.default.green(`Successfully created "${outputDir}/${item.id}.info.mdx"`));
104
129
  }
105
130
  catch (err) {
106
- console.error(chalk_1.default.red(`Failed to write "${outputDir}/index.api.mdx"`), chalk_1.default.yellow(err));
131
+ console.error(chalk_1.default.red(`Failed to write "${outputDir}/${item.id}.info.mdx"`), chalk_1.default.yellow(err));
107
132
  }
108
133
  }
109
134
  }
@@ -119,7 +144,7 @@ sidebar_class_name: "{{{api.method}}} api-method"
119
144
  async function cleanApiDocs(options) {
120
145
  const { outputDir } = options;
121
146
  const apiDir = path_1.default.join(siteDir, outputDir);
122
- const apiMdxFiles = await (0, utils_1.Globby)(["*.api.mdx"], {
147
+ const apiMdxFiles = await (0, utils_1.Globby)(["*.api.mdx", "*.info.mdx"], {
123
148
  cwd: path_1.default.resolve(apiDir),
124
149
  });
125
150
  const sidebarFile = await (0, utils_1.Globby)(["sidebar.js"], {
@@ -1,11 +1,12 @@
1
1
  import { ApiMetadata } from "../types";
2
- import { OpenApiObjectWithRef } from "./types";
2
+ import { OpenApiObjectWithRef, TagObject } from "./types";
3
3
  interface OpenApiFiles {
4
4
  source: string;
5
5
  sourceDirName: string;
6
6
  data: OpenApiObjectWithRef;
7
7
  }
8
8
  export declare function readOpenapiFiles(openapiPath: string, _options: {}): Promise<OpenApiFiles[]>;
9
- export declare function processOpenapiFiles(files: OpenApiFiles[]): Promise<ApiMetadata[]>;
10
- export declare function processOpenapiFile(openapiDataWithRefs: OpenApiObjectWithRef): Promise<ApiMetadata[]>;
9
+ export declare function processOpenapiFiles(files: OpenApiFiles[]): Promise<[ApiMetadata[], TagObject[]]>;
10
+ export declare function processOpenapiFile(openapiDataWithRefs: OpenApiObjectWithRef): Promise<[ApiMetadata[], TagObject[]]>;
11
+ export declare function getTagDisplayName(tagName: string, tags: TagObject[]): string;
11
12
  export {};
@@ -9,7 +9,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
9
9
  return (mod && mod.__esModule) ? mod : { "default": mod };
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.processOpenapiFile = exports.processOpenapiFiles = exports.readOpenapiFiles = void 0;
12
+ exports.getTagDisplayName = exports.processOpenapiFile = exports.processOpenapiFiles = exports.readOpenapiFiles = void 0;
13
13
  const path_1 = __importDefault(require("path"));
14
14
  const utils_1 = require("@docusaurus/utils");
15
15
  const openapi_to_postmanv2_1 = __importDefault(require("@paloaltonetworks/openapi-to-postmanv2"));
@@ -64,22 +64,24 @@ async function createPostmanCollection(openapiData) {
64
64
  return await jsonToCollection(data);
65
65
  }
66
66
  function createItems(openapiData) {
67
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
67
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
68
68
  // TODO: Find a better way to handle this
69
69
  let items = [];
70
70
  // Only create an info page if we have a description.
71
71
  if (openapiData.info.description) {
72
+ const infoId = (0, lodash_1.kebabCase)(openapiData.info.title);
72
73
  const infoPage = {
73
74
  type: "info",
74
- id: "introduction",
75
- unversionedId: "introduction",
76
- title: "Introduction",
75
+ id: infoId,
76
+ unversionedId: infoId,
77
+ title: openapiData.info.title,
77
78
  description: openapiData.info.description,
78
- slug: "/introduction",
79
+ slug: "/" + infoId,
79
80
  frontMatter: {},
80
81
  info: {
81
82
  ...openapiData.info,
82
- title: (_a = openapiData.info.title) !== null && _a !== void 0 ? _a : "Introduction",
83
+ tags: (_a = openapiData.tags) === null || _a === void 0 ? void 0 : _a.map((tagName) => { var _a; return getTagDisplayName(tagName.name, (_a = openapiData.tags) !== null && _a !== void 0 ? _a : []); }),
84
+ title: (_b = openapiData.info.title) !== null && _b !== void 0 ? _b : "Introduction",
83
85
  },
84
86
  };
85
87
  items.push(infoPage);
@@ -87,16 +89,16 @@ function createItems(openapiData) {
87
89
  for (let [path, pathObject] of Object.entries(openapiData.paths)) {
88
90
  const { $ref, description, parameters, servers, summary, ...rest } = pathObject;
89
91
  for (let [method, operationObject] of Object.entries({ ...rest })) {
90
- const title = (_c = (_b = operationObject.summary) !== null && _b !== void 0 ? _b : operationObject.operationId) !== null && _c !== void 0 ? _c : "Missing summary";
92
+ const title = (_d = (_c = operationObject.summary) !== null && _c !== void 0 ? _c : operationObject.operationId) !== null && _d !== void 0 ? _d : "Missing summary";
91
93
  if (operationObject.description === undefined) {
92
94
  operationObject.description =
93
- (_e = (_d = operationObject.summary) !== null && _d !== void 0 ? _d : operationObject.operationId) !== null && _e !== void 0 ? _e : "";
95
+ (_f = (_e = operationObject.summary) !== null && _e !== void 0 ? _e : operationObject.operationId) !== null && _f !== void 0 ? _f : "";
94
96
  }
95
97
  const baseId = (0, lodash_1.kebabCase)(title);
96
- const servers = (_g = (_f = operationObject.servers) !== null && _f !== void 0 ? _f : pathObject.servers) !== null && _g !== void 0 ? _g : openapiData.servers;
97
- const security = (_h = operationObject.security) !== null && _h !== void 0 ? _h : openapiData.security;
98
+ const servers = (_h = (_g = operationObject.servers) !== null && _g !== void 0 ? _g : pathObject.servers) !== null && _h !== void 0 ? _h : openapiData.servers;
99
+ const security = (_j = operationObject.security) !== null && _j !== void 0 ? _j : openapiData.security;
98
100
  // Add security schemes so we know how to handle security.
99
- const securitySchemes = (_j = openapiData.components) === null || _j === void 0 ? void 0 : _j.securitySchemes;
101
+ const securitySchemes = (_k = openapiData.components) === null || _k === void 0 ? void 0 : _k.securitySchemes;
100
102
  // Make sure schemes are lowercase. See: https://github.com/cloud-annotations/docusaurus-plugin-openapi/issues/79
101
103
  if (securitySchemes) {
102
104
  for (let securityScheme of Object.values(securitySchemes)) {
@@ -106,7 +108,7 @@ function createItems(openapiData) {
106
108
  }
107
109
  }
108
110
  let jsonRequestBodyExample;
109
- const body = (_l = (_k = operationObject.requestBody) === null || _k === void 0 ? void 0 : _k.content) === null || _l === void 0 ? void 0 : _l["application/json"];
111
+ const body = (_m = (_l = operationObject.requestBody) === null || _l === void 0 ? void 0 : _l.content) === null || _m === void 0 ? void 0 : _m["application/json"];
110
112
  if (body === null || body === void 0 ? void 0 : body.schema) {
111
113
  jsonRequestBodyExample = (0, createExample_1.sampleFromSchema)(body.schema);
112
114
  }
@@ -122,7 +124,7 @@ function createItems(openapiData) {
122
124
  frontMatter: {},
123
125
  api: {
124
126
  ...defaults,
125
- tags: (_m = operationObject.tags) === null || _m === void 0 ? void 0 : _m.map((tagName) => { var _a; return getTagDisplayName(tagName, (_a = openapiData.tags) !== null && _a !== void 0 ? _a : []); }),
127
+ tags: (_o = operationObject.tags) === null || _o === void 0 ? void 0 : _o.map((tagName) => { var _a; return getTagDisplayName(tagName, (_a = openapiData.tags) !== null && _a !== void 0 ? _a : []); }),
126
128
  method,
127
129
  path,
128
130
  servers,
@@ -192,14 +194,23 @@ async function readOpenapiFiles(openapiPath, _options) {
192
194
  exports.readOpenapiFiles = readOpenapiFiles;
193
195
  async function processOpenapiFiles(files) {
194
196
  const promises = files.map(async (file) => {
195
- const items = await processOpenapiFile(file.data);
196
- return items.map((item) => ({
197
+ const processedFile = await processOpenapiFile(file.data);
198
+ const itemsObjectsArray = processedFile[0].map((item) => ({
197
199
  ...item,
198
200
  }));
201
+ const tags = processedFile[1];
202
+ return [itemsObjectsArray, tags];
199
203
  });
200
204
  const metadata = await Promise.all(promises);
201
- const items = metadata.flat();
202
- return items;
205
+ const items = metadata
206
+ .map(function (x) {
207
+ return x[0];
208
+ })
209
+ .flat();
210
+ const tags = metadata.map(function (x) {
211
+ return x[1];
212
+ });
213
+ return [items, tags];
203
214
  }
204
215
  exports.processOpenapiFiles = processOpenapiFiles;
205
216
  async function processOpenapiFile(openapiDataWithRefs) {
@@ -207,7 +218,11 @@ async function processOpenapiFile(openapiDataWithRefs) {
207
218
  const postmanCollection = await createPostmanCollection(openapiData);
208
219
  const items = createItems(openapiData);
209
220
  bindCollectionToApiItems(items, postmanCollection);
210
- return items;
221
+ let tags = [];
222
+ if (openapiData.tags !== undefined) {
223
+ tags = openapiData.tags;
224
+ }
225
+ return [items, tags];
211
226
  }
212
227
  exports.processOpenapiFile = processOpenapiFile;
213
228
  // order for picking items as a display name of tags
@@ -229,3 +244,4 @@ function getTagDisplayName(tagName, tags) {
229
244
  // always default to the tagName
230
245
  return tagName;
231
246
  }
247
+ exports.getTagDisplayName = getTagDisplayName;
@@ -29,6 +29,7 @@ export interface InfoObject {
29
29
  contact?: ContactObject;
30
30
  license?: LicenseObject;
31
31
  version: string;
32
+ tags?: String[];
32
33
  }
33
34
  export interface ContactObject {
34
35
  name?: string;
@@ -237,7 +238,7 @@ export interface LinkObject {
237
238
  export declare type HeaderObject = Omit<ParameterObject, "name" | "in">;
238
239
  export declare type HeaderObjectWithRef = Omit<ParameterObjectWithRef, "name" | "in">;
239
240
  export interface TagObject {
240
- name: string;
241
+ name?: string;
241
242
  description?: string;
242
243
  externalDocs?: ExternalDocumentationObject;
243
244
  "x-displayName"?: string;
@@ -1,3 +1,4 @@
1
1
  import { ProcessedSidebar } from "@docusaurus/plugin-content-docs/src/sidebars/types";
2
+ import { TagObject } from "../openapi/types";
2
3
  import type { SidebarOptions, APIOptions, ApiMetadata } from "../types";
3
- export default function generateSidebarSlice(sidebarOptions: SidebarOptions, options: APIOptions, api: ApiMetadata[]): ProcessedSidebar;
4
+ export default function generateSidebarSlice(sidebarOptions: SidebarOptions, options: APIOptions, api: ApiMetadata[], tags: TagObject[]): ProcessedSidebar;
@@ -10,25 +10,30 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const clsx_1 = __importDefault(require("clsx"));
13
+ const lodash_1 = require("lodash");
13
14
  const uniq_1 = __importDefault(require("lodash/uniq"));
14
15
  function isApiItem(item) {
15
16
  return item.type === "api";
16
17
  }
17
- function groupByTags(items, sidebarOptions, options) {
18
- // TODO: Figure out how to handle these
19
- // const intros = items.filter(isInfoItem).map((item) => {
20
- // return {
21
- // type: "link" as const,
22
- // label: item.title,
23
- // href: item.permalink,
24
- // docId: item.id,
25
- // };
26
- // });
18
+ function isInfoItem(item) {
19
+ return item.type === "info";
20
+ }
21
+ function groupByTags(items, sidebarOptions, options, tags) {
27
22
  const { outputDir } = options;
28
- const { sidebarCollapsed, sidebarCollapsible, customProps } = sidebarOptions;
23
+ const { sidebarCollapsed, sidebarCollapsible, customProps, categoryLinkSource, } = sidebarOptions;
24
+ const linkSource = categoryLinkSource !== null && categoryLinkSource !== void 0 ? categoryLinkSource : "tag";
29
25
  const apiItems = items.filter(isApiItem);
26
+ const infoItems = items.filter(isInfoItem);
27
+ const intros = infoItems.map((item) => {
28
+ return {
29
+ id: item.id,
30
+ title: item.title,
31
+ description: item.description,
32
+ tags: item.info.tags,
33
+ };
34
+ });
30
35
  // TODO: make sure we only take the first tag
31
- const tags = (0, uniq_1.default)(apiItems
36
+ const tags_ = (0, uniq_1.default)(apiItems
32
37
  .flatMap((item) => item.api.tags)
33
38
  .filter((item) => !!item));
34
39
  // TODO: optimize this or make it a function
@@ -51,11 +56,47 @@ function groupByTags(items, sidebarOptions, options) {
51
56
  }, item.api.method),
52
57
  };
53
58
  }
54
- const tagged = tags
59
+ let introDoc = undefined;
60
+ if (linkSource === "info") {
61
+ const infoItem = infoItems[0];
62
+ const id = infoItem.id;
63
+ introDoc = {
64
+ type: "doc",
65
+ id: `${basePath}/${id}`,
66
+ };
67
+ }
68
+ const tagged = tags_
55
69
  .map((tag) => {
70
+ // TODO: should we also use the info.title as generated-index title?
71
+ const infoObject = intros.find((i) => i.tags.includes(tag));
72
+ const tagObject = tags.flat().find((t) => {
73
+ var _a;
74
+ return (_a = (tag === t.name || tag === t["x-displayName"])) !== null && _a !== void 0 ? _a : {
75
+ name: tag,
76
+ description: `${tag} Index`,
77
+ };
78
+ });
79
+ // TODO: perhaps move all this into a getLinkConfig() function
80
+ let linkConfig = undefined;
81
+ if (infoObject !== undefined && linkSource === "info") {
82
+ linkConfig = {
83
+ type: "doc",
84
+ id: `${basePath}/${infoObject.id}`,
85
+ };
86
+ }
87
+ if (tagObject !== undefined && linkSource === "tag") {
88
+ const linkDescription = tagObject === null || tagObject === void 0 ? void 0 : tagObject.description;
89
+ linkConfig = {
90
+ type: "generated-index",
91
+ title: tag,
92
+ description: linkDescription,
93
+ slug: "/category/" + (0, lodash_1.kebabCase)(tag),
94
+ };
95
+ }
56
96
  return {
57
97
  type: "category",
58
98
  label: tag,
99
+ link: linkConfig,
59
100
  collapsible: sidebarCollapsible,
60
101
  collapsed: sidebarCollapsed,
61
102
  items: apiItems
@@ -64,25 +105,28 @@ function groupByTags(items, sidebarOptions, options) {
64
105
  };
65
106
  })
66
107
  .filter((item) => item.items.length > 0); // Filter out any categories with no items.
108
+ // TODO: determine how we want to handle these
67
109
  // const untagged = [
68
- // // TODO: determine if needed and how
69
110
  // {
70
111
  // type: "category" as const,
71
112
  // label: "UNTAGGED",
72
- // // collapsible: options.sidebarCollapsible, TODO: add option
73
- // // collapsed: options.sidebarCollapsed, TODO: add option
113
+ // collapsible: sidebarCollapsible,
114
+ // collapsed: sidebarCollapsed,
74
115
  // items: apiItems
75
- // //@ts-ignore
76
116
  // .filter(({ api }) => api.tags === undefined || api.tags.length === 0)
77
117
  // .map(createDocItem),
78
118
  // },
79
119
  // ];
120
+ // Shift intro doc to top of sidebar
121
+ if (introDoc && linkSource === "info") {
122
+ tagged.unshift(introDoc);
123
+ }
80
124
  return [...tagged];
81
125
  }
82
- function generateSidebarSlice(sidebarOptions, options, api) {
126
+ function generateSidebarSlice(sidebarOptions, options, api, tags) {
83
127
  let sidebarSlice = [];
84
128
  if (sidebarOptions.groupPathsBy === "tags") {
85
- sidebarSlice = groupByTags(api, sidebarOptions, options);
129
+ sidebarSlice = groupByTags(api, sidebarOptions, options, tags);
86
130
  }
87
131
  return sidebarSlice;
88
132
  }
package/lib/types.d.ts CHANGED
@@ -60,6 +60,8 @@ export interface ApiNavLink {
60
60
  }
61
61
  export interface SidebarOptions {
62
62
  groupPathsBy?: string;
63
+ useInfoAsCategoryLink?: boolean;
64
+ categoryLinkSource?: string;
63
65
  customProps?: {
64
66
  [key: string]: unknown;
65
67
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "docusaurus-plugin-openapi-docs",
3
3
  "description": "OpenAPI plugin for Docusaurus.",
4
- "version": "0.0.0-351",
4
+ "version": "0.0.0-352",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "openapi",
@@ -60,5 +60,5 @@
60
60
  "engines": {
61
61
  "node": ">=14"
62
62
  },
63
- "gitHead": "475483bcb20b232da5c12ee23c48855dd32ba0d8"
63
+ "gitHead": "dc9b85400c4cf3f675641234bf38c398b5ba9dd0"
64
64
  }
package/src/index.ts CHANGED
@@ -32,8 +32,7 @@ export default function pluginOpenAPI(
32
32
 
33
33
  try {
34
34
  const openapiFiles = await readOpenapiFiles(contentPath, {});
35
- const loadedApi = await processOpenapiFiles(openapiFiles);
36
-
35
+ const [loadedApi, tags] = await processOpenapiFiles(openapiFiles);
37
36
  if (!fs.existsSync(outputDir)) {
38
37
  try {
39
38
  fs.mkdirSync(outputDir, { recursive: true });
@@ -51,7 +50,8 @@ export default function pluginOpenAPI(
51
50
  const sidebarSlice = generateSidebarSlice(
52
51
  sidebarOptions!, // TODO: find a better way to handle null
53
52
  options,
54
- loadedApi
53
+ loadedApi,
54
+ tags
55
55
  );
56
56
 
57
57
  const sidebarSliceTemplate = template
@@ -81,7 +81,12 @@ export default function pluginOpenAPI(
81
81
  ? fs.readFileSync(template).toString()
82
82
  : `---
83
83
  id: {{{id}}}
84
+ {{^api}}
85
+ sidebar_label: Introduction
86
+ {{/api}}
87
+ {{#api}}
84
88
  sidebar_label: {{{title}}}
89
+ {{/api}}
85
90
  {{^api}}
86
91
  sidebar_position: 0
87
92
  {{/api}}
@@ -100,6 +105,24 @@ sidebar_class_name: "{{{api.method}}} api-method"
100
105
  {{{markdown}}}
101
106
  `;
102
107
 
108
+ const infoMdTemplate = template
109
+ ? fs.readFileSync(template).toString()
110
+ : `---
111
+ id: {{{id}}}
112
+ sidebar_label: {{{title}}}
113
+ hide_title: true
114
+ ---
115
+
116
+ {{{markdown}}}
117
+
118
+ \`\`\`mdx-code-block
119
+ import DocCardList from '@theme/DocCardList';
120
+ import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
121
+
122
+ <DocCardList items={useCurrentSidebarCategory().items}/>
123
+ \`\`\`
124
+ `;
125
+
103
126
  loadedApi.map(async (item) => {
104
127
  const markdown =
105
128
  item.type === "api" ? createApiPageMD(item) : createInfoPageMD(item);
@@ -108,6 +131,7 @@ sidebar_class_name: "{{{api.method}}} api-method"
108
131
  item.json = JSON.stringify(item.api);
109
132
  }
110
133
  const view = render(mdTemplate, item);
134
+ const utils = render(infoMdTemplate, item);
111
135
 
112
136
  if (item.type === "api") {
113
137
  if (!fs.existsSync(`${outputDir}/${item.id}.api.mdx`)) {
@@ -129,15 +153,27 @@ sidebar_class_name: "{{{api.method}}} api-method"
129
153
 
130
154
  // TODO: determine if we actually want/need this
131
155
  if (item.type === "info") {
132
- if (!fs.existsSync(`${outputDir}/index.api.mdx`)) {
156
+ if (!fs.existsSync(`${outputDir}/${item.id}.info.mdx`)) {
133
157
  try {
134
- fs.writeFileSync(`${outputDir}/index.api.mdx`, view, "utf8");
158
+ sidebarOptions?.useInfoAsCategoryLink
159
+ ? fs.writeFileSync(
160
+ `${outputDir}/${item.id}.info.mdx`,
161
+ utils,
162
+ "utf8"
163
+ )
164
+ : fs.writeFileSync(
165
+ `${outputDir}/${item.id}.info.mdx`,
166
+ view,
167
+ "utf8"
168
+ );
135
169
  console.log(
136
- chalk.green(`Successfully created "${outputDir}/index.api.mdx"`)
170
+ chalk.green(
171
+ `Successfully created "${outputDir}/${item.id}.info.mdx"`
172
+ )
137
173
  );
138
174
  } catch (err) {
139
175
  console.error(
140
- chalk.red(`Failed to write "${outputDir}/index.api.mdx"`),
176
+ chalk.red(`Failed to write "${outputDir}/${item.id}.info.mdx"`),
141
177
  chalk.yellow(err)
142
178
  );
143
179
  }
@@ -155,7 +191,7 @@ sidebar_class_name: "{{{api.method}}} api-method"
155
191
  async function cleanApiDocs(options: APIOptions) {
156
192
  const { outputDir } = options;
157
193
  const apiDir = path.join(siteDir, outputDir);
158
- const apiMdxFiles = await Globby(["*.api.mdx"], {
194
+ const apiMdxFiles = await Globby(["*.api.mdx", "*.info.mdx"], {
159
195
  cwd: path.resolve(apiDir),
160
196
  });
161
197
  const sidebarFile = await Globby(["sidebar.js"], {
@@ -81,16 +81,20 @@ function createItems(openapiData: OpenApiObject): ApiMetadata[] {
81
81
 
82
82
  // Only create an info page if we have a description.
83
83
  if (openapiData.info.description) {
84
+ const infoId = kebabCase(openapiData.info.title);
84
85
  const infoPage: PartialPage<InfoPageMetadata> = {
85
86
  type: "info",
86
- id: "introduction",
87
- unversionedId: "introduction",
88
- title: "Introduction",
87
+ id: infoId,
88
+ unversionedId: infoId,
89
+ title: openapiData.info.title,
89
90
  description: openapiData.info.description,
90
- slug: "/introduction",
91
+ slug: "/" + infoId,
91
92
  frontMatter: {},
92
93
  info: {
93
94
  ...openapiData.info,
95
+ tags: openapiData.tags?.map((tagName) =>
96
+ getTagDisplayName(tagName.name!, openapiData.tags ?? [])
97
+ ),
94
98
  title: openapiData.info.title ?? "Introduction",
95
99
  },
96
100
  };
@@ -245,34 +249,47 @@ export async function readOpenapiFiles(
245
249
 
246
250
  export async function processOpenapiFiles(
247
251
  files: OpenApiFiles[]
248
- ): Promise<ApiMetadata[]> {
252
+ ): Promise<[ApiMetadata[], TagObject[]]> {
249
253
  const promises = files.map(async (file) => {
250
- const items = await processOpenapiFile(file.data);
251
- return items.map((item) => ({
254
+ const processedFile = await processOpenapiFile(file.data);
255
+ const itemsObjectsArray = processedFile[0].map((item) => ({
252
256
  ...item,
253
257
  }));
258
+ const tags = processedFile[1];
259
+ return [itemsObjectsArray, tags];
254
260
  });
255
261
  const metadata = await Promise.all(promises);
256
- const items = metadata.flat();
257
- return items;
262
+ const items = metadata
263
+ .map(function (x) {
264
+ return x[0];
265
+ })
266
+ .flat();
267
+ const tags = metadata.map(function (x) {
268
+ return x[1];
269
+ });
270
+ return [items as ApiMetadata[], tags as TagObject[]];
258
271
  }
259
272
 
260
273
  export async function processOpenapiFile(
261
274
  openapiDataWithRefs: OpenApiObjectWithRef
262
- ): Promise<ApiMetadata[]> {
275
+ ): Promise<[ApiMetadata[], TagObject[]]> {
263
276
  const openapiData = await resolveRefs(openapiDataWithRefs);
264
277
  const postmanCollection = await createPostmanCollection(openapiData);
265
278
  const items = createItems(openapiData);
266
279
 
267
280
  bindCollectionToApiItems(items, postmanCollection);
268
281
 
269
- return items;
282
+ let tags: TagObject[] = [];
283
+ if (openapiData.tags !== undefined) {
284
+ tags = openapiData.tags;
285
+ }
286
+ return [items, tags];
270
287
  }
271
288
 
272
289
  // order for picking items as a display name of tags
273
290
  const tagDisplayNameProperties = ["x-displayName", "name"] as const;
274
291
 
275
- function getTagDisplayName(tagName: string, tags: TagObject[]): string {
292
+ export function getTagDisplayName(tagName: string, tags: TagObject[]): string {
276
293
  // find the very own tagObject
277
294
  const tagObject = tags.find((tagObject) => tagObject.name === tagName) ?? {
278
295
  // if none found, just fake one
@@ -40,6 +40,7 @@ export interface InfoObject {
40
40
  contact?: ContactObject;
41
41
  license?: LicenseObject;
42
42
  version: string;
43
+ tags?: String[];
43
44
  }
44
45
 
45
46
  export interface ContactObject {
@@ -294,7 +295,7 @@ export type HeaderObject = Omit<ParameterObject, "name" | "in">;
294
295
  export type HeaderObjectWithRef = Omit<ParameterObjectWithRef, "name" | "in">;
295
296
 
296
297
  export interface TagObject {
297
- name: string;
298
+ name?: string;
298
299
  description?: string;
299
300
  externalDocs?: ExternalDocumentationObject;
300
301
  "x-displayName"?: string;
@@ -7,11 +7,14 @@
7
7
 
8
8
  import {
9
9
  ProcessedSidebar,
10
+ SidebarItemCategoryLinkConfig,
10
11
  SidebarItemDoc,
11
12
  } from "@docusaurus/plugin-content-docs/src/sidebars/types";
12
13
  import clsx from "clsx";
14
+ import { kebabCase } from "lodash";
13
15
  import uniq from "lodash/uniq";
14
16
 
17
+ import { TagObject } from "../openapi/types";
15
18
  import type {
16
19
  SidebarOptions,
17
20
  APIOptions,
@@ -23,28 +26,38 @@ function isApiItem(item: ApiMetadata): item is ApiMetadata {
23
26
  return item.type === "api";
24
27
  }
25
28
 
29
+ function isInfoItem(item: ApiMetadata): item is ApiMetadata {
30
+ return item.type === "info";
31
+ }
32
+
26
33
  function groupByTags(
27
34
  items: ApiPageMetadata[],
28
35
  sidebarOptions: SidebarOptions,
29
- options: APIOptions
36
+ options: APIOptions,
37
+ tags: TagObject[]
30
38
  ): ProcessedSidebar {
31
- // TODO: Figure out how to handle these
32
- // const intros = items.filter(isInfoItem).map((item) => {
33
- // return {
34
- // type: "link" as const,
35
- // label: item.title,
36
- // href: item.permalink,
37
- // docId: item.id,
38
- // };
39
- // });
40
-
41
39
  const { outputDir } = options;
42
- const { sidebarCollapsed, sidebarCollapsible, customProps } = sidebarOptions;
40
+ const {
41
+ sidebarCollapsed,
42
+ sidebarCollapsible,
43
+ customProps,
44
+ categoryLinkSource,
45
+ } = sidebarOptions;
46
+ const linkSource = categoryLinkSource ?? "tag";
43
47
 
44
48
  const apiItems = items.filter(isApiItem);
49
+ const infoItems = items.filter(isInfoItem);
50
+ const intros = infoItems.map((item: any) => {
51
+ return {
52
+ id: item.id,
53
+ title: item.title,
54
+ description: item.description,
55
+ tags: item.info.tags,
56
+ };
57
+ });
45
58
 
46
59
  // TODO: make sure we only take the first tag
47
- const tags = uniq(
60
+ const tags_ = uniq(
48
61
  apiItems
49
62
  .flatMap((item) => item.api.tags)
50
63
  .filter((item): item is string => !!item)
@@ -74,11 +87,51 @@ function groupByTags(
74
87
  };
75
88
  }
76
89
 
77
- const tagged = tags
90
+ let introDoc = undefined;
91
+ if (linkSource === "info") {
92
+ const infoItem = infoItems[0];
93
+ const id = infoItem.id;
94
+ introDoc = {
95
+ type: "doc" as const,
96
+ id: `${basePath}/${id}`,
97
+ };
98
+ }
99
+
100
+ const tagged = tags_
78
101
  .map((tag) => {
102
+ // TODO: should we also use the info.title as generated-index title?
103
+ const infoObject = intros.find((i) => i.tags.includes(tag));
104
+ const tagObject = tags.flat().find(
105
+ (t) =>
106
+ (tag === t.name || tag === t["x-displayName"]) ?? {
107
+ name: tag,
108
+ description: `${tag} Index`,
109
+ }
110
+ );
111
+
112
+ // TODO: perhaps move all this into a getLinkConfig() function
113
+ let linkConfig = undefined;
114
+ if (infoObject !== undefined && linkSource === "info") {
115
+ linkConfig = {
116
+ type: "doc",
117
+ id: `${basePath}/${infoObject.id}`,
118
+ } as SidebarItemCategoryLinkConfig;
119
+ }
120
+
121
+ if (tagObject !== undefined && linkSource === "tag") {
122
+ const linkDescription = tagObject?.description;
123
+ linkConfig = {
124
+ type: "generated-index" as "generated-index",
125
+ title: tag,
126
+ description: linkDescription,
127
+ slug: "/category/" + kebabCase(tag),
128
+ } as SidebarItemCategoryLinkConfig;
129
+ }
130
+
79
131
  return {
80
132
  type: "category" as const,
81
133
  label: tag,
134
+ link: linkConfig,
82
135
  collapsible: sidebarCollapsible,
83
136
  collapsed: sidebarCollapsed,
84
137
  items: apiItems
@@ -88,33 +141,40 @@ function groupByTags(
88
141
  })
89
142
  .filter((item) => item.items.length > 0); // Filter out any categories with no items.
90
143
 
144
+ // TODO: determine how we want to handle these
91
145
  // const untagged = [
92
- // // TODO: determine if needed and how
93
146
  // {
94
147
  // type: "category" as const,
95
148
  // label: "UNTAGGED",
96
- // // collapsible: options.sidebarCollapsible, TODO: add option
97
- // // collapsed: options.sidebarCollapsed, TODO: add option
149
+ // collapsible: sidebarCollapsible,
150
+ // collapsed: sidebarCollapsed,
98
151
  // items: apiItems
99
- // //@ts-ignore
100
152
  // .filter(({ api }) => api.tags === undefined || api.tags.length === 0)
101
153
  // .map(createDocItem),
102
154
  // },
103
155
  // ];
156
+
157
+ // Shift intro doc to top of sidebar
158
+ if (introDoc && linkSource === "info") {
159
+ tagged.unshift(introDoc as any);
160
+ }
161
+
104
162
  return [...tagged];
105
163
  }
106
164
 
107
165
  export default function generateSidebarSlice(
108
166
  sidebarOptions: SidebarOptions,
109
167
  options: APIOptions,
110
- api: ApiMetadata[]
168
+ api: ApiMetadata[],
169
+ tags: TagObject[]
111
170
  ) {
112
171
  let sidebarSlice: ProcessedSidebar = [];
113
172
  if (sidebarOptions.groupPathsBy === "tags") {
114
173
  sidebarSlice = groupByTags(
115
174
  api as ApiPageMetadata[],
116
175
  sidebarOptions,
117
- options
176
+ options,
177
+ tags
118
178
  );
119
179
  }
120
180
  return sidebarSlice;
package/src/types.ts CHANGED
@@ -90,6 +90,8 @@ export interface ApiNavLink {
90
90
 
91
91
  export interface SidebarOptions {
92
92
  groupPathsBy?: string;
93
+ useInfoAsCategoryLink?: boolean; // TODO: confirm name of option
94
+ categoryLinkSource?: string;
93
95
  customProps?: { [key: string]: unknown };
94
96
  sidebarCollapsible?: boolean;
95
97
  sidebarCollapsed?: boolean;