@matfsw/docusaurus-plantuml-plugin 1.6.2 → 1.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,49 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.7.0] - 2026-09-02
11
+
12
+ ### Added
13
+
14
+ - **Support for PlantUML diagrams up to 32768 points tall or wide, instead of 4096.** The
15
+ bundled `@plantuml/core` refuses to serialize anything larger, reporting
16
+ `Diagram too large for browser rendering: 278x5667 (max 4096)` — about forty stacked class
17
+ boxes, which a real architecture diagram or release runbook passes without trying.
18
+
19
+ It is not an SVG or a browser limit, and there is no engine option behind it:
20
+ `renderToString` takes only `{dark}`. Nor is there a way around it from diagram source. The
21
+ check reads the *pre-scale* dimensions, so `scale` does nothing; `skinparam dpi` does not
22
+ move it; `left to right direction` only swaps which of the two numbers is too large. Only
23
+ shrinking the layout by hand worked, which for a large diagram means drawing a different
24
+ diagram.
25
+
26
+ The plugin now rewrites that ceiling in the engine it serves. Diagrams that previously
27
+ showed an error panel render normally, in both dimensions. The rewrite is two literal
28
+ replacements whose occurrence counts are verified before either is applied, so a future
29
+ `@plantuml/core` that changes shape fails the build naming the version rather than silently
30
+ going unpatched — and the unit suite runs the patcher against the installed engine, so a
31
+ dependency bump catches it here first. See
32
+ [ADR 0007](docs/adr/0007-engine-size-ceiling-patch.md).
33
+
34
+ ### Changed
35
+
36
+ - **The runtime assets moved to `assets/plantuml-client-<coreVersion>-max32768/`.** What is
37
+ served is a patched engine, so a reader holding a cached copy of the unpatched one must not
38
+ be handed it from the old URL. The standard library is nested inside that directory and is
39
+ therefore re-downloaded once. No configuration changes.
40
+ - **A very large diagram is no longer refused quickly.** It renders instead, but the resulting
41
+ SVG is parsed synchronously twice on its way in — once to detect engine error pictures, once
42
+ by the sanitizer — so a diagram of many thousands of points can stall the tab briefly and may
43
+ need `renderTimeoutMs` raised above its 20 s default. A very wide diagram is scaled down to
44
+ the content column, which keeps the page layout intact but can leave it too small to read
45
+ until it is zoomed or maximized.
46
+
47
+ ## [1.6.3] - 2026-08-31
48
+
49
+ ### Changed
50
+
51
+ - Bump the all group across 1 directory with 6 updates (#35)
52
+
10
53
  ## [1.6.2] - 2026-08-24
11
54
 
12
55
  ### Changed
@@ -609,7 +652,9 @@ Initial release.
609
652
  opt-out.
610
653
  - Plugin option validation that rejects unknown keys and out-of-range values at build time.
611
654
 
612
- [Unreleased]: https://github.com/matf/docusaurus-plantuml-plugin/compare/v1.6.2...HEAD
655
+ [Unreleased]: https://github.com/matf/docusaurus-plantuml-plugin/compare/v1.7.0...HEAD
656
+ [1.7.0]: https://github.com/matf/docusaurus-plantuml-plugin/compare/v1.6.3...v1.7.0
657
+ [1.6.3]: https://github.com/matf/docusaurus-plantuml-plugin/compare/v1.6.2...v1.6.3
613
658
  [1.6.2]: https://github.com/matf/docusaurus-plantuml-plugin/compare/v1.6.1...v1.6.2
614
659
  [1.6.1]: https://github.com/matf/docusaurus-plantuml-plugin/compare/v1.6.0...v1.6.1
615
660
  [1.6.0]: https://github.com/matf/docusaurus-plantuml-plugin/compare/v1.5.2...v1.6.0
package/README.md CHANGED
@@ -1043,7 +1043,7 @@ Failures are classified internally as one of:
1043
1043
  | Kind | Cause |
1044
1044
  | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1045
1045
  | `load` | `viz-global.js` or `plantuml.js` could not be fetched, timed out, or was not the expected module (usually a proxy or service worker returning the wrong file). |
1046
- | `engine` | The engine threw, or its output could not be used at all. |
1046
+ | `engine` | The engine threw, or its output could not be used at all. Includes `Diagram too large for browser rendering: WxH (max 32768)` — see [Diagram size](#diagram-size). |
1047
1047
  | `diagram` | The source is not valid PlantUML. |
1048
1048
  | `syntax` | The source is not valid DOT. Carries Graphviz's own diagnostic, including the line number. |
1049
1049
  | `too-large` | A DOT source exceeded `graphviz.maxSourceBytes`. |
@@ -1065,25 +1065,61 @@ The two engines report invalid source very differently:
1065
1065
  - **Invalid DOT is reported as structured data**, so the panel shows Graphviz's own message
1066
1066
  verbatim — `syntax error in line 3 near '}'` — with no heuristics involved.
1067
1067
 
1068
+ ## Diagram size
1069
+
1070
+ `@plantuml/core` refuses to serialize a diagram wider or taller than a fixed number of
1071
+ PlantUML points, reporting it through its error callback:
1072
+
1073
+ ```text
1074
+ Diagram too large for browser rendering: 78x12916 (max 4096)
1075
+ ```
1076
+
1077
+ Upstream that limit is **4096**. This plugin patches the engine it serves and raises it to
1078
+ **32768**, so diagrams that the stock engine rejects render normally here. It is not
1079
+ configurable — one constant, applied on every build.
1080
+
1081
+ The limit is not something a diagram can talk its way around, which is why patching it was
1082
+ worth doing. Measured against the engine:
1083
+
1084
+ | Attempt | Effect |
1085
+ | ------------------------- | -------------------------------------------------------- |
1086
+ | `scale 0.3` | **none** — the check reads the pre-scale dimensions |
1087
+ | `skinparam dpi 40` | **none** — `dpi` affects raster output, not this measure |
1088
+ | `left to right direction` | none; it transposes width and height |
1089
+ | `skinparam ranksep 5` | works — it genuinely shrinks the layout |
1090
+
1091
+ If you still hit the ceiling at 32768, the levers that work are the ones that make the
1092
+ diagram smaller: fewer nodes and edges, tighter `skinparam ranksep`/`nodesep`, a smaller
1093
+ `skinparam defaultFontSize`, or splitting one diagram into several fences.
1094
+
1095
+ **A very large diagram is not free.** A 30 000-point diagram is a multi-hundred-KB SVG, and
1096
+ both the error detection and the DOMPurify sanitization parse it synchronously on the main
1097
+ thread. Such a diagram can visibly stall the tab while it lands, and may need
1098
+ `renderTimeoutMs` raised above its 20 s default. See
1099
+ [ADR 0007](docs/adr/0007-engine-size-ceiling-patch.md).
1100
+
1068
1101
  ## `baseUrl` support
1069
1102
 
1070
1103
  The runtime assets are emitted into
1071
1104
 
1072
1105
  ```text
1073
- <baseUrl>assets/plantuml-client-<coreVersion>/
1106
+ <baseUrl>assets/plantuml-client-<coreVersion>-max32768/
1074
1107
  ```
1075
1108
 
1076
1109
  and resolved in the browser through Docusaurus' own `useBaseUrl()`. A site deployed at
1077
1110
  `baseUrl: '/plantuml-test/'` therefore fetches:
1078
1111
 
1079
1112
  ```text
1080
- /plantuml-test/assets/plantuml-client-1.2026.6/viz-global.js
1081
- /plantuml-test/assets/plantuml-client-1.2026.6/plantuml.js
1113
+ /plantuml-test/assets/plantuml-client-1.2026.6-max32768/viz-global.js
1114
+ /plantuml-test/assets/plantuml-client-1.2026.6-max32768/plantuml.js
1082
1115
  ```
1083
1116
 
1084
1117
  No path is hard-coded to the site root, so project-pages deployments and reverse-proxied
1085
1118
  subpaths work without configuration. The engine version is part of the directory name, so an
1086
- upgrade changes the URL and stale caches are defeated automatically. The bundled example site
1119
+ upgrade changes the URL and stale caches are defeated automatically. The `-max32768` segment
1120
+ is part of the same identity: what is served is a _patched_ engine (see
1121
+ [Diagram size](#diagram-size)), and a reader holding a cached copy from a plugin version that
1122
+ patched differently must not be handed it from the same URL. The bundled example site
1087
1123
  deploys under a non-root `baseUrl` precisely so this path is exercised on every build.
1088
1124
 
1089
1125
  ## Browser compatibility
@@ -1430,6 +1466,16 @@ too and was rejected: it carries one person's account reach and expires on a cal
1430
1466
  - **One PlantUML diagram renders at a time.** The PlantUML engine has module-level shared
1431
1467
  state, so a page with many large diagrams renders them sequentially. This is a correctness
1432
1468
  requirement, not a tuning knob. Graphviz has no such constraint and is not queued.
1469
+ - **PlantUML diagrams are capped at 32768 points in each dimension.** The bundled engine has
1470
+ a hard ceiling with no option behind it; the plugin patches it up from the stock 4096, but
1471
+ it cannot be removed or configured. Past that, the diagram itself has to get smaller — see
1472
+ [Diagram size](#diagram-size). The raised ceiling also means a genuinely enormous diagram
1473
+ now renders instead of failing fast, and both the error detection and the sanitizer parse
1474
+ the resulting SVG synchronously, so it can stall the tab on its way in.
1475
+ - **A `@plantuml/core` release that changes the shape of that ceiling fails the build.** The
1476
+ patch is anchored on two literals whose occurrence counts are verified before anything is
1477
+ rewritten, and the plugin refuses to guess. The unit suite runs the patcher against the
1478
+ installed engine, so a dependency bump that broke it goes red here rather than in your build.
1433
1479
  - **Graphviz rendering blocks the main thread.** Viz.js lays out synchronously. A 300-edge
1434
1480
  graph takes ~26 ms, but a 3 000-edge graph takes ~2.25 s and visibly freezes the page;
1435
1481
  `graphviz.maxSourceBytes` is the guard. Rendering in a Web Worker would remove the ceiling
@@ -1452,7 +1498,7 @@ too and was rejected: it carries one person's account reach and expires on a cal
1452
1498
  ### Diagrams stay on "Loading PlantUML runtime…" and the assets 404 under a subpath
1453
1499
 
1454
1500
  Open the network tab and check the failing URL. It should be
1455
- `<baseUrl>assets/plantuml-client-<version>/viz-global.js`. If the request goes to
1501
+ `<baseUrl>assets/plantuml-client-<version>-max32768/viz-global.js`. If the request goes to
1456
1502
  `/assets/...` when your site lives at `/docs/`, the site's `baseUrl` is wrong — not the
1457
1503
  plugin's paths, which are always resolved through `useBaseUrl()`.
1458
1504
 
@@ -1476,7 +1522,7 @@ served as `text/plain`, `application/octet-stream` or `text/html`. The console s
1476
1522
  MIME type checking error. Check with:
1477
1523
 
1478
1524
  ```bash
1479
- curl -I https://example.com/your-base-url/assets/plantuml-client-1.2026.6/plantuml.js
1525
+ curl -I https://example.com/your-base-url/assets/plantuml-client-1.2026.6-max32768/plantuml.js
1480
1526
  ```
1481
1527
 
1482
1528
  The `content-type` must be a JavaScript type (`text/javascript` or
@@ -10,7 +10,26 @@ export declare const PLUGIN_ID = "plantuml-client";
10
10
  /** Docusaurus' own default plugin instance id, mirrored to avoid a runtime import. */
11
11
  export declare const DEFAULT_PLUGIN_ID = "default";
12
12
  export declare const PLUGIN_NAME = "docusaurus-plugin-plantuml-client";
13
- /** Directory (relative to `baseUrl`) that the PlantUML runtime assets are emitted into. */
13
+ /**
14
+ * The diagram size ceiling hard-coded in `@plantuml/core`'s own build, in PlantUML points.
15
+ *
16
+ * The engine compares the laid-out diagram's width and height against it and refuses anything
17
+ * larger with `Diagram too large for browser rendering: WxH (max 4096)`.
18
+ */
19
+ export declare const UPSTREAM_MAX_DIAGRAM_SIZE = 4096;
20
+ /**
21
+ * The ceiling this plugin patches into the engine it serves. See `src/enginePatch.ts` and
22
+ * `docs/adr/0007-engine-size-ceiling-patch.md`.
23
+ */
24
+ export declare const MAX_DIAGRAM_SIZE = 32768;
25
+ /**
26
+ * Directory (relative to `baseUrl`) that the PlantUML runtime assets are emitted into.
27
+ *
28
+ * The `-max…` segment is part of the engine's identity, not decoration: the file served here
29
+ * is a *patched* `plantuml.js`, so a reader holding a cached copy from a plugin version that
30
+ * patched to a different ceiling — or did not patch at all — must not be handed it from the
31
+ * same URL. Same reasoning as the `stdlib-<revision>` segment nested inside this one.
32
+ */
14
33
  export declare function assetsDirForVersion(coreVersion: string): string;
15
34
  export declare const VIZ_SCRIPT_FILENAME = "viz-global.js";
16
35
  export declare const PLANTUML_MODULE_FILENAME = "plantuml.js";
package/dist/constants.js CHANGED
@@ -10,9 +10,28 @@ export const PLUGIN_ID = 'plantuml-client';
10
10
  /** Docusaurus' own default plugin instance id, mirrored to avoid a runtime import. */
11
11
  export const DEFAULT_PLUGIN_ID = 'default';
12
12
  export const PLUGIN_NAME = `docusaurus-plugin-${PLUGIN_ID}`;
13
- /** Directory (relative to `baseUrl`) that the PlantUML runtime assets are emitted into. */
13
+ /**
14
+ * The diagram size ceiling hard-coded in `@plantuml/core`'s own build, in PlantUML points.
15
+ *
16
+ * The engine compares the laid-out diagram's width and height against it and refuses anything
17
+ * larger with `Diagram too large for browser rendering: WxH (max 4096)`.
18
+ */
19
+ export const UPSTREAM_MAX_DIAGRAM_SIZE = 4096;
20
+ /**
21
+ * The ceiling this plugin patches into the engine it serves. See `src/enginePatch.ts` and
22
+ * `docs/adr/0007-engine-size-ceiling-patch.md`.
23
+ */
24
+ export const MAX_DIAGRAM_SIZE = 32_768;
25
+ /**
26
+ * Directory (relative to `baseUrl`) that the PlantUML runtime assets are emitted into.
27
+ *
28
+ * The `-max…` segment is part of the engine's identity, not decoration: the file served here
29
+ * is a *patched* `plantuml.js`, so a reader holding a cached copy from a plugin version that
30
+ * patched to a different ceiling — or did not patch at all — must not be handed it from the
31
+ * same URL. Same reasoning as the `stdlib-<revision>` segment nested inside this one.
32
+ */
14
33
  export function assetsDirForVersion(coreVersion) {
15
- return `assets/${PLUGIN_ID}-${coreVersion}`;
34
+ return `assets/${PLUGIN_ID}-${coreVersion}-max${MAX_DIAGRAM_SIZE}`;
16
35
  }
17
36
  export const VIZ_SCRIPT_FILENAME = 'viz-global.js';
18
37
  export const PLANTUML_MODULE_FILENAME = 'plantuml.js';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * How many bytes the patch adds. Every replacement is ASCII, so the byte delta is the
3
+ * character delta, and it is fixed for a given {@link MAX_DIAGRAM_SIZE}. This is what makes
4
+ * an already-generated file cheap to validate: its size is an exact fingerprint.
5
+ */
6
+ export declare const PATCH_SIZE_DELTA: number;
7
+ /**
8
+ * Rewrites the engine source so it accepts diagrams up to {@link MAX_DIAGRAM_SIZE} points.
9
+ *
10
+ * Both literals are counted *before* anything is replaced, so an engine that changed shape
11
+ * fails the build with a name attached rather than being silently half-patched. Patching the
12
+ * message as well as the comparison is not cosmetic — an engine that still reported
13
+ * `(max 4096)` while refusing at 32768 would send every future reader of that error down the
14
+ * wrong path. It is also safe: TeaVM's string pool is a plain list of JavaScript string
15
+ * literals, so a literal that changes length does not disturb its neighbours.
16
+ */
17
+ export declare function patchEngineSource(source: string, coreVersion: string): string;
18
+ export interface ResolvePatchedEngineOptions {
19
+ /** Absolute path of the vendored `plantuml.js` inside the installed `@plantuml/core`. */
20
+ vendoredPath: string;
21
+ /** Installed `@plantuml/core` version, which names the cache directory. */
22
+ coreVersion: string;
23
+ /** Site directory, used when `cacheDir` is absent. */
24
+ siteDir: string;
25
+ /**
26
+ * Where the patched engine is cached. Docusaurus passes its `generatedFilesDir`; when it
27
+ * is absent the site's `.docusaurus` directory is assumed, as in `src/stdlib.ts`.
28
+ */
29
+ cacheDir?: string;
30
+ }
31
+ /**
32
+ * Returns the path of the `plantuml.js` this build should serve, generating it if needed.
33
+ *
34
+ * The basename stays `plantuml.js`: the copy pattern emits `[name][ext]`, and the browser
35
+ * loader joins {@link PLANTUML_MODULE_FILENAME} onto the assets directory.
36
+ *
37
+ * Generation is skipped when the cached file is already the right size. That check is exact
38
+ * rather than approximate — {@link PATCH_SIZE_DELTA} is fixed — and it is O(1) where reading
39
+ * 7 MB back to inspect its contents would not be.
40
+ */
41
+ export declare function resolvePatchedEngine(options: ResolvePatchedEngineOptions): string;
@@ -0,0 +1,120 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { MAX_DIAGRAM_SIZE, PLANTUML_MODULE_FILENAME, PLUGIN_NAME, UPSTREAM_MAX_DIAGRAM_SIZE, } from './constants.js';
4
+ /**
5
+ * Raises the diagram size ceiling compiled into `@plantuml/core`.
6
+ *
7
+ * The engine measures the laid-out diagram and refuses to serialize anything wider or taller
8
+ * than 4096 points, reporting `Diagram too large for browser rendering: 78x12916 (max 4096)`
9
+ * through its error callback. `renderToString` takes only `{dark}`, so there is no option to
10
+ * turn this off, and no way to work around it from diagram source: the check runs *before* a
11
+ * `scale` directive is applied, `skinparam dpi` does not move it, and `left to right
12
+ * direction` only transposes the two numbers. Shrinking the Graphviz layout by hand —
13
+ * `skinparam ranksep`, fewer nodes, smaller fonts — is the only lever an author has.
14
+ *
15
+ * The ceiling is arbitrary. It is not an SVG or a browser constraint; patched to 65536 the
16
+ * same engine produced correct SVG for a 79x43157 diagram. So this plugin patches it, and
17
+ * serves the patched file rather than the vendored one.
18
+ *
19
+ * See `docs/adr/0007-engine-size-ceiling-patch.md` for the evidence and the rejected
20
+ * alternatives.
21
+ */
22
+ /**
23
+ * The comparison the engine guards on, as it appears in the minified bundle:
24
+ *
25
+ * ```js
26
+ * p=o.bBY;if(!(p>4096.0)){q=o.bB0;if(!(q>4096.0)){ …serialize… }}
27
+ * ```
28
+ *
29
+ * Deliberately anchored on `>4096.0)` rather than on the surrounding code: `p`, `q` and `o`
30
+ * are TeaVM-mangled locals that are renamed by every `@plantuml/core` build, so a pattern
31
+ * that named them would break on a release that changed nothing meaningful.
32
+ */
33
+ const COMPARISON = `>${UPSTREAM_MAX_DIAGRAM_SIZE}.0)`;
34
+ /**
35
+ * `4096.0` occurs exactly twice in the whole 7 MB bundle, and both occurrences are the width
36
+ * and height halves of this one guard. That makes the count the safety property: two matches
37
+ * means the engine still has the shape this patch was written against, and any other number
38
+ * means it does not.
39
+ */
40
+ const COMPARISON_OCCURRENCES = 2;
41
+ /** The suffix of the engine's own error message, in TeaVM's string pool. */
42
+ const MESSAGE_SUFFIX = ` (max ${UPSTREAM_MAX_DIAGRAM_SIZE})`;
43
+ const MESSAGE_SUFFIX_OCCURRENCES = 1;
44
+ const PATCHED_COMPARISON = `>${MAX_DIAGRAM_SIZE}.0)`;
45
+ const PATCHED_MESSAGE_SUFFIX = ` (max ${MAX_DIAGRAM_SIZE})`;
46
+ /**
47
+ * How many bytes the patch adds. Every replacement is ASCII, so the byte delta is the
48
+ * character delta, and it is fixed for a given {@link MAX_DIAGRAM_SIZE}. This is what makes
49
+ * an already-generated file cheap to validate: its size is an exact fingerprint.
50
+ */
51
+ export const PATCH_SIZE_DELTA = COMPARISON_OCCURRENCES * (PATCHED_COMPARISON.length - COMPARISON.length) +
52
+ MESSAGE_SUFFIX_OCCURRENCES * (PATCHED_MESSAGE_SUFFIX.length - MESSAGE_SUFFIX.length);
53
+ /** Directory under the Docusaurus cache that generated engine builds live in. */
54
+ const CACHE_SUBDIRECTORY = 'plantuml-engine';
55
+ function countOccurrences(haystack, needle) {
56
+ let count = 0;
57
+ let index = haystack.indexOf(needle);
58
+ while (index !== -1) {
59
+ count += 1;
60
+ index = haystack.indexOf(needle, index + needle.length);
61
+ }
62
+ return count;
63
+ }
64
+ function assertOccurrences(source, needle, expected, coreVersion) {
65
+ const found = countOccurrences(source, needle);
66
+ if (found === expected)
67
+ return;
68
+ throw new Error(`[${PLUGIN_NAME}] Cannot raise the diagram size ceiling of ` +
69
+ `'@plantuml/core@${coreVersion}': expected ${expected} occurrence(s) of ` +
70
+ `${JSON.stringify(needle)} in '${PLANTUML_MODULE_FILENAME}', found ${found}. ` +
71
+ 'The engine no longer has the shape this plugin patches, which means the installed ' +
72
+ '@plantuml/core is not one this plugin supports. Pin @plantuml/core to the last ' +
73
+ 'version that worked, or upgrade this plugin.');
74
+ }
75
+ /**
76
+ * Rewrites the engine source so it accepts diagrams up to {@link MAX_DIAGRAM_SIZE} points.
77
+ *
78
+ * Both literals are counted *before* anything is replaced, so an engine that changed shape
79
+ * fails the build with a name attached rather than being silently half-patched. Patching the
80
+ * message as well as the comparison is not cosmetic — an engine that still reported
81
+ * `(max 4096)` while refusing at 32768 would send every future reader of that error down the
82
+ * wrong path. It is also safe: TeaVM's string pool is a plain list of JavaScript string
83
+ * literals, so a literal that changes length does not disturb its neighbours.
84
+ */
85
+ export function patchEngineSource(source, coreVersion) {
86
+ assertOccurrences(source, COMPARISON, COMPARISON_OCCURRENCES, coreVersion);
87
+ assertOccurrences(source, MESSAGE_SUFFIX, MESSAGE_SUFFIX_OCCURRENCES, coreVersion);
88
+ return source
89
+ .split(COMPARISON)
90
+ .join(PATCHED_COMPARISON)
91
+ .split(MESSAGE_SUFFIX)
92
+ .join(PATCHED_MESSAGE_SUFFIX);
93
+ }
94
+ /**
95
+ * Returns the path of the `plantuml.js` this build should serve, generating it if needed.
96
+ *
97
+ * The basename stays `plantuml.js`: the copy pattern emits `[name][ext]`, and the browser
98
+ * loader joins {@link PLANTUML_MODULE_FILENAME} onto the assets directory.
99
+ *
100
+ * Generation is skipped when the cached file is already the right size. That check is exact
101
+ * rather than approximate — {@link PATCH_SIZE_DELTA} is fixed — and it is O(1) where reading
102
+ * 7 MB back to inspect its contents would not be.
103
+ */
104
+ export function resolvePatchedEngine(options) {
105
+ const { vendoredPath, coreVersion, siteDir, cacheDir } = options;
106
+ const generatedDir = cacheDir ?? path.join(siteDir, '.docusaurus');
107
+ const target = path.join(generatedDir, CACHE_SUBDIRECTORY, coreVersion, PLANTUML_MODULE_FILENAME);
108
+ const expectedSize = fs.statSync(vendoredPath).size + PATCH_SIZE_DELTA;
109
+ if (fs.existsSync(target) && fs.statSync(target).size === expectedSize)
110
+ return target;
111
+ const patched = patchEngineSource(fs.readFileSync(vendoredPath, 'utf8'), coreVersion);
112
+ fs.mkdirSync(path.dirname(target), { recursive: true });
113
+ // `.docusaurus` is shared by every locale, and Docusaurus builds locales in separate
114
+ // processes. Writing 7 MB in place would let a concurrent build read a torn file — and a
115
+ // torn file of the right length would pass the size check above. Rename is atomic.
116
+ const temporary = `${target}.${process.pid}.tmp`;
117
+ fs.writeFileSync(temporary, patched, 'utf8');
118
+ fs.renameSync(temporary, target);
119
+ return target;
120
+ }
package/dist/index.cjs CHANGED
@@ -46,7 +46,7 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
46
46
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
47
47
 
48
48
  // src/index.ts
49
- var import_node_path4 = __toESM(require("path"), 1);
49
+ var import_node_path5 = __toESM(require("path"), 1);
50
50
  var import_node_url = require("url");
51
51
  var import_copy_webpack_plugin = __toESM(require("copy-webpack-plugin"), 1);
52
52
 
@@ -59,8 +59,10 @@ var import_node_path = __toESM(require("path"), 1);
59
59
  var PLUGIN_ID = "plantuml-client";
60
60
  var DEFAULT_PLUGIN_ID = "default";
61
61
  var PLUGIN_NAME = `docusaurus-plugin-${PLUGIN_ID}`;
62
+ var UPSTREAM_MAX_DIAGRAM_SIZE = 4096;
63
+ var MAX_DIAGRAM_SIZE = 32768;
62
64
  function assetsDirForVersion(coreVersion) {
63
- return `assets/${PLUGIN_ID}-${coreVersion}`;
65
+ return `assets/${PLUGIN_ID}-${coreVersion}-max${MAX_DIAGRAM_SIZE}`;
64
66
  }
65
67
  var VIZ_SCRIPT_FILENAME = "viz-global.js";
66
68
  var PLANTUML_MODULE_FILENAME = "plantuml.js";
@@ -94,6 +96,52 @@ function assertRuntimeFile(file, version) {
94
96
  );
95
97
  }
96
98
 
99
+ // src/enginePatch.ts
100
+ var import_node_fs2 = __toESM(require("fs"), 1);
101
+ var import_node_path2 = __toESM(require("path"), 1);
102
+ var COMPARISON = `>${UPSTREAM_MAX_DIAGRAM_SIZE}.0)`;
103
+ var COMPARISON_OCCURRENCES = 2;
104
+ var MESSAGE_SUFFIX = ` (max ${UPSTREAM_MAX_DIAGRAM_SIZE})`;
105
+ var MESSAGE_SUFFIX_OCCURRENCES = 1;
106
+ var PATCHED_COMPARISON = `>${MAX_DIAGRAM_SIZE}.0)`;
107
+ var PATCHED_MESSAGE_SUFFIX = ` (max ${MAX_DIAGRAM_SIZE})`;
108
+ var PATCH_SIZE_DELTA = COMPARISON_OCCURRENCES * (PATCHED_COMPARISON.length - COMPARISON.length) + MESSAGE_SUFFIX_OCCURRENCES * (PATCHED_MESSAGE_SUFFIX.length - MESSAGE_SUFFIX.length);
109
+ var CACHE_SUBDIRECTORY = "plantuml-engine";
110
+ function countOccurrences(haystack, needle) {
111
+ let count = 0;
112
+ let index = haystack.indexOf(needle);
113
+ while (index !== -1) {
114
+ count += 1;
115
+ index = haystack.indexOf(needle, index + needle.length);
116
+ }
117
+ return count;
118
+ }
119
+ function assertOccurrences(source, needle, expected, coreVersion) {
120
+ const found = countOccurrences(source, needle);
121
+ if (found === expected) return;
122
+ throw new Error(
123
+ `[${PLUGIN_NAME}] Cannot raise the diagram size ceiling of '@plantuml/core@${coreVersion}': expected ${expected} occurrence(s) of ${JSON.stringify(needle)} in '${PLANTUML_MODULE_FILENAME}', found ${found}. The engine no longer has the shape this plugin patches, which means the installed @plantuml/core is not one this plugin supports. Pin @plantuml/core to the last version that worked, or upgrade this plugin.`
124
+ );
125
+ }
126
+ function patchEngineSource(source, coreVersion) {
127
+ assertOccurrences(source, COMPARISON, COMPARISON_OCCURRENCES, coreVersion);
128
+ assertOccurrences(source, MESSAGE_SUFFIX, MESSAGE_SUFFIX_OCCURRENCES, coreVersion);
129
+ return source.split(COMPARISON).join(PATCHED_COMPARISON).split(MESSAGE_SUFFIX).join(PATCHED_MESSAGE_SUFFIX);
130
+ }
131
+ function resolvePatchedEngine(options) {
132
+ const { vendoredPath, coreVersion, siteDir, cacheDir } = options;
133
+ const generatedDir = cacheDir ?? import_node_path2.default.join(siteDir, ".docusaurus");
134
+ const target = import_node_path2.default.join(generatedDir, CACHE_SUBDIRECTORY, coreVersion, PLANTUML_MODULE_FILENAME);
135
+ const expectedSize = import_node_fs2.default.statSync(vendoredPath).size + PATCH_SIZE_DELTA;
136
+ if (import_node_fs2.default.existsSync(target) && import_node_fs2.default.statSync(target).size === expectedSize) return target;
137
+ const patched = patchEngineSource(import_node_fs2.default.readFileSync(vendoredPath, "utf8"), coreVersion);
138
+ import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(target), { recursive: true });
139
+ const temporary = `${target}.${process.pid}.tmp`;
140
+ import_node_fs2.default.writeFileSync(temporary, patched, "utf8");
141
+ import_node_fs2.default.renameSync(temporary, target);
142
+ return target;
143
+ }
144
+
97
145
  // src/options.ts
98
146
  var GRAPHVIZ_ENGINES = [
99
147
  "circo",
@@ -405,12 +453,12 @@ function resolveOptions(rawOptions) {
405
453
 
406
454
  // src/stdlib.ts
407
455
  var import_node_crypto = __toESM(require("crypto"), 1);
408
- var import_node_fs3 = __toESM(require("fs"), 1);
409
- var import_node_path3 = __toESM(require("path"), 1);
456
+ var import_node_fs4 = __toESM(require("fs"), 1);
457
+ var import_node_path4 = __toESM(require("path"), 1);
410
458
 
411
459
  // src/stdlibBundle.ts
412
- var import_node_fs2 = __toESM(require("fs"), 1);
413
- var import_node_path2 = __toESM(require("path"), 1);
460
+ var import_node_fs3 = __toESM(require("fs"), 1);
461
+ var import_node_path3 = __toESM(require("path"), 1);
414
462
 
415
463
  // src/stdlibShared.ts
416
464
  var STDLIB_EXAMPLES_DIRECTORY = "_examples_";
@@ -444,17 +492,17 @@ function isSkippedDirectory(name) {
444
492
  return name.startsWith("_") && name.endsWith("_");
445
493
  }
446
494
  function collectFiles(directory, namespaceRoot) {
447
- const entries = import_node_fs2.default.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
495
+ const entries = import_node_fs3.default.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
448
496
  const files = [];
449
497
  for (const entry of entries) {
450
- const absolutePath = import_node_path2.default.join(directory, entry.name);
498
+ const absolutePath = import_node_path3.default.join(directory, entry.name);
451
499
  if (entry.isDirectory()) {
452
500
  if (!isSkippedDirectory(entry.name)) files.push(...collectFiles(absolutePath, namespaceRoot));
453
501
  continue;
454
502
  }
455
- const extension = import_node_path2.default.extname(entry.name).toLowerCase();
503
+ const extension = import_node_path3.default.extname(entry.name).toLowerCase();
456
504
  if (extension !== ".puml" && extension !== ".json") continue;
457
- const key = import_node_path2.default.relative(namespaceRoot, absolutePath).split(import_node_path2.default.sep).join("/").toLowerCase();
505
+ const key = import_node_path3.default.relative(namespaceRoot, absolutePath).split(import_node_path3.default.sep).join("/").toLowerCase();
458
506
  files.push({
459
507
  key,
460
508
  absolutePath,
@@ -465,9 +513,9 @@ function collectFiles(directory, namespaceRoot) {
465
513
  return files;
466
514
  }
467
515
  function readNamespaceInfo(namespaceRoot) {
468
- const readme = import_node_path2.default.join(namespaceRoot, "README.md");
469
- if (!import_node_fs2.default.existsSync(readme)) return {};
470
- const lines = import_node_fs2.default.readFileSync(readme, "utf8").split(/\r\n|\r|\n/);
516
+ const readme = import_node_path3.default.join(namespaceRoot, "README.md");
517
+ if (!import_node_fs3.default.existsSync(readme)) return {};
518
+ const lines = import_node_fs3.default.readFileSync(readme, "utf8").split(/\r\n|\r|\n/);
471
519
  if (lines[0]?.trim() !== "---") return {};
472
520
  const info = {};
473
521
  for (const line of lines.slice(1)) {
@@ -506,8 +554,8 @@ function stdlibBundleFileName(namespace) {
506
554
  return `${namespace.toLowerCase()}.min.js`;
507
555
  }
508
556
  function buildStdlibNamespace(stdlibRoot, directoryName) {
509
- const namespaceRoot = import_node_path2.default.join(stdlibRoot, directoryName);
510
- if (!import_node_fs2.default.existsSync(namespaceRoot) || !import_node_fs2.default.statSync(namespaceRoot).isDirectory()) {
557
+ const namespaceRoot = import_node_path3.default.join(stdlibRoot, directoryName);
558
+ if (!import_node_fs3.default.existsSync(namespaceRoot) || !import_node_fs3.default.statSync(namespaceRoot).isDirectory()) {
511
559
  throw new Error(`'${namespaceRoot}' is not a plantuml-stdlib namespace directory.`);
512
560
  }
513
561
  const namespace = directoryName.toLowerCase();
@@ -528,7 +576,7 @@ function buildStdlibNamespace(stdlibRoot, directoryName) {
528
576
  ` var J = (w.PLANTUML_STDLIB_JSON[${quoted}] = w.PLANTUML_STDLIB_JSON[${quoted}] || {});`
529
577
  ];
530
578
  for (const file of files) {
531
- const text = import_node_fs2.default.readFileSync(file.absolutePath, "utf8");
579
+ const text = import_node_fs3.default.readFileSync(file.absolutePath, "utf8");
532
580
  const bare = withoutExtension(file.key);
533
581
  const target = file.extension === ".puml" ? "P" : "J";
534
582
  const value = file.extension === ".puml" ? toJsLiteral(unwrapDocument(text.split(/\r\n|\r|\n/))) : toJsonLiteral(text, file.absolutePath);
@@ -556,16 +604,16 @@ function buildStdlibNamespace(stdlibRoot, directoryName) {
556
604
 
557
605
  // src/stdlib.ts
558
606
  function vendoredDirectory(currentDir2) {
559
- return import_node_path3.default.join(currentDir2, "..", "assets", "stdlib");
607
+ return import_node_path4.default.join(currentDir2, "..", "assets", "stdlib");
560
608
  }
561
609
  function readVendoredManifest(directory) {
562
- const manifestPath = import_node_path3.default.join(directory, "manifest.json");
563
- if (!import_node_fs3.default.existsSync(manifestPath)) {
610
+ const manifestPath = import_node_path4.default.join(directory, "manifest.json");
611
+ if (!import_node_fs4.default.existsSync(manifestPath)) {
564
612
  throw new Error(
565
613
  `[${PLUGIN_NAME}] The vendored standard library manifest is missing from '${manifestPath}'. This is a packaging fault: reinstall the plugin, or run 'npm run stdlib:update' if you are working in a checkout of it.`
566
614
  );
567
615
  }
568
- return JSON.parse(import_node_fs3.default.readFileSync(manifestPath, "utf8"));
616
+ return JSON.parse(import_node_fs4.default.readFileSync(manifestPath, "utf8"));
569
617
  }
570
618
  function digest(...parts) {
571
619
  const hash = import_node_crypto.default.createHash("sha256");
@@ -575,13 +623,13 @@ function digest(...parts) {
575
623
  function directoryStamp(directory) {
576
624
  const parts = [];
577
625
  const walk = (current) => {
578
- for (const entry of import_node_fs3.default.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
579
- const absolute = import_node_path3.default.join(current, entry.name);
626
+ for (const entry of import_node_fs4.default.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
627
+ const absolute = import_node_path4.default.join(current, entry.name);
580
628
  if (entry.isDirectory()) {
581
629
  walk(absolute);
582
630
  continue;
583
631
  }
584
- const stats = import_node_fs3.default.statSync(absolute);
632
+ const stats = import_node_fs4.default.statSync(absolute);
585
633
  parts.push(`${absolute}:${stats.size}:${stats.mtimeMs}`);
586
634
  }
587
635
  };
@@ -590,10 +638,10 @@ function directoryStamp(directory) {
590
638
  }
591
639
  function findNamespaceDirectory(sourceRoots, namespace) {
592
640
  for (const root of sourceRoots) {
593
- if (!import_node_fs3.default.existsSync(root)) continue;
594
- for (const entry of import_node_fs3.default.readdirSync(root, { withFileTypes: true })) {
641
+ if (!import_node_fs4.default.existsSync(root)) continue;
642
+ for (const entry of import_node_fs4.default.readdirSync(root, { withFileTypes: true })) {
595
643
  if (entry.isDirectory() && entry.name.toLowerCase() === namespace) {
596
- return import_node_path3.default.join(root, entry.name);
644
+ return import_node_path4.default.join(root, entry.name);
597
645
  }
598
646
  }
599
647
  }
@@ -615,8 +663,8 @@ function resolveStdlibAssets({
615
663
  if (namespace in namespaces) return;
616
664
  const entry = vendoredNamespaces[namespace];
617
665
  if (!entry) return;
618
- const bundlePath = import_node_path3.default.join(directory, entry.file);
619
- if (!import_node_fs3.default.existsSync(bundlePath)) {
666
+ const bundlePath = import_node_path4.default.join(directory, entry.file);
667
+ if (!import_node_fs4.default.existsSync(bundlePath)) {
620
668
  throw new Error(
621
669
  `[${PLUGIN_NAME}] The vendored standard library manifest lists '${entry.file}', but '${bundlePath}' does not exist. Reinstall the plugin.`
622
670
  );
@@ -640,8 +688,8 @@ function resolveStdlibAssets({
640
688
  }
641
689
  }
642
690
  for (const namespace of selected) addVendored(namespace);
643
- const sourceRoots = options.source.map((entry) => import_node_path3.default.resolve(siteDir, entry));
644
- const generatedDir = import_node_path3.default.join(cacheDir ?? import_node_path3.default.join(siteDir, ".docusaurus"), "plantuml-stdlib");
691
+ const sourceRoots = options.source.map((entry) => import_node_path4.default.resolve(siteDir, entry));
692
+ const generatedDir = import_node_path4.default.join(cacheDir ?? import_node_path4.default.join(siteDir, ".docusaurus"), "plantuml-stdlib");
645
693
  const stamps = [];
646
694
  for (const namespace of options.include) {
647
695
  const namespaceDir = findNamespaceDirectory(sourceRoots, namespace);
@@ -652,12 +700,12 @@ function resolveStdlibAssets({
652
700
  }
653
701
  const stamp = directoryStamp(namespaceDir);
654
702
  stamps.push(`${namespace}:${stamp}`);
655
- const cached = import_node_path3.default.join(generatedDir, stamp, stdlibBundleFileName(namespace));
656
- if (!import_node_fs3.default.existsSync(cached)) {
657
- const bundle = buildStdlibNamespace(import_node_path3.default.dirname(namespaceDir), import_node_path3.default.basename(namespaceDir));
658
- import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(cached), { recursive: true });
659
- import_node_fs3.default.writeFileSync(cached, bundle.script, "utf8");
660
- import_node_fs3.default.writeFileSync(
703
+ const cached = import_node_path4.default.join(generatedDir, stamp, stdlibBundleFileName(namespace));
704
+ if (!import_node_fs4.default.existsSync(cached)) {
705
+ const bundle = buildStdlibNamespace(import_node_path4.default.dirname(namespaceDir), import_node_path4.default.basename(namespaceDir));
706
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(cached), { recursive: true });
707
+ import_node_fs4.default.writeFileSync(cached, bundle.script, "utf8");
708
+ import_node_fs4.default.writeFileSync(
661
709
  `${cached}.json`,
662
710
  JSON.stringify({
663
711
  dependencies: bundle.dependencies,
@@ -666,9 +714,9 @@ function resolveStdlibAssets({
666
714
  "utf8"
667
715
  );
668
716
  }
669
- const meta = JSON.parse(import_node_fs3.default.readFileSync(`${cached}.json`, "utf8"));
717
+ const meta = JSON.parse(import_node_fs4.default.readFileSync(`${cached}.json`, "utf8"));
670
718
  const previous = files.findIndex(
671
- (file) => import_node_path3.default.basename(file) === stdlibBundleFileName(namespace)
719
+ (file) => import_node_path4.default.basename(file) === stdlibBundleFileName(namespace)
672
720
  );
673
721
  if (previous !== -1) files.splice(previous, 1);
674
722
  files.push(cached);
@@ -687,7 +735,7 @@ function resolveStdlibAssets({
687
735
  }
688
736
 
689
737
  // src/index.ts
690
- var currentDir = import_node_path4.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
738
+ var currentDir = import_node_path5.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
691
739
  function createCopyPlugin(currentBundler, patterns) {
692
740
  if (currentBundler?.name === "rspack") {
693
741
  const { CopyRspackPlugin } = currentBundler.instance;
@@ -714,7 +762,7 @@ function plantumlPlugin(context, options = {}) {
714
762
  return {
715
763
  name: PLUGIN_NAME,
716
764
  getThemePath() {
717
- return import_node_path4.default.join(currentDir, "theme");
765
+ return import_node_path5.default.join(currentDir, "theme");
718
766
  },
719
767
  contentLoaded({ actions }) {
720
768
  const globalData = {
@@ -729,8 +777,14 @@ function plantumlPlugin(context, options = {}) {
729
777
  if (isServer) {
730
778
  return {};
731
779
  }
780
+ const engine = resolvePatchedEngine({
781
+ vendoredPath: import_node_path5.default.join(core.packageDir, PLANTUML_MODULE_FILENAME),
782
+ coreVersion: core.version,
783
+ siteDir: context.siteDir,
784
+ cacheDir: context.generatedFilesDir
785
+ });
732
786
  const patterns = core.files.map((absolutePath) => ({
733
- from: absolutePath,
787
+ from: import_node_path5.default.basename(absolutePath) === PLANTUML_MODULE_FILENAME ? engine : absolutePath,
734
788
  to: `${assetsDir}/[name][ext]`,
735
789
  // These files are already minified upstream; re-processing 8 MB is wasted work.
736
790
  info: { minimized: true }
package/dist/index.js CHANGED
@@ -2,7 +2,8 @@ import path from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import CopyWebpackPlugin from 'copy-webpack-plugin';
4
4
  import { locatePlantUmlCore } from './assets.js';
5
- import { assetsDirForVersion, DEFAULT_PLUGIN_ID, PLUGIN_NAME } from './constants.js';
5
+ import { assetsDirForVersion, DEFAULT_PLUGIN_ID, PLANTUML_MODULE_FILENAME, PLUGIN_NAME, } from './constants.js';
6
+ import { resolvePatchedEngine } from './enginePatch.js';
6
7
  import { resolveOptions, } from './options.js';
7
8
  import { resolveStdlibAssets } from './stdlib.js';
8
9
  export { DEFAULT_OPTIONS, PlantUmlOptionsError, resolveOptions } from './options.js';
@@ -62,8 +63,18 @@ export default function plantumlPlugin(context, options = {}) {
62
63
  if (isServer) {
63
64
  return {};
64
65
  }
66
+ // The engine is patched to raise its 4096-point diagram ceiling, so what is emitted is
67
+ // a generated file rather than the vendored one. It happens here rather than in the
68
+ // plugin factory so that `swizzle`, `write-translations` and the server compilation do
69
+ // not each pay to read and rewrite 7 MB for output they never emit.
70
+ const engine = resolvePatchedEngine({
71
+ vendoredPath: path.join(core.packageDir, PLANTUML_MODULE_FILENAME),
72
+ coreVersion: core.version,
73
+ siteDir: context.siteDir,
74
+ cacheDir: context.generatedFilesDir,
75
+ });
65
76
  const patterns = core.files.map((absolutePath) => ({
66
- from: absolutePath,
77
+ from: path.basename(absolutePath) === PLANTUML_MODULE_FILENAME ? engine : absolutePath,
67
78
  to: `${assetsDir}/[name][ext]`,
68
79
  // These files are already minified upstream; re-processing 8 MB is wasted work.
69
80
  info: { minimized: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matfsw/docusaurus-plantuml-plugin",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "Render PlantUML diagrams in Docusaurus 3 entirely in the browser - no Java, no PlantUML server, no CDN.",
5
5
  "keywords": [
6
6
  "docusaurus",