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

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.categoryLinkSource) === "info" // Only use utils template if set to "info"
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"], {
@@ -9,7 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.createContactInfo = void 0;
10
10
  const utils_1 = require("./utils");
11
11
  function createContactInfo(contact) {
12
- if (!contact)
12
+ if (!contact || !Object.keys(contact).length)
13
13
  return "";
14
14
  const { name, url, email } = contact;
15
15
  return (0, utils_1.create)("div", {
@@ -27,14 +27,14 @@ function createContactInfo(contact) {
27
27
  }),
28
28
  (0, utils_1.create)("span", {
29
29
  children: [
30
- `${name}: `,
31
- (0, utils_1.create)("a", {
30
+ (0, utils_1.guard)(name, () => `${name}: `),
31
+ (0, utils_1.guard)(email, () => (0, utils_1.create)("a", {
32
32
  href: `mailto:${email}`,
33
33
  children: `${email}`,
34
- }),
34
+ })),
35
35
  ],
36
36
  }),
37
- (0, utils_1.create)("span", {
37
+ (0, utils_1.guard)(url, () => (0, utils_1.create)("span", {
38
38
  children: [
39
39
  "URL: ",
40
40
  (0, utils_1.create)("a", {
@@ -42,7 +42,7 @@ function createContactInfo(contact) {
42
42
  children: `${url}`,
43
43
  }),
44
44
  ],
45
- }),
45
+ })),
46
46
  ],
47
47
  });
48
48
  }
@@ -9,7 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.createLicense = void 0;
10
10
  const utils_1 = require("./utils");
11
11
  function createLicense(license) {
12
- if (!license)
12
+ if (!license || !Object.keys(license).length)
13
13
  return "";
14
14
  const { name, url } = license;
15
15
  return (0, utils_1.create)("div", {
@@ -23,10 +23,10 @@ function createLicense(license) {
23
23
  },
24
24
  children: "License",
25
25
  }),
26
- (0, utils_1.create)("a", {
26
+ (0, utils_1.guard)(url, () => (0, utils_1.create)("a", {
27
27
  href: url,
28
- children: name,
29
- }),
28
+ children: name !== null && name !== void 0 ? name : url,
29
+ })),
30
30
  ],
31
31
  });
32
32
  }
@@ -9,7 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.createTermsOfService = void 0;
10
10
  const utils_1 = require("./utils");
11
11
  function createTermsOfService(termsOfService) {
12
- if (!createTermsOfService)
12
+ if (!termsOfService)
13
13
  return "";
14
14
  return (0, utils_1.create)("div", {
15
15
  style: {
@@ -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,29 @@ 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;
29
24
  const apiItems = items.filter(isApiItem);
25
+ const infoItems = items.filter(isInfoItem);
26
+ const intros = infoItems.map((item) => {
27
+ return {
28
+ id: item.id,
29
+ title: item.title,
30
+ description: item.description,
31
+ tags: item.info.tags,
32
+ };
33
+ });
30
34
  // TODO: make sure we only take the first tag
31
- const tags = (0, uniq_1.default)(apiItems
35
+ const apiTags = (0, uniq_1.default)(apiItems
32
36
  .flatMap((item) => item.api.tags)
33
37
  .filter((item) => !!item));
34
38
  // TODO: optimize this or make it a function
@@ -51,11 +55,56 @@ function groupByTags(items, sidebarOptions, options) {
51
55
  }, item.api.method),
52
56
  };
53
57
  }
54
- const tagged = tags
58
+ let rootIntroDoc = undefined;
59
+ if (infoItems.length === 1) {
60
+ const infoItem = infoItems[0];
61
+ const id = infoItem.id;
62
+ rootIntroDoc = {
63
+ type: "doc",
64
+ id: `${basePath}/${id}`,
65
+ };
66
+ }
67
+ const tagged = apiTags
55
68
  .map((tag) => {
69
+ // Map info object to tag
70
+ const infoObject = intros.find((i) => i.tags.includes(tag));
71
+ const tagObject = tags.flat().find((t) => {
72
+ var _a;
73
+ return (_a = (tag === t.name || tag === t["x-displayName"])) !== null && _a !== void 0 ? _a : {
74
+ name: tag,
75
+ description: `${tag} Index`,
76
+ };
77
+ });
78
+ // TODO: perhaps move this into a getLinkConfig() function
79
+ let linkConfig = undefined;
80
+ if (infoObject !== undefined && categoryLinkSource === "info") {
81
+ linkConfig = {
82
+ type: "doc",
83
+ id: `${basePath}/${infoObject.id}`,
84
+ };
85
+ }
86
+ // TODO: perhaps move this into a getLinkConfig() function
87
+ if (tagObject !== undefined && categoryLinkSource === "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
+ }
96
+ // Default behavior
97
+ if (categoryLinkSource === undefined) {
98
+ linkConfig = {
99
+ type: "generated-index",
100
+ title: tag,
101
+ slug: "/category/" + (0, lodash_1.kebabCase)(tag),
102
+ };
103
+ }
56
104
  return {
57
105
  type: "category",
58
106
  label: tag,
107
+ link: linkConfig,
59
108
  collapsible: sidebarCollapsible,
60
109
  collapsed: sidebarCollapsed,
61
110
  items: apiItems
@@ -64,25 +113,29 @@ function groupByTags(items, sidebarOptions, options) {
64
113
  };
65
114
  })
66
115
  .filter((item) => item.items.length > 0); // Filter out any categories with no items.
116
+ // TODO: determine how we want to handle these
67
117
  // const untagged = [
68
- // // TODO: determine if needed and how
69
118
  // {
70
119
  // type: "category" as const,
71
120
  // label: "UNTAGGED",
72
- // // collapsible: options.sidebarCollapsible, TODO: add option
73
- // // collapsed: options.sidebarCollapsed, TODO: add option
121
+ // collapsible: sidebarCollapsible,
122
+ // collapsed: sidebarCollapsed,
74
123
  // items: apiItems
75
- // //@ts-ignore
76
124
  // .filter(({ api }) => api.tags === undefined || api.tags.length === 0)
77
125
  // .map(createDocItem),
78
126
  // },
79
127
  // ];
128
+ // Shift root intro doc to top of sidebar
129
+ // TODO: Add input validation for categoryLinkSource options
130
+ if (rootIntroDoc && categoryLinkSource !== "info") {
131
+ tagged.unshift(rootIntroDoc);
132
+ }
80
133
  return [...tagged];
81
134
  }
82
- function generateSidebarSlice(sidebarOptions, options, api) {
135
+ function generateSidebarSlice(sidebarOptions, options, api, tags) {
83
136
  let sidebarSlice = [];
84
137
  if (sidebarOptions.groupPathsBy === "tags") {
85
- sidebarSlice = groupByTags(api, sidebarOptions, options);
138
+ sidebarSlice = groupByTags(api, sidebarOptions, options, tags);
86
139
  }
87
140
  return sidebarSlice;
88
141
  }
package/lib/types.d.ts CHANGED
@@ -60,6 +60,7 @@ export interface ApiNavLink {
60
60
  }
61
61
  export interface SidebarOptions {
62
62
  groupPathsBy?: string;
63
+ categoryLinkSource?: string;
63
64
  customProps?: {
64
65
  [key: string]: unknown;
65
66
  };
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-354",
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": "14f792796067d499c5d244976da0f3adcdbbb383"
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?.categoryLinkSource === "info" // Only use utils template if set to "info"
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"], {
@@ -6,10 +6,10 @@
6
6
  * ========================================================================== */
7
7
 
8
8
  import { ContactObject } from "../openapi/types";
9
- import { create } from "./utils";
9
+ import { create, guard } from "./utils";
10
10
 
11
11
  export function createContactInfo(contact: ContactObject) {
12
- if (!contact) return "";
12
+ if (!contact || !Object.keys(contact).length) return "";
13
13
  const { name, url, email } = contact;
14
14
 
15
15
  return create("div", {
@@ -27,22 +27,26 @@ export function createContactInfo(contact: ContactObject) {
27
27
  }),
28
28
  create("span", {
29
29
  children: [
30
- `${name}: `,
31
- create("a", {
32
- href: `mailto:${email}`,
33
- children: `${email}`,
34
- }),
35
- ],
36
- }),
37
- create("span", {
38
- children: [
39
- "URL: ",
40
- create("a", {
41
- href: `${url}`,
42
- children: `${url}`,
43
- }),
30
+ guard(name, () => `${name}: `),
31
+ guard(email, () =>
32
+ create("a", {
33
+ href: `mailto:${email}`,
34
+ children: `${email}`,
35
+ })
36
+ ),
44
37
  ],
45
38
  }),
39
+ guard(url, () =>
40
+ create("span", {
41
+ children: [
42
+ "URL: ",
43
+ create("a", {
44
+ href: `${url}`,
45
+ children: `${url}`,
46
+ }),
47
+ ],
48
+ })
49
+ ),
46
50
  ],
47
51
  });
48
52
  }
@@ -6,10 +6,10 @@
6
6
  * ========================================================================== */
7
7
 
8
8
  import { LicenseObject } from "../openapi/types";
9
- import { create } from "./utils";
9
+ import { create, guard } from "./utils";
10
10
 
11
11
  export function createLicense(license: LicenseObject) {
12
- if (!license) return "";
12
+ if (!license || !Object.keys(license).length) return "";
13
13
  const { name, url } = license;
14
14
 
15
15
  return create("div", {
@@ -23,10 +23,12 @@ export function createLicense(license: LicenseObject) {
23
23
  },
24
24
  children: "License",
25
25
  }),
26
- create("a", {
27
- href: url,
28
- children: name,
29
- }),
26
+ guard(url, () =>
27
+ create("a", {
28
+ href: url,
29
+ children: name ?? url,
30
+ })
31
+ ),
30
32
  ],
31
33
  });
32
34
  }
@@ -8,7 +8,7 @@
8
8
  import { create } from "./utils";
9
9
 
10
10
  export function createTermsOfService(termsOfService: string | undefined) {
11
- if (!createTermsOfService) return "";
11
+ if (!termsOfService) return "";
12
12
 
13
13
  return create("div", {
14
14
  style: {
@@ -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,37 @@ 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;
43
46
 
44
47
  const apiItems = items.filter(isApiItem);
48
+ const infoItems = items.filter(isInfoItem);
49
+ const intros = infoItems.map((item: any) => {
50
+ return {
51
+ id: item.id,
52
+ title: item.title,
53
+ description: item.description,
54
+ tags: item.info.tags,
55
+ };
56
+ });
45
57
 
46
58
  // TODO: make sure we only take the first tag
47
- const tags = uniq(
59
+ const apiTags = uniq(
48
60
  apiItems
49
61
  .flatMap((item) => item.api.tags)
50
62
  .filter((item): item is string => !!item)
@@ -74,11 +86,61 @@ function groupByTags(
74
86
  };
75
87
  }
76
88
 
77
- const tagged = tags
89
+ let rootIntroDoc = undefined;
90
+ if (infoItems.length === 1) {
91
+ const infoItem = infoItems[0];
92
+ const id = infoItem.id;
93
+ rootIntroDoc = {
94
+ type: "doc" as const,
95
+ id: `${basePath}/${id}`,
96
+ };
97
+ }
98
+
99
+ const tagged = apiTags
78
100
  .map((tag) => {
101
+ // Map info object to tag
102
+ const infoObject = intros.find((i) => i.tags.includes(tag));
103
+ const tagObject = tags.flat().find(
104
+ (t) =>
105
+ (tag === t.name || tag === t["x-displayName"]) ?? {
106
+ name: tag,
107
+ description: `${tag} Index`,
108
+ }
109
+ );
110
+
111
+ // TODO: perhaps move this into a getLinkConfig() function
112
+ let linkConfig = undefined;
113
+ if (infoObject !== undefined && categoryLinkSource === "info") {
114
+ linkConfig = {
115
+ type: "doc",
116
+ id: `${basePath}/${infoObject.id}`,
117
+ } as SidebarItemCategoryLinkConfig;
118
+ }
119
+
120
+ // TODO: perhaps move this into a getLinkConfig() function
121
+ if (tagObject !== undefined && categoryLinkSource === "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
+
131
+ // Default behavior
132
+ if (categoryLinkSource === undefined) {
133
+ linkConfig = {
134
+ type: "generated-index" as "generated-index",
135
+ title: tag,
136
+ slug: "/category/" + kebabCase(tag),
137
+ } as SidebarItemCategoryLinkConfig;
138
+ }
139
+
79
140
  return {
80
141
  type: "category" as const,
81
142
  label: tag,
143
+ link: linkConfig,
82
144
  collapsible: sidebarCollapsible,
83
145
  collapsed: sidebarCollapsed,
84
146
  items: apiItems
@@ -88,33 +150,41 @@ function groupByTags(
88
150
  })
89
151
  .filter((item) => item.items.length > 0); // Filter out any categories with no items.
90
152
 
153
+ // TODO: determine how we want to handle these
91
154
  // const untagged = [
92
- // // TODO: determine if needed and how
93
155
  // {
94
156
  // type: "category" as const,
95
157
  // label: "UNTAGGED",
96
- // // collapsible: options.sidebarCollapsible, TODO: add option
97
- // // collapsed: options.sidebarCollapsed, TODO: add option
158
+ // collapsible: sidebarCollapsible,
159
+ // collapsed: sidebarCollapsed,
98
160
  // items: apiItems
99
- // //@ts-ignore
100
161
  // .filter(({ api }) => api.tags === undefined || api.tags.length === 0)
101
162
  // .map(createDocItem),
102
163
  // },
103
164
  // ];
165
+
166
+ // Shift root intro doc to top of sidebar
167
+ // TODO: Add input validation for categoryLinkSource options
168
+ if (rootIntroDoc && categoryLinkSource !== "info") {
169
+ tagged.unshift(rootIntroDoc as any);
170
+ }
171
+
104
172
  return [...tagged];
105
173
  }
106
174
 
107
175
  export default function generateSidebarSlice(
108
176
  sidebarOptions: SidebarOptions,
109
177
  options: APIOptions,
110
- api: ApiMetadata[]
178
+ api: ApiMetadata[],
179
+ tags: TagObject[]
111
180
  ) {
112
181
  let sidebarSlice: ProcessedSidebar = [];
113
182
  if (sidebarOptions.groupPathsBy === "tags") {
114
183
  sidebarSlice = groupByTags(
115
184
  api as ApiPageMetadata[],
116
185
  sidebarOptions,
117
- options
186
+ options,
187
+ tags
118
188
  );
119
189
  }
120
190
  return sidebarSlice;
package/src/types.ts CHANGED
@@ -90,6 +90,7 @@ export interface ApiNavLink {
90
90
 
91
91
  export interface SidebarOptions {
92
92
  groupPathsBy?: string;
93
+ categoryLinkSource?: string;
93
94
  customProps?: { [key: string]: unknown };
94
95
  sidebarCollapsible?: boolean;
95
96
  sidebarCollapsed?: boolean;