docusaurus-plugin-openapi-docs 4.5.1 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +110 -22
  2. package/lib/index.js +122 -50
  3. package/lib/markdown/createHeading.js +1 -1
  4. package/lib/markdown/createRequestHeader.js +5 -3
  5. package/lib/markdown/createSchema.js +2 -2
  6. package/lib/markdown/index.js +3 -2
  7. package/lib/markdown/schema.js +5 -0
  8. package/lib/markdown/utils.d.ts +38 -1
  9. package/lib/markdown/utils.js +100 -2
  10. package/lib/openapi/createSchemaExample.js +16 -2
  11. package/lib/openapi/createSchemaExample.test.d.ts +1 -0
  12. package/lib/openapi/createSchemaExample.test.js +48 -0
  13. package/lib/openapi/openapi.js +38 -17
  14. package/lib/openapi/openapi.test.js +48 -0
  15. package/lib/openapi/webhooks.test.d.ts +1 -0
  16. package/lib/openapi/webhooks.test.js +23 -0
  17. package/lib/options.js +4 -0
  18. package/lib/sidebars/index.js +12 -3
  19. package/package.json +16 -16
  20. package/src/index.ts +165 -62
  21. package/src/markdown/createHeading.ts +2 -2
  22. package/src/markdown/createRequestHeader.ts +9 -11
  23. package/src/markdown/createSchema.ts +4 -2
  24. package/src/markdown/index.ts +3 -2
  25. package/src/markdown/schema.ts +6 -0
  26. package/src/markdown/utils.ts +153 -3
  27. package/src/openapi/__fixtures__/webhook/openapi.yaml +17 -0
  28. package/src/openapi/createSchemaExample.test.ts +57 -0
  29. package/src/openapi/createSchemaExample.ts +26 -2
  30. package/src/openapi/openapi.test.ts +58 -0
  31. package/src/openapi/openapi.ts +35 -6
  32. package/src/openapi/webhooks.test.ts +30 -0
  33. package/src/options.ts +4 -0
  34. package/src/plugin-openapi.d.ts +1 -1
  35. package/src/sidebars/index.ts +15 -3
  36. package/src/{types.ts → types.d.ts} +12 -2
  37. package/lib/types.d.ts +0 -135
  38. package/lib/types.js +0 -8
  39. package/src/plugin-content-docs-types.d.ts +0 -42
@@ -5,9 +5,93 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  * ========================================================================== */
7
7
 
8
+ /**
9
+ * Represents an external JSON file to be written alongside the MDX.
10
+ */
11
+ export interface ExternalFile {
12
+ /** The filename for the JSON file (relative to outputDir) */
13
+ filename: string;
14
+ /** The JSON content to write */
15
+ content: string;
16
+ }
17
+
18
+ /**
19
+ * Result of running MDX generation with externalization.
20
+ */
21
+ export interface ExternalizationResult<T> {
22
+ /** The result of the generation function */
23
+ result: T;
24
+ /** External JSON files to write */
25
+ files: ExternalFile[];
26
+ }
27
+
28
+ /**
29
+ * Context for externalization during MDX generation.
30
+ */
31
+ interface ExternalizationContext {
32
+ /** Base filename for external files (e.g., "add-pet" for "add-pet.api.mdx") */
33
+ baseFilename: string;
34
+ /** Counter for generating unique filenames per component type */
35
+ componentCounters: Record<string, number>;
36
+ /** Collected external files during generation */
37
+ files: ExternalFile[];
38
+ }
39
+
40
+ /**
41
+ * Module-level externalization context.
42
+ * Note: AsyncLocalStorage would be cleaner but isn't available in browser bundles.
43
+ */
44
+ let externalizationContext: ExternalizationContext | null = null;
45
+
46
+ /**
47
+ * Components whose props should be externalized to separate JSON files.
48
+ * These are the components that typically receive large JSON objects.
49
+ */
50
+ const EXTERNALIZABLE_COMPONENTS = new Set([
51
+ "StatusCodes",
52
+ "ParamsDetails",
53
+ "RequestSchema",
54
+ "Schema",
55
+ "SchemaItem",
56
+ ]);
57
+
58
+ /**
59
+ * Runs a function with externalization enabled.
60
+ * Any calls to create() within the function will externalize eligible component props.
61
+ *
62
+ * @param baseFilename - Base filename for the MDX file (without extension)
63
+ * @param fn - Function to run with externalization enabled
64
+ * @returns The function result and any external files that were collected
65
+ *
66
+ * @example
67
+ * const { result: mdx, files } = runWithExternalization("add-pet", () => {
68
+ * return createApiPageMD(item);
69
+ * });
70
+ */
71
+ export function runWithExternalization<T>(
72
+ baseFilename: string,
73
+ fn: () => T
74
+ ): ExternalizationResult<T> {
75
+ // Set up context
76
+ externalizationContext = {
77
+ baseFilename,
78
+ componentCounters: {},
79
+ files: [],
80
+ };
81
+
82
+ try {
83
+ const result = fn();
84
+ const files = externalizationContext.files;
85
+ return { result, files };
86
+ } finally {
87
+ // Always clear context
88
+ externalizationContext = null;
89
+ }
90
+ }
91
+
8
92
  /**
9
93
  * Children in the plugin does not accept DOM elements, when compared with Children in the theme.
10
- * It is designed for rendering HTML a strings.
94
+ * It is designed for rendering HTML as strings.
11
95
  */
12
96
  export type Children = string | undefined | (string | string[] | undefined)[];
13
97
 
@@ -15,6 +99,11 @@ export type Props = Record<string, any> & { children?: Children };
15
99
 
16
100
  export type Options = { inline?: boolean };
17
101
 
102
+ /**
103
+ * Creates a JSX component string with the given tag, props, and options.
104
+ * When called within runWithExternalization(), props for eligible
105
+ * components are externalized to a single JSON file and spread.
106
+ */
18
107
  export function create(
19
108
  tag: string,
20
109
  props: Props,
@@ -23,9 +112,27 @@ export function create(
23
112
  const { children, ...rest } = props;
24
113
 
25
114
  let propString = "";
26
- for (const [key, value] of Object.entries(rest)) {
27
- propString += `\n ${key}={${JSON.stringify(value)}}`;
115
+
116
+ // Check if this component's props should be externalized
117
+ if (shouldExternalizeComponent(tag, rest)) {
118
+ const filename = generateExternalFilename(tag);
119
+ const content = JSON.stringify(rest);
120
+
121
+ // Add to external files
122
+ externalizationContext!.files.push({
123
+ filename,
124
+ content,
125
+ });
126
+
127
+ // Use spread syntax with require
128
+ propString = `\n {...require("./${filename}")}`;
129
+ } else {
130
+ // Inline props as usual
131
+ for (const [key, value] of Object.entries(rest)) {
132
+ propString += `\n ${key}={${JSON.stringify(value)}}`;
133
+ }
28
134
  }
135
+
29
136
  let indentedChildren = render(children).replace(/^/gm, " ");
30
137
 
31
138
  if (options.inline) {
@@ -38,6 +145,49 @@ export function create(
38
145
  return `<${tag}${propString}>\n${indentedChildren}</${tag}>`;
39
146
  }
40
147
 
148
+ /**
149
+ * Determines if a component's props should be externalized.
150
+ */
151
+ function shouldExternalizeComponent(
152
+ tag: string,
153
+ props: Record<string, any>
154
+ ): boolean {
155
+ // No context means externalization is not enabled
156
+ if (!externalizationContext) {
157
+ return false;
158
+ }
159
+
160
+ if (!EXTERNALIZABLE_COMPONENTS.has(tag)) {
161
+ return false;
162
+ }
163
+
164
+ // Don't externalize if props are empty or only contain undefined/null
165
+ const hasContent = Object.values(props).some(
166
+ (v) => v !== undefined && v !== null
167
+ );
168
+ if (!hasContent) {
169
+ return false;
170
+ }
171
+
172
+ return true;
173
+ }
174
+
175
+ /**
176
+ * Generates a unique filename for an externalized component's props.
177
+ */
178
+ function generateExternalFilename(componentName: string): string {
179
+ if (!externalizationContext) {
180
+ throw new Error("Externalization context not set");
181
+ }
182
+
183
+ const count =
184
+ (externalizationContext.componentCounters[componentName] ?? 0) + 1;
185
+ externalizationContext.componentCounters[componentName] = count;
186
+
187
+ const suffix = count > 1 ? `.${count}` : "";
188
+ return `${externalizationContext.baseFilename}.${componentName}${suffix}.json`;
189
+ }
190
+
41
191
  export function guard<T>(
42
192
  value: T | undefined,
43
193
  cb: (value: T) => Children
@@ -0,0 +1,17 @@
1
+ openapi: 3.0.3
2
+ info:
3
+ title: Webhook Example
4
+ version: 1.0.0
5
+ paths: {}
6
+ webhooks:
7
+ order.created:
8
+ post:
9
+ requestBody:
10
+ description: example body
11
+ content:
12
+ application/json:
13
+ schema:
14
+ type: object
15
+ responses:
16
+ "200":
17
+ description: OK
@@ -0,0 +1,57 @@
1
+ /* ============================================================================
2
+ * Copyright (c) Palo Alto Networks
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ * ========================================================================== */
7
+
8
+ import { sampleFromSchema } from "./createSchemaExample";
9
+ import { SchemaObject } from "./types";
10
+
11
+ describe("sampleFromSchema", () => {
12
+ describe("const support", () => {
13
+ it("should return default string value when const is not present", () => {
14
+ const schema: SchemaObject = {
15
+ type: "string",
16
+ };
17
+ const context = { type: "request" as const };
18
+
19
+ const result = sampleFromSchema(schema, context);
20
+
21
+ expect(result).toBe("string");
22
+ });
23
+
24
+ it("should return const value when const is present", () => {
25
+ const schema: SchemaObject = {
26
+ type: "string",
27
+ const: "example",
28
+ };
29
+ const context = { type: "request" as const };
30
+
31
+ const result = sampleFromSchema(schema, context);
32
+
33
+ expect(result).toBe("example");
34
+ });
35
+
36
+ it("should handle anyOf with const values", () => {
37
+ const schema: SchemaObject = {
38
+ type: "string",
39
+ anyOf: [
40
+ {
41
+ type: "string",
42
+ const: "dog",
43
+ },
44
+ {
45
+ type: "string",
46
+ const: "cat",
47
+ },
48
+ ],
49
+ };
50
+ const context = { type: "request" as const };
51
+
52
+ const result = sampleFromSchema(schema, context);
53
+
54
+ expect(result).toBe("dog");
55
+ });
56
+ });
57
+ });
@@ -91,7 +91,12 @@ function sampleFromProp(
91
91
 
92
92
  // TODO: handle discriminators
93
93
 
94
- if (prop.oneOf) {
94
+ // Check for explicit example/examples first (OAS 3.1 support)
95
+ if (prop.example !== undefined) {
96
+ obj[name] = prop.example;
97
+ } else if (prop.examples !== undefined && prop.examples.length > 0) {
98
+ obj[name] = prop.examples[0];
99
+ } else if (prop.oneOf) {
95
100
  obj[name] = sampleFromSchema(prop.oneOf[0], context);
96
101
  } else if (prop.anyOf) {
97
102
  obj[name] = sampleFromSchema(prop.anyOf[0], context);
@@ -111,12 +116,27 @@ export const sampleFromSchema = (
111
116
  try {
112
117
  // deep copy schema before processing
113
118
  let schemaCopy = JSON.parse(JSON.stringify(schema));
114
- let { type, example, allOf, properties, items, oneOf, anyOf } = schemaCopy;
119
+ let {
120
+ type,
121
+ example,
122
+ examples,
123
+ allOf,
124
+ properties,
125
+ items,
126
+ oneOf,
127
+ anyOf,
128
+ const: constant,
129
+ } = schemaCopy;
115
130
 
116
131
  if (example !== undefined) {
117
132
  return example;
118
133
  }
119
134
 
135
+ // OAS 3.1 / JSON Schema: examples is an array
136
+ if (examples !== undefined && examples.length > 0) {
137
+ return examples[0];
138
+ }
139
+
120
140
  if (oneOf) {
121
141
  if (properties) {
122
142
  const combinedSchemas = merge(schemaCopy, oneOf[0]);
@@ -218,6 +238,10 @@ export const sampleFromSchema = (
218
238
  return undefined;
219
239
  }
220
240
 
241
+ if (constant) {
242
+ return constant;
243
+ }
244
+
221
245
  return primitive(schemaCopy);
222
246
  } catch (err) {
223
247
  console.error(
@@ -11,6 +11,8 @@ import path from "path";
11
11
  import { posixPath } from "@docusaurus/utils";
12
12
 
13
13
  import { readOpenapiFiles } from ".";
14
+ import { processOpenapiFile } from "./openapi";
15
+ import type { APIOptions, SidebarOptions } from "../types";
14
16
 
15
17
  // npx jest packages/docusaurus-plugin-openapi/src/openapi/openapi.test.ts --watch
16
18
 
@@ -37,4 +39,60 @@ describe("openapi", () => {
37
39
  ).toBeDefined();
38
40
  });
39
41
  });
42
+
43
+ describe("schemasOnly", () => {
44
+ it("includes schema metadata when showSchemas is disabled", async () => {
45
+ const openapiData = {
46
+ openapi: "3.0.0",
47
+ info: {
48
+ title: "Schema Only",
49
+ version: "1.0.0",
50
+ },
51
+ paths: {
52
+ "/ping": {
53
+ get: {
54
+ summary: "Ping",
55
+ responses: {
56
+ "200": {
57
+ description: "OK",
58
+ },
59
+ },
60
+ },
61
+ },
62
+ },
63
+ components: {
64
+ schemas: {
65
+ WithoutTags: {
66
+ title: "Without Tags",
67
+ type: "object",
68
+ properties: {
69
+ value: {
70
+ type: "string",
71
+ },
72
+ },
73
+ },
74
+ },
75
+ },
76
+ };
77
+
78
+ const options: APIOptions = {
79
+ specPath: "dummy", // required by the type but unused in this context
80
+ outputDir: "build",
81
+ showSchemas: false,
82
+ schemasOnly: true,
83
+ };
84
+
85
+ const sidebarOptions = {} as SidebarOptions;
86
+
87
+ const [items] = await processOpenapiFile(
88
+ openapiData as any,
89
+ options,
90
+ sidebarOptions
91
+ );
92
+
93
+ const schemaItems = items.filter((item) => item.type === "schema");
94
+ expect(schemaItems).toHaveLength(1);
95
+ expect(schemaItems[0].id).toBe("without-tags");
96
+ });
97
+ });
40
98
  });
@@ -43,9 +43,15 @@ function jsonToCollection(data: OpenApiObject): Promise<Collection> {
43
43
  { schemaFaker: false }
44
44
  );
45
45
  schemaPack.computedOptions.schemaFaker = false;
46
+
47
+ // Make sure the schema was properly validated or reject with error
48
+ if (!schemaPack.validationResult?.result) {
49
+ return reject(schemaPack.validationResult?.reason);
50
+ }
51
+
46
52
  schemaPack.convert((_err: any, conversionResult: any) => {
47
- if (!conversionResult.result) {
48
- return reject(conversionResult.reason);
53
+ if (_err || !conversionResult.result) {
54
+ return reject(_err || conversionResult.reason);
49
55
  }
50
56
  return resolve(new sdk.Collection(conversionResult.output[0].data));
51
57
  });
@@ -89,9 +95,13 @@ function createItems(
89
95
  let items: PartialPage<ApiMetadata>[] = [];
90
96
  const infoIdSpaces = openapiData.info.title.replace(" ", "-").toLowerCase();
91
97
  const infoId = kebabCase(infoIdSpaces);
98
+ const schemasOnly = options?.schemasOnly === true;
92
99
 
93
- if (openapiData.info.description || openapiData.info.title) {
94
- // Only create an info page if we have a description.
100
+ // Only create an info page if we have a description/title AND showInfoPage is not false
101
+ if (
102
+ (openapiData.info.description || openapiData.info.title) &&
103
+ options?.showInfoPage !== false
104
+ ) {
95
105
  const infoDescription = openapiData.info?.description;
96
106
  let splitDescription: any;
97
107
  if (infoDescription) {
@@ -250,6 +260,9 @@ function createItems(
250
260
  ...(options?.showExtensions && {
251
261
  show_extensions: options.showExtensions,
252
262
  }),
263
+ ...(options?.maskCredentials === false && {
264
+ mask_credentials_disabled: true,
265
+ }),
253
266
  },
254
267
  api: {
255
268
  ...defaults,
@@ -274,11 +287,19 @@ function createItems(
274
287
  for (let [path, pathObject] of Object.entries(
275
288
  openapiData["x-webhooks"] ?? openapiData["webhooks"] ?? {}
276
289
  )) {
290
+ const eventName = path;
277
291
  path = "webhook";
278
292
  const { $ref, description, parameters, servers, summary, ...rest } =
279
293
  pathObject;
280
294
  for (let [method, operationObject] of Object.entries({ ...rest })) {
281
295
  method = "event";
296
+ if (
297
+ operationObject.summary === undefined &&
298
+ operationObject.operationId === undefined
299
+ ) {
300
+ operationObject.summary = eventName;
301
+ }
302
+
282
303
  const title =
283
304
  operationObject.summary ??
284
305
  operationObject.operationId ??
@@ -290,7 +311,7 @@ function createItems(
290
311
 
291
312
  const baseId = operationObject.operationId
292
313
  ? kebabCase(operationObject.operationId)
293
- : kebabCase(operationObject.summary);
314
+ : kebabCase(operationObject.summary ?? eventName);
294
315
 
295
316
  const extensions = [];
296
317
  const commonExtensions = ["x-codeSamples"];
@@ -395,6 +416,9 @@ function createItems(
395
416
  ...(options?.showExtensions && {
396
417
  show_extensions: options.showExtensions,
397
418
  }),
419
+ ...(options?.maskCredentials === false && {
420
+ mask_credentials_disabled: true,
421
+ }),
398
422
  },
399
423
  api: {
400
424
  ...defaults,
@@ -414,6 +438,7 @@ function createItems(
414
438
  }
415
439
 
416
440
  if (
441
+ schemasOnly ||
417
442
  options?.showSchemas === true ||
418
443
  Object.entries(openapiData?.components?.schemas ?? {})
419
444
  .flatMap(([_, s]) => s["x-tags"])
@@ -423,7 +448,11 @@ function createItems(
423
448
  for (let [schema, schemaObject] of Object.entries(
424
449
  openapiData?.components?.schemas ?? {}
425
450
  )) {
426
- if (options?.showSchemas === true || schemaObject["x-tags"]) {
451
+ if (
452
+ schemasOnly ||
453
+ options?.showSchemas === true ||
454
+ schemaObject["x-tags"]
455
+ ) {
427
456
  const baseIdSpaces =
428
457
  schemaObject?.title?.replace(" ", "-").toLowerCase() ?? "";
429
458
  const baseId = kebabCase(baseIdSpaces);
@@ -0,0 +1,30 @@
1
+ /* ============================================================================
2
+ * Copyright (c) Palo Alto Networks
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ * ========================================================================== */
7
+
8
+ import path from "path";
9
+
10
+ // eslint-disable-next-line import/no-extraneous-dependencies
11
+ import { posixPath } from "@docusaurus/utils";
12
+
13
+ import { readOpenapiFiles, processOpenapiFiles } from ".";
14
+
15
+ describe("webhooks", () => {
16
+ it("uses event name when summary and operationId are missing", async () => {
17
+ const files = await readOpenapiFiles(
18
+ posixPath(path.join(__dirname, "__fixtures__/webhook/openapi.yaml"))
19
+ );
20
+
21
+ const [items] = await processOpenapiFiles(
22
+ files,
23
+ { specPath: "", outputDir: "" } as any,
24
+ {}
25
+ );
26
+
27
+ const webhookItem = items.find((item) => item.type === "api");
28
+ expect(webhookItem?.id).toBe("order-created");
29
+ });
30
+ });
package/src/options.ts CHANGED
@@ -48,7 +48,11 @@ export const OptionsSchema = Joi.object({
48
48
  sidebarOptions: sidebarOptions,
49
49
  markdownGenerators: markdownGenerators,
50
50
  showSchemas: Joi.boolean(),
51
+ showInfoPage: Joi.boolean(),
52
+ schemasOnly: Joi.boolean(),
51
53
  disableCompression: Joi.boolean(),
54
+ maskCredentials: Joi.boolean(),
55
+ externalJsonProps: Joi.boolean().default(true),
52
56
  version: Joi.string().when("versions", {
53
57
  is: Joi.exist(),
54
58
  then: Joi.required(),
@@ -11,7 +11,7 @@ import type { FrontMatter as DocsFrontMatter } from "@docusaurus/types";
11
11
  import type { Props as DocsProps } from "@docusaurus/types";
12
12
 
13
13
  declare module "docusaurus-plugin-openapi-docs" {
14
- import type { PropSidebars } from "@docusaurus/plugin-content-docs-types";
14
+ import type { PropSidebars } from "@docusaurus/plugin-content-docs/lib/sidebars/types";
15
15
 
16
16
  export type Options = Partial<import("./types").APIOptions>;
17
17
 
@@ -125,9 +125,21 @@ function groupByTags(
125
125
  apiTags = uniq(apiTags.concat(operationTags, schemaTags));
126
126
  }
127
127
 
128
- const basePath = docPath
129
- ? outputDir.split(docPath!)[1].replace(/^\/+/g, "")
130
- : outputDir.slice(outputDir.indexOf("/", 1)).replace(/^\/+/g, "");
128
+ // Extract base path from outputDir, handling cases where docPath may not be in outputDir
129
+ const getBasePathFromOutput = (
130
+ output: string,
131
+ doc: string | undefined
132
+ ): string => {
133
+ if (doc && output.includes(doc)) {
134
+ return output.split(doc)[1]?.replace(/^\/+/g, "") ?? "";
135
+ }
136
+ const slashIndex = output.indexOf("/", 1);
137
+ return slashIndex === -1
138
+ ? ""
139
+ : output.slice(slashIndex).replace(/^\/+/g, "");
140
+ };
141
+
142
+ const basePath = getBasePathFromOutput(outputDir, docPath);
131
143
 
132
144
  const createDocItemFnContext = {
133
145
  sidebarOptions,
@@ -5,7 +5,7 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  * ========================================================================== */
7
7
 
8
- import { SidebarItemDoc } from "@docusaurus/plugin-content-docs/src/sidebars/types";
8
+ import type { SidebarItemDoc } from "@docusaurus/plugin-content-docs/lib/sidebars/types";
9
9
  import Request from "postman-collection";
10
10
 
11
11
  import {
@@ -21,7 +21,7 @@ export type {
21
21
  SidebarItemLink,
22
22
  PropSidebar,
23
23
  PropSidebarItem,
24
- } from "@docusaurus/plugin-content-docs-types";
24
+ } from "@docusaurus/plugin-content-docs/lib/sidebars/types";
25
25
  export interface PluginOptions {
26
26
  id?: string;
27
27
  docsPlugin?: string;
@@ -51,7 +51,17 @@ export interface APIOptions {
51
51
  proxy?: string;
52
52
  markdownGenerators?: MarkdownGenerator;
53
53
  showSchemas?: boolean;
54
+ showInfoPage?: boolean;
55
+ schemasOnly?: boolean;
54
56
  disableCompression?: boolean;
57
+ maskCredentials?: boolean;
58
+ /**
59
+ * When enabled, large JSON props in generated MDX are written to external
60
+ * files and loaded via require(). This can significantly improve MDX
61
+ * compilation performance for large OpenAPI specs.
62
+ * @see https://github.com/facebook/docusaurus/discussions/11664
63
+ */
64
+ externalJsonProps?: boolean;
55
65
  }
56
66
 
57
67
  export interface MarkdownGenerator {