docusaurus-plugin-openapi-docs 0.0.0-1089 → 0.0.0-1092

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/README.md CHANGED
@@ -41,7 +41,7 @@ Key Features:
41
41
 
42
42
  | Docusaurus OpenAPI Docs | Docusaurus |
43
43
  | ----------------------- | --------------- |
44
- | 4.0.x (current) | `3.5.0 - 3.8.1` |
44
+ | 4.x.x (current) | `3.5.0 - 3.9.2` |
45
45
  | 3.0.x (end-of-support) | `3.0.1 - 3.4.0` |
46
46
  | 2.2.3 (legacy) | `2.4.1 - 2.4.3` |
47
47
  | 1.7.3 (end-of-support) | `2.0.1 - 2.2.0` |
@@ -51,7 +51,7 @@ Key Features:
51
51
  Run the following to bootstrap a Docusaurus v3 site (classic theme) with `docusaurus-openapi-docs`:
52
52
 
53
53
  ```bash
54
- npx create-docusaurus@3.8.1 my-website --package-manager yarn
54
+ npx create-docusaurus@3.9.2 my-website --package-manager yarn
55
55
  ```
56
56
 
57
57
  > When prompted to select a template choose `Git repository`.
@@ -156,28 +156,29 @@ The `docusaurus-plugin-openapi-docs` plugin can be configured with the following
156
156
 
157
157
  `config` can be configured with the following options:
158
158
 
159
- | Name | Type | Default | Description |
160
- | -------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
161
- | `specPath` | `string` | `null` | Designated URL or path to the source of an OpenAPI specification file or directory of multiple OpenAPI specification files. |
162
- | `outputDir` | `string` | `null` | Desired output path for generated MDX and sidebar files. |
163
- | `proxy` | `string` | `null` | _Optional:_ Proxy URL to prepend to base URL when performing API requests from browser. Overrides site-wide `themeConfig.api.proxy` if set. |
164
- | `template` | `string` | `null` | _Optional:_ Customize MDX content with a desired template. |
165
- | `infoTemplate` | `string` | `null` | _Optional:_ Customize MDX content for **info** pages only. |
166
- | `tagTemplate` | `string` | `null` | _Optional:_ Customize MDX content for **tag** pages only. |
167
- | `schemaTemplate` | `string` | `null` | _Optional:_ Customize MDX content for **schema** pages only. |
168
- | `downloadUrl` | `string` | `null` | _Optional:_ Designated URL for downloading OpenAPI specification. (requires `info` section/doc) |
169
- | `hideSendButton` | `boolean` | `null` | _Optional:_ If set to `true`, hides the “Send API Request” button in the API demo panel. |
170
- | `showExtensions` | `boolean` | `null` | _Optional:_ If set to `true`, renders operation‑level vendor‑extensions in descriptions. |
171
- | `maskCredentials` | `boolean` | `true` | _Optional:_ If set to `false`, disables credential masking in generated code snippets. By default, credentials are masked for security. |
172
- | `sidebarOptions` | `object` | `null` | _Optional:_ Set of options for sidebar configuration. See below for a list of supported options. |
173
- | `version` | `string` | `null` | _Optional:_ Version assigned to a single or micro‑spec API specified in `specPath`. |
174
- | `label` | `string` | `null` | _Optional:_ Version label used when generating the version‑selector dropdown menu. |
175
- | `baseUrl` | `string` | `null` | _Optional:_ Base URL for versioned docs in the version‑selector dropdown. |
176
- | `versions` | `object` | `null` | _Optional:_ Options for versioning configuration. See below for a list of supported options. |
177
- | `markdownGenerators` | `object` | `null` | _Optional:_ Customize MDX content via generator functions. See below for a list of supported options. |
178
- | `showSchemas` | `boolean` | `null` | _Optional:_ If set to `true`, generates standalone schema pages and adds them to the sidebar. |
179
- | `showInfoPage` | `boolean` | `true` | _Optional:_ If set to `false`, disables generation of the info page (overview page with API title and description). |
180
- | `schemasOnly` | `boolean` | `false` | _Optional:_ If set to `true`, generates only schema pages (no API endpoint pages). Also available as `--schemas-only` CLI flag. |
159
+ | Name | Type | Default | Description |
160
+ | -------------------- | --------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
161
+ | `specPath` | `string` | `null` | Designated URL or path to the source of an OpenAPI specification file or directory of multiple OpenAPI specification files. |
162
+ | `outputDir` | `string` | `null` | Desired output path for generated MDX and sidebar files. |
163
+ | `proxy` | `string` | `null` | _Optional:_ Proxy URL to prepend to base URL when performing API requests from browser. Overrides site-wide `themeConfig.api.proxy` if set. |
164
+ | `template` | `string` | `null` | _Optional:_ Customize MDX content with a desired template. |
165
+ | `infoTemplate` | `string` | `null` | _Optional:_ Customize MDX content for **info** pages only. |
166
+ | `tagTemplate` | `string` | `null` | _Optional:_ Customize MDX content for **tag** pages only. |
167
+ | `schemaTemplate` | `string` | `null` | _Optional:_ Customize MDX content for **schema** pages only. |
168
+ | `downloadUrl` | `string` | `null` | _Optional:_ Designated URL for downloading OpenAPI specification. (requires `info` section/doc) |
169
+ | `hideSendButton` | `boolean` | `null` | _Optional:_ If set to `true`, hides the “Send API Request” button in the API demo panel. |
170
+ | `showExtensions` | `boolean` | `null` | _Optional:_ If set to `true`, renders operation‑level vendor‑extensions in descriptions. |
171
+ | `maskCredentials` | `boolean` | `true` | _Optional:_ If set to `false`, disables credential masking in generated code snippets. By default, credentials are masked for security. |
172
+ | `sidebarOptions` | `object` | `null` | _Optional:_ Set of options for sidebar configuration. See below for a list of supported options. |
173
+ | `version` | `string` | `null` | _Optional:_ Version assigned to a single or micro‑spec API specified in `specPath`. |
174
+ | `label` | `string` | `null` | _Optional:_ Version label used when generating the version‑selector dropdown menu. |
175
+ | `baseUrl` | `string` | `null` | _Optional:_ Base URL for versioned docs in the version‑selector dropdown. |
176
+ | `versions` | `object` | `null` | _Optional:_ Options for versioning configuration. See below for a list of supported options. |
177
+ | `markdownGenerators` | `object` | `null` | _Optional:_ Customize MDX content via generator functions. See below for a list of supported options. |
178
+ | `showSchemas` | `boolean` | `null` | _Optional:_ If set to `true`, generates standalone schema pages and adds them to the sidebar. |
179
+ | `showInfoPage` | `boolean` | `true` | _Optional:_ If set to `false`, disables generation of the info page (overview page with API title and description). |
180
+ | `schemasOnly` | `boolean` | `false` | _Optional:_ If set to `true`, generates only schema pages (no API endpoint pages). Also available as `--schemas-only` CLI flag. |
181
+ | `externalJsonProps` | `boolean` | `true` | _Optional:_ If set to `false`, disables externalization of large JSON props. By default, large JSON is written to external files for better build performance. |
181
182
 
182
183
  ### sidebarOptions
183
184
 
@@ -226,6 +227,28 @@ The `docusaurus-plugin-openapi-docs` plugin can be configured with the following
226
227
  | --------------- | ---------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
227
228
  | `createDocItem` | `function` | `null` | Optional: Returns a `SidebarItemDoc` object containing metadata for a sidebar item.<br/><br/>**Function type:** `(item: ApiPageMetadata \| SchemaPageMetadata, context: { sidebarOptions: SidebarOptions; basePath: string }) => SidebarItemDoc` |
228
229
 
230
+ ### Performance Optimization
231
+
232
+ By default, `externalJsonProps` is enabled to optimize Docusaurus build times. This externalizes large JSON objects to separate files, significantly improving build performance for large OpenAPI specs.
233
+
234
+ **How it works:**
235
+
236
+ - With `externalJsonProps: true` (default): Large JSON props are written to separate `.json` files and loaded via `require()`. This bypasses MDX AST processing entirely, allowing the bundler to handle JSON more efficiently.
237
+
238
+ - With `externalJsonProps: false`: Large JSON objects (responses, request bodies, parameters) are embedded directly in the MDX. The MDX compiler must parse these into an AST and serialize them back, which can be slow for deeply nested schemas.
239
+
240
+ **When to disable:**
241
+
242
+ You may set `externalJsonProps: false` if you have custom tooling that depends on parsing the MDX files directly:
243
+
244
+ ```typescript
245
+ petstore: {
246
+ specPath: "examples/petstore.yaml",
247
+ outputDir: "docs/petstore",
248
+ externalJsonProps: false, // Disable if needed for custom tooling
249
+ } satisfies OpenApiPlugin.Options,
250
+ ```
251
+
229
252
  ## Theme Configuration Options
230
253
 
231
254
  The `docusaurus-theme-openapi-docs` theme can be configured with the following options in `themeConfig.api`:
package/lib/index.js CHANGED
@@ -20,6 +20,7 @@ const chalk_1 = __importDefault(require("chalk"));
20
20
  const json5_1 = __importDefault(require("json5"));
21
21
  const mustache_1 = require("mustache");
22
22
  const markdown_1 = require("./markdown");
23
+ const externalizeJsonProps_1 = require("./markdown/externalizeJsonProps");
23
24
  const openapi_1 = require("./openapi");
24
25
  const options_1 = require("./options");
25
26
  const sidebars_1 = __importDefault(require("./sidebars"));
@@ -281,7 +282,23 @@ custom_edit_url: null
281
282
  if (item.id.length === 0) {
282
283
  throw Error("Operation must have summary or operationId defined");
283
284
  }
284
- fs_1.default.writeFileSync(`${outputDir}/${item.id}.api.mdx`, view, "utf8");
285
+ let finalView = view;
286
+ let jsonFilesToWrite = [];
287
+ // Externalize large JSON props if enabled
288
+ if (options.externalJsonProps) {
289
+ const result = (0, externalizeJsonProps_1.externalizeJsonPropsSimple)(view, item.id);
290
+ finalView = result.mdx;
291
+ jsonFilesToWrite = result.jsonFiles;
292
+ // Write JSON files
293
+ for (const jsonFile of jsonFilesToWrite) {
294
+ const jsonPath = `${outputDir}/${jsonFile.filename}`;
295
+ if (!fs_1.default.existsSync(jsonPath)) {
296
+ fs_1.default.writeFileSync(jsonPath, jsonFile.content, "utf8");
297
+ console.log(chalk_1.default.green(`Successfully created "${jsonPath}"`));
298
+ }
299
+ }
300
+ }
301
+ fs_1.default.writeFileSync(`${outputDir}/${item.id}.api.mdx`, finalView, "utf8");
285
302
  console.log(chalk_1.default.green(`Successfully created "${outputDir}/${item.id}.api.mdx"`));
286
303
  }
287
304
  catch (err) {
@@ -330,7 +347,22 @@ custom_edit_url: null
330
347
  throw Error("Schema must have title defined");
331
348
  }
332
349
  // eslint-disable-next-line testing-library/render-result-naming-convention
333
- const schemaView = (0, mustache_1.render)(schemaMdTemplate, item);
350
+ let schemaView = (0, mustache_1.render)(schemaMdTemplate, item);
351
+ let jsonFilesToWrite = [];
352
+ // Externalize large JSON props if enabled
353
+ if (options.externalJsonProps) {
354
+ const result = (0, externalizeJsonProps_1.externalizeJsonPropsSimple)(schemaView, item.id);
355
+ schemaView = result.mdx;
356
+ jsonFilesToWrite = result.jsonFiles;
357
+ // Write JSON files in schemas directory
358
+ for (const jsonFile of jsonFilesToWrite) {
359
+ const jsonPath = `${outputDir}/schemas/${jsonFile.filename}`;
360
+ if (!fs_1.default.existsSync(jsonPath)) {
361
+ fs_1.default.writeFileSync(jsonPath, jsonFile.content, "utf8");
362
+ console.log(chalk_1.default.green(`Successfully created "${jsonPath}"`));
363
+ }
364
+ }
365
+ }
334
366
  fs_1.default.writeFileSync(`${outputDir}/schemas/${item.id}.schema.mdx`, schemaView, "utf8");
335
367
  console.log(chalk_1.default.green(`Successfully created "${outputDir}/${item.id}.schema.mdx"`));
336
368
  }
@@ -361,6 +393,11 @@ custom_edit_url: null
361
393
  cwd: path_1.default.resolve(apiDir),
362
394
  deep: 1,
363
395
  });
396
+ // Clean up externalized JSON files
397
+ const jsonFiles = await (0, utils_1.Globby)(["*.json", "!versions.json"], {
398
+ cwd: path_1.default.resolve(apiDir),
399
+ deep: 1,
400
+ });
364
401
  apiMdxFiles.map((mdx) => fs_1.default.unlink(`${apiDir}/${mdx}`, (err) => {
365
402
  if (err) {
366
403
  console.error(chalk_1.default.red(`Cleanup failed for "${apiDir}/${mdx}"`), chalk_1.default.yellow(err));
@@ -377,6 +414,15 @@ custom_edit_url: null
377
414
  console.log(chalk_1.default.green(`Cleanup succeeded for "${apiDir}/${sidebar}"`));
378
415
  }
379
416
  }));
417
+ // Clean up externalized JSON files
418
+ jsonFiles.map((jsonFile) => fs_1.default.unlink(`${apiDir}/${jsonFile}`, (err) => {
419
+ if (err) {
420
+ console.error(chalk_1.default.red(`Cleanup failed for "${apiDir}/${jsonFile}"`), chalk_1.default.yellow(err));
421
+ }
422
+ else {
423
+ console.log(chalk_1.default.green(`Cleanup succeeded for "${apiDir}/${jsonFile}"`));
424
+ }
425
+ }));
380
426
  }
381
427
  try {
382
428
  fs_1.default.rmSync(`${apiDir}/schemas`, { recursive: true });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * This module provides utilities for externalizing large JSON props in MDX files.
3
+ *
4
+ * The motivation is to improve MDX compilation performance. When large JSON objects
5
+ * are embedded directly as JSX props, MDX needs to parse them into AST and then
6
+ * serialize them back. By externalizing to JSON files and using require(), the
7
+ * MDX compiler can skip this expensive processing.
8
+ *
9
+ * @see https://github.com/facebook/docusaurus/discussions/11664
10
+ */
11
+ export interface ExternalizedJsonFile {
12
+ /** The filename for the JSON file (without path) */
13
+ filename: string;
14
+ /** The JSON content to write */
15
+ content: string;
16
+ }
17
+ export interface ExternalizeResult {
18
+ /** The transformed MDX content with require() statements */
19
+ mdx: string;
20
+ /** List of JSON files that need to be written */
21
+ jsonFiles: ExternalizedJsonFile[];
22
+ }
23
+ /**
24
+ * Externalizes large JSON props from MDX content.
25
+ * Uses brace-counting to correctly handle deeply nested JSON.
26
+ *
27
+ * @param mdx - The original MDX content
28
+ * @param baseFilename - The base filename (without extension) for the MDX file
29
+ * @returns The transformed MDX and list of JSON files to write
30
+ */
31
+ export declare function externalizeJsonPropsSimple(mdx: string, baseFilename: string): ExternalizeResult;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ /* ============================================================================
3
+ * Copyright (c) Palo Alto Networks
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ * ========================================================================== */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.externalizeJsonPropsSimple = externalizeJsonPropsSimple;
10
+ /**
11
+ * Components and their props that should be externalized.
12
+ * These are the components that typically receive large JSON objects.
13
+ */
14
+ const COMPONENTS_TO_EXTERNALIZE = [
15
+ { component: "StatusCodes", prop: "responses" },
16
+ { component: "ParamsDetails", prop: "parameters" },
17
+ { component: "RequestSchema", prop: "body" },
18
+ { component: "Schema", prop: "schema" },
19
+ { component: "SchemaItem", prop: "schema" },
20
+ ];
21
+ /**
22
+ * Minimum size (in characters) for a JSON prop to be externalized.
23
+ * Props smaller than this threshold will remain inline.
24
+ */
25
+ const MIN_SIZE_THRESHOLD = 500;
26
+ /**
27
+ * Extracts the content between balanced braces starting at a given position.
28
+ * Returns the content (without outer braces) and the end position.
29
+ */
30
+ function extractBalancedBraces(str, startIndex) {
31
+ if (str[startIndex] !== "{") {
32
+ return null;
33
+ }
34
+ let depth = 0;
35
+ let inString = false;
36
+ let stringChar = "";
37
+ let escaped = false;
38
+ for (let i = startIndex; i < str.length; i++) {
39
+ const char = str[i];
40
+ if (escaped) {
41
+ escaped = false;
42
+ continue;
43
+ }
44
+ if (char === "\\") {
45
+ escaped = true;
46
+ continue;
47
+ }
48
+ if (!inString) {
49
+ if (char === '"' || char === "'") {
50
+ inString = true;
51
+ stringChar = char;
52
+ }
53
+ else if (char === "{") {
54
+ depth++;
55
+ }
56
+ else if (char === "}") {
57
+ depth--;
58
+ if (depth === 0) {
59
+ // Found the matching closing brace
60
+ return {
61
+ content: str.substring(startIndex + 1, i),
62
+ endIndex: i,
63
+ };
64
+ }
65
+ }
66
+ }
67
+ else {
68
+ if (char === stringChar) {
69
+ inString = false;
70
+ }
71
+ }
72
+ }
73
+ return null; // Unbalanced braces
74
+ }
75
+ /**
76
+ * Externalizes large JSON props from MDX content.
77
+ * Uses brace-counting to correctly handle deeply nested JSON.
78
+ *
79
+ * @param mdx - The original MDX content
80
+ * @param baseFilename - The base filename (without extension) for the MDX file
81
+ * @returns The transformed MDX and list of JSON files to write
82
+ */
83
+ function externalizeJsonPropsSimple(mdx, baseFilename) {
84
+ const jsonFiles = [];
85
+ let transformedMdx = mdx;
86
+ for (const { component, prop } of COMPONENTS_TO_EXTERNALIZE) {
87
+ // Find pattern: prop={
88
+ const propPattern = new RegExp(`${prop}=\\{`, "g");
89
+ let match;
90
+ // Keep searching until no more matches (need to restart after each replacement)
91
+ let searchStart = 0;
92
+ while (true) {
93
+ propPattern.lastIndex = searchStart;
94
+ match = propPattern.exec(transformedMdx);
95
+ if (!match)
96
+ break;
97
+ const propStart = match.index;
98
+ const braceStart = propStart + prop.length + 1; // Position of '{'
99
+ // Extract the balanced content
100
+ const extracted = extractBalancedBraces(transformedMdx, braceStart);
101
+ if (!extracted) {
102
+ searchStart = braceStart + 1;
103
+ continue;
104
+ }
105
+ const jsonContent = extracted.content;
106
+ const propEnd = extracted.endIndex + 1; // Position after '}'
107
+ // Skip small or non-JSON content
108
+ const trimmed = jsonContent.trim();
109
+ if (jsonContent.length < MIN_SIZE_THRESHOLD ||
110
+ trimmed === "undefined" ||
111
+ trimmed === "null" ||
112
+ trimmed === "true" ||
113
+ trimmed === "false" ||
114
+ trimmed.startsWith("require(")) {
115
+ searchStart = propEnd;
116
+ continue;
117
+ }
118
+ // Check if this is within the target component
119
+ // Look backwards for the component tag
120
+ const beforeProp = transformedMdx.substring(0, propStart);
121
+ const componentTagPattern = new RegExp(`<${component}[\\s\\S]*$`);
122
+ if (!componentTagPattern.test(beforeProp)) {
123
+ searchStart = propEnd;
124
+ continue;
125
+ }
126
+ // Generate filename
127
+ const existingCount = jsonFiles.filter((f) => f.filename.includes(`.${prop}`)).length;
128
+ const suffix = existingCount > 0 ? `.${existingCount + 1}` : "";
129
+ const jsonFilename = `${baseFilename}.${prop}${suffix}.json`;
130
+ // Store the JSON file
131
+ jsonFiles.push({
132
+ filename: jsonFilename,
133
+ content: jsonContent.trim(),
134
+ });
135
+ // Replace the prop value with require()
136
+ const newProp = `${prop}={require("./${jsonFilename}")}`;
137
+ transformedMdx =
138
+ transformedMdx.substring(0, propStart) +
139
+ newProp +
140
+ transformedMdx.substring(propEnd);
141
+ // Continue searching after the replacement
142
+ searchStart = propStart + newProp.length;
143
+ }
144
+ }
145
+ return {
146
+ mdx: transformedMdx,
147
+ jsonFiles,
148
+ };
149
+ }
package/lib/options.js CHANGED
@@ -48,6 +48,7 @@ exports.OptionsSchema = utils_validation_1.Joi.object({
48
48
  schemasOnly: utils_validation_1.Joi.boolean(),
49
49
  disableCompression: utils_validation_1.Joi.boolean(),
50
50
  maskCredentials: utils_validation_1.Joi.boolean(),
51
+ externalJsonProps: utils_validation_1.Joi.boolean().default(true),
51
52
  version: utils_validation_1.Joi.string().when("versions", {
52
53
  is: utils_validation_1.Joi.exist(),
53
54
  then: utils_validation_1.Joi.required(),
package/lib/types.d.ts CHANGED
@@ -33,6 +33,13 @@ export interface APIOptions {
33
33
  schemasOnly?: boolean;
34
34
  disableCompression?: boolean;
35
35
  maskCredentials?: boolean;
36
+ /**
37
+ * When enabled, large JSON props in generated MDX are written to external
38
+ * files and loaded via require(). This can significantly improve MDX
39
+ * compilation performance for large OpenAPI specs.
40
+ * @see https://github.com/facebook/docusaurus/discussions/11664
41
+ */
42
+ externalJsonProps?: boolean;
36
43
  }
37
44
  export interface MarkdownGenerator {
38
45
  createApiPageMD?: (pageData: ApiPageMetadata) => string;
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-1089",
4
+ "version": "0.0.0-1092",
5
5
  "license": "MIT",
6
6
  "keywords": [
7
7
  "openapi",
@@ -65,5 +65,5 @@
65
65
  "engines": {
66
66
  "node": ">=14"
67
67
  },
68
- "gitHead": "c3a9819571e1ca0aa03c26f93332b7ec71acc857"
68
+ "gitHead": "52285283e2fc0ee02b7f5e42a9678a3bb89c095b"
69
69
  }
package/src/index.ts CHANGED
@@ -21,6 +21,10 @@ import {
21
21
  createSchemaPageMD,
22
22
  createTagPageMD,
23
23
  } from "./markdown";
24
+ import {
25
+ externalizeJsonPropsSimple,
26
+ ExternalizedJsonFile,
27
+ } from "./markdown/externalizeJsonProps";
24
28
  import { processOpenapiFiles, readOpenapiFiles } from "./openapi";
25
29
  import { OptionsSchema } from "./options";
26
30
  import generateSidebarSlice from "./sidebars";
@@ -383,7 +387,33 @@ custom_edit_url: null
383
387
  "Operation must have summary or operationId defined"
384
388
  );
385
389
  }
386
- fs.writeFileSync(`${outputDir}/${item.id}.api.mdx`, view, "utf8");
390
+
391
+ let finalView = view;
392
+ let jsonFilesToWrite: ExternalizedJsonFile[] = [];
393
+
394
+ // Externalize large JSON props if enabled
395
+ if (options.externalJsonProps) {
396
+ const result = externalizeJsonPropsSimple(view, item.id);
397
+ finalView = result.mdx;
398
+ jsonFilesToWrite = result.jsonFiles;
399
+
400
+ // Write JSON files
401
+ for (const jsonFile of jsonFilesToWrite) {
402
+ const jsonPath = `${outputDir}/${jsonFile.filename}`;
403
+ if (!fs.existsSync(jsonPath)) {
404
+ fs.writeFileSync(jsonPath, jsonFile.content, "utf8");
405
+ console.log(
406
+ chalk.green(`Successfully created "${jsonPath}"`)
407
+ );
408
+ }
409
+ }
410
+ }
411
+
412
+ fs.writeFileSync(
413
+ `${outputDir}/${item.id}.api.mdx`,
414
+ finalView,
415
+ "utf8"
416
+ );
387
417
  console.log(
388
418
  chalk.green(
389
419
  `Successfully created "${outputDir}/${item.id}.api.mdx"`
@@ -469,7 +499,27 @@ custom_edit_url: null
469
499
  throw Error("Schema must have title defined");
470
500
  }
471
501
  // eslint-disable-next-line testing-library/render-result-naming-convention
472
- const schemaView = render(schemaMdTemplate, item);
502
+ let schemaView = render(schemaMdTemplate, item);
503
+ let jsonFilesToWrite: ExternalizedJsonFile[] = [];
504
+
505
+ // Externalize large JSON props if enabled
506
+ if (options.externalJsonProps) {
507
+ const result = externalizeJsonPropsSimple(schemaView, item.id);
508
+ schemaView = result.mdx;
509
+ jsonFilesToWrite = result.jsonFiles;
510
+
511
+ // Write JSON files in schemas directory
512
+ for (const jsonFile of jsonFilesToWrite) {
513
+ const jsonPath = `${outputDir}/schemas/${jsonFile.filename}`;
514
+ if (!fs.existsSync(jsonPath)) {
515
+ fs.writeFileSync(jsonPath, jsonFile.content, "utf8");
516
+ console.log(
517
+ chalk.green(`Successfully created "${jsonPath}"`)
518
+ );
519
+ }
520
+ }
521
+ }
522
+
473
523
  fs.writeFileSync(
474
524
  `${outputDir}/schemas/${item.id}.schema.mdx`,
475
525
  schemaView,
@@ -517,6 +567,11 @@ custom_edit_url: null
517
567
  cwd: path.resolve(apiDir),
518
568
  deep: 1,
519
569
  });
570
+ // Clean up externalized JSON files
571
+ const jsonFiles = await Globby(["*.json", "!versions.json"], {
572
+ cwd: path.resolve(apiDir),
573
+ deep: 1,
574
+ });
520
575
  apiMdxFiles.map((mdx) =>
521
576
  fs.unlink(`${apiDir}/${mdx}`, (err) => {
522
577
  if (err) {
@@ -546,6 +601,22 @@ custom_edit_url: null
546
601
  }
547
602
  })
548
603
  );
604
+
605
+ // Clean up externalized JSON files
606
+ jsonFiles.map((jsonFile) =>
607
+ fs.unlink(`${apiDir}/${jsonFile}`, (err) => {
608
+ if (err) {
609
+ console.error(
610
+ chalk.red(`Cleanup failed for "${apiDir}/${jsonFile}"`),
611
+ chalk.yellow(err)
612
+ );
613
+ } else {
614
+ console.log(
615
+ chalk.green(`Cleanup succeeded for "${apiDir}/${jsonFile}"`)
616
+ );
617
+ }
618
+ })
619
+ );
549
620
  }
550
621
 
551
622
  try {
@@ -0,0 +1,201 @@
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
+ /**
9
+ * This module provides utilities for externalizing large JSON props in MDX files.
10
+ *
11
+ * The motivation is to improve MDX compilation performance. When large JSON objects
12
+ * are embedded directly as JSX props, MDX needs to parse them into AST and then
13
+ * serialize them back. By externalizing to JSON files and using require(), the
14
+ * MDX compiler can skip this expensive processing.
15
+ *
16
+ * @see https://github.com/facebook/docusaurus/discussions/11664
17
+ */
18
+
19
+ export interface ExternalizedJsonFile {
20
+ /** The filename for the JSON file (without path) */
21
+ filename: string;
22
+ /** The JSON content to write */
23
+ content: string;
24
+ }
25
+
26
+ export interface ExternalizeResult {
27
+ /** The transformed MDX content with require() statements */
28
+ mdx: string;
29
+ /** List of JSON files that need to be written */
30
+ jsonFiles: ExternalizedJsonFile[];
31
+ }
32
+
33
+ /**
34
+ * Components and their props that should be externalized.
35
+ * These are the components that typically receive large JSON objects.
36
+ */
37
+ const COMPONENTS_TO_EXTERNALIZE = [
38
+ { component: "StatusCodes", prop: "responses" },
39
+ { component: "ParamsDetails", prop: "parameters" },
40
+ { component: "RequestSchema", prop: "body" },
41
+ { component: "Schema", prop: "schema" },
42
+ { component: "SchemaItem", prop: "schema" },
43
+ ];
44
+
45
+ /**
46
+ * Minimum size (in characters) for a JSON prop to be externalized.
47
+ * Props smaller than this threshold will remain inline.
48
+ */
49
+ const MIN_SIZE_THRESHOLD = 500;
50
+
51
+ /**
52
+ * Extracts the content between balanced braces starting at a given position.
53
+ * Returns the content (without outer braces) and the end position.
54
+ */
55
+ function extractBalancedBraces(
56
+ str: string,
57
+ startIndex: number
58
+ ): { content: string; endIndex: number } | null {
59
+ if (str[startIndex] !== "{") {
60
+ return null;
61
+ }
62
+
63
+ let depth = 0;
64
+ let inString = false;
65
+ let stringChar = "";
66
+ let escaped = false;
67
+
68
+ for (let i = startIndex; i < str.length; i++) {
69
+ const char = str[i];
70
+
71
+ if (escaped) {
72
+ escaped = false;
73
+ continue;
74
+ }
75
+
76
+ if (char === "\\") {
77
+ escaped = true;
78
+ continue;
79
+ }
80
+
81
+ if (!inString) {
82
+ if (char === '"' || char === "'") {
83
+ inString = true;
84
+ stringChar = char;
85
+ } else if (char === "{") {
86
+ depth++;
87
+ } else if (char === "}") {
88
+ depth--;
89
+ if (depth === 0) {
90
+ // Found the matching closing brace
91
+ return {
92
+ content: str.substring(startIndex + 1, i),
93
+ endIndex: i,
94
+ };
95
+ }
96
+ }
97
+ } else {
98
+ if (char === stringChar) {
99
+ inString = false;
100
+ }
101
+ }
102
+ }
103
+
104
+ return null; // Unbalanced braces
105
+ }
106
+
107
+ /**
108
+ * Externalizes large JSON props from MDX content.
109
+ * Uses brace-counting to correctly handle deeply nested JSON.
110
+ *
111
+ * @param mdx - The original MDX content
112
+ * @param baseFilename - The base filename (without extension) for the MDX file
113
+ * @returns The transformed MDX and list of JSON files to write
114
+ */
115
+ export function externalizeJsonPropsSimple(
116
+ mdx: string,
117
+ baseFilename: string
118
+ ): ExternalizeResult {
119
+ const jsonFiles: ExternalizedJsonFile[] = [];
120
+ let transformedMdx = mdx;
121
+
122
+ for (const { component, prop } of COMPONENTS_TO_EXTERNALIZE) {
123
+ // Find pattern: prop={
124
+ const propPattern = new RegExp(`${prop}=\\{`, "g");
125
+ let match;
126
+
127
+ // Keep searching until no more matches (need to restart after each replacement)
128
+ let searchStart = 0;
129
+ while (true) {
130
+ propPattern.lastIndex = searchStart;
131
+ match = propPattern.exec(transformedMdx);
132
+
133
+ if (!match) break;
134
+
135
+ const propStart = match.index;
136
+ const braceStart = propStart + prop.length + 1; // Position of '{'
137
+
138
+ // Extract the balanced content
139
+ const extracted = extractBalancedBraces(transformedMdx, braceStart);
140
+ if (!extracted) {
141
+ searchStart = braceStart + 1;
142
+ continue;
143
+ }
144
+
145
+ const jsonContent = extracted.content;
146
+ const propEnd = extracted.endIndex + 1; // Position after '}'
147
+
148
+ // Skip small or non-JSON content
149
+ const trimmed = jsonContent.trim();
150
+ if (
151
+ jsonContent.length < MIN_SIZE_THRESHOLD ||
152
+ trimmed === "undefined" ||
153
+ trimmed === "null" ||
154
+ trimmed === "true" ||
155
+ trimmed === "false" ||
156
+ trimmed.startsWith("require(")
157
+ ) {
158
+ searchStart = propEnd;
159
+ continue;
160
+ }
161
+
162
+ // Check if this is within the target component
163
+ // Look backwards for the component tag
164
+ const beforeProp = transformedMdx.substring(0, propStart);
165
+ const componentTagPattern = new RegExp(`<${component}[\\s\\S]*$`);
166
+ if (!componentTagPattern.test(beforeProp)) {
167
+ searchStart = propEnd;
168
+ continue;
169
+ }
170
+
171
+ // Generate filename
172
+ const existingCount = jsonFiles.filter((f) =>
173
+ f.filename.includes(`.${prop}`)
174
+ ).length;
175
+ const suffix = existingCount > 0 ? `.${existingCount + 1}` : "";
176
+ const jsonFilename = `${baseFilename}.${prop}${suffix}.json`;
177
+
178
+ // Store the JSON file
179
+ jsonFiles.push({
180
+ filename: jsonFilename,
181
+ content: jsonContent.trim(),
182
+ });
183
+
184
+ // Replace the prop value with require()
185
+ const newProp = `${prop}={require("./${jsonFilename}")}`;
186
+
187
+ transformedMdx =
188
+ transformedMdx.substring(0, propStart) +
189
+ newProp +
190
+ transformedMdx.substring(propEnd);
191
+
192
+ // Continue searching after the replacement
193
+ searchStart = propStart + newProp.length;
194
+ }
195
+ }
196
+
197
+ return {
198
+ mdx: transformedMdx,
199
+ jsonFiles,
200
+ };
201
+ }
package/src/options.ts CHANGED
@@ -52,6 +52,7 @@ export const OptionsSchema = Joi.object({
52
52
  schemasOnly: Joi.boolean(),
53
53
  disableCompression: Joi.boolean(),
54
54
  maskCredentials: Joi.boolean(),
55
+ externalJsonProps: Joi.boolean().default(true),
55
56
  version: Joi.string().when("versions", {
56
57
  is: Joi.exist(),
57
58
  then: Joi.required(),
package/src/types.ts CHANGED
@@ -55,6 +55,13 @@ export interface APIOptions {
55
55
  schemasOnly?: boolean;
56
56
  disableCompression?: boolean;
57
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;
58
65
  }
59
66
 
60
67
  export interface MarkdownGenerator {