@matfsw/docusaurus-plantuml-plugin 1.7.1 → 1.8.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/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_path5 = __toESM(require("path"), 1);
49
+ var import_node_path4 = __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,10 +59,9 @@ 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
+ var MINIMUM_CORE_VERSION = "1.2026.8";
64
63
  function assetsDirForVersion(coreVersion) {
65
- return `assets/${PLUGIN_ID}-${coreVersion}-max${MAX_DIAGRAM_SIZE}`;
64
+ return `assets/${PLUGIN_ID}-${coreVersion}`;
66
65
  }
67
66
  var VIZ_SCRIPT_FILENAME = "viz-global.js";
68
67
  var PLANTUML_MODULE_FILENAME = "plantuml.js";
@@ -82,6 +81,7 @@ function locatePlantUmlCore() {
82
81
  const packageDir = import_node_path.default.dirname(packageJsonPath);
83
82
  const packageJson = JSON.parse(import_node_fs.default.readFileSync(packageJsonPath, "utf8"));
84
83
  const version = typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
84
+ assertSupportedVersion(version);
85
85
  const files = [
86
86
  import_node_path.default.join(packageDir, VIZ_SCRIPT_FILENAME),
87
87
  import_node_path.default.join(packageDir, PLANTUML_MODULE_FILENAME)
@@ -95,52 +95,22 @@ function assertRuntimeFile(file, version) {
95
95
  `[${PLUGIN_NAME}] '@plantuml/core@${version}' does not contain '${import_node_path.default.basename(file)}', which this plugin serves as a runtime asset. This means the installed version of @plantuml/core is not one this plugin supports. Pin a supported version, or open an issue if the package has genuinely changed its layout.`
96
96
  );
97
97
  }
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;
98
+ function compareVersions(left, right) {
99
+ const parse = (value) => (value.split("-")[0] ?? "").split(".").map((part) => Number.parseInt(part, 10) || 0);
100
+ const a = parse(left);
101
+ const b = parse(right);
102
+ for (let index = 0; index < 3; index += 1) {
103
+ const difference = (a[index] ?? 0) - (b[index] ?? 0);
104
+ if (difference !== 0) return difference;
105
+ }
106
+ return 0;
107
+ }
108
+ function assertSupportedVersion(version) {
109
+ if (compareVersions(version, MINIMUM_CORE_VERSION) >= 0) return;
122
110
  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.`
111
+ `[${PLUGIN_NAME}] '@plantuml/core@${version}' is older than the minimum this plugin supports, ${MINIMUM_CORE_VERSION}. That release added the 'maxSvgSize' render option this plugin's \`maxSvgSize\` setting relies on; on an older engine the setting would be silently ignored and diagrams larger than 4096 points would be refused. Update @plantuml/core, or remove the override or stale lockfile entry pinning it.`
124
112
  );
125
113
  }
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
114
 
145
115
  // src/options.ts
146
116
  var GRAPHVIZ_ENGINES = [
@@ -179,6 +149,7 @@ var DEFAULT_OPTIONS = {
179
149
  showSourceOnError: true,
180
150
  renderTimeoutMs: 2e4,
181
151
  cacheMaxEntries: 50,
152
+ maxSvgSize: 32768,
182
153
  zoom: true,
183
154
  showSource: true,
184
155
  graphviz: DEFAULT_GRAPHVIZ_OPTIONS,
@@ -373,6 +344,15 @@ function validateCacheMaxEntries(value) {
373
344
  }
374
345
  return value;
375
346
  }
347
+ function validateMaxSvgSize(value) {
348
+ if (value === void 0) return DEFAULT_OPTIONS.maxSvgSize;
349
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
350
+ fail(
351
+ `options.maxSvgSize must be a non-negative integer (0 disables the ceiling), received ${JSON.stringify(value)}.`
352
+ );
353
+ }
354
+ return value;
355
+ }
376
356
  function resolveOptions(rawOptions) {
377
357
  if (rawOptions === void 0 || rawOptions === null) {
378
358
  return {
@@ -394,6 +374,7 @@ function resolveOptions(rawOptions) {
394
374
  "showSourceOnError",
395
375
  "renderTimeoutMs",
396
376
  "cacheMaxEntries",
377
+ "maxSvgSize",
397
378
  "zoom",
398
379
  "showSource",
399
380
  "graphviz",
@@ -446,6 +427,7 @@ function resolveOptions(rawOptions) {
446
427
  ),
447
428
  renderTimeoutMs: validateRenderTimeout(rawOptions.renderTimeoutMs),
448
429
  cacheMaxEntries: validateCacheMaxEntries(rawOptions.cacheMaxEntries),
430
+ maxSvgSize: validateMaxSvgSize(rawOptions.maxSvgSize),
449
431
  zoom: validateBoolean(rawOptions.zoom, "zoom", DEFAULT_OPTIONS.zoom),
450
432
  showSource: validateBoolean(rawOptions.showSource, "showSource", DEFAULT_OPTIONS.showSource)
451
433
  };
@@ -453,13 +435,13 @@ function resolveOptions(rawOptions) {
453
435
 
454
436
  // src/stdlib.ts
455
437
  var import_node_crypto = __toESM(require("crypto"), 1);
456
- var import_node_fs4 = __toESM(require("fs"), 1);
457
- var import_node_path4 = __toESM(require("path"), 1);
458
-
459
- // src/stdlibBundle.ts
460
438
  var import_node_fs3 = __toESM(require("fs"), 1);
461
439
  var import_node_path3 = __toESM(require("path"), 1);
462
440
 
441
+ // src/stdlibBundle.ts
442
+ var import_node_fs2 = __toESM(require("fs"), 1);
443
+ var import_node_path2 = __toESM(require("path"), 1);
444
+
463
445
  // src/stdlibShared.ts
464
446
  var STDLIB_EXAMPLES_DIRECTORY = "_examples_";
465
447
  var STDLIB_INCLUDE_PATTERN = /^[^\S\r\n]*!include(?:sub|url|_many)?[^\S\r\n]+<([^>\r\n]+)>/gim;
@@ -492,17 +474,17 @@ function isSkippedDirectory(name) {
492
474
  return name.startsWith("_") && name.endsWith("_");
493
475
  }
494
476
  function collectFiles(directory, namespaceRoot) {
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);
477
+ const entries = import_node_fs2.default.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
496
478
  const files = [];
497
479
  for (const entry of entries) {
498
- const absolutePath = import_node_path3.default.join(directory, entry.name);
480
+ const absolutePath = import_node_path2.default.join(directory, entry.name);
499
481
  if (entry.isDirectory()) {
500
482
  if (!isSkippedDirectory(entry.name)) files.push(...collectFiles(absolutePath, namespaceRoot));
501
483
  continue;
502
484
  }
503
- const extension = import_node_path3.default.extname(entry.name).toLowerCase();
485
+ const extension = import_node_path2.default.extname(entry.name).toLowerCase();
504
486
  if (extension !== ".puml" && extension !== ".json") continue;
505
- const key = import_node_path3.default.relative(namespaceRoot, absolutePath).split(import_node_path3.default.sep).join("/").toLowerCase();
487
+ const key = import_node_path2.default.relative(namespaceRoot, absolutePath).split(import_node_path2.default.sep).join("/").toLowerCase();
506
488
  files.push({
507
489
  key,
508
490
  absolutePath,
@@ -513,9 +495,9 @@ function collectFiles(directory, namespaceRoot) {
513
495
  return files;
514
496
  }
515
497
  function readNamespaceInfo(namespaceRoot) {
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/);
498
+ const readme = import_node_path2.default.join(namespaceRoot, "README.md");
499
+ if (!import_node_fs2.default.existsSync(readme)) return {};
500
+ const lines = import_node_fs2.default.readFileSync(readme, "utf8").split(/\r\n|\r|\n/);
519
501
  if (lines[0]?.trim() !== "---") return {};
520
502
  const info = {};
521
503
  for (const line of lines.slice(1)) {
@@ -554,8 +536,8 @@ function stdlibBundleFileName(namespace) {
554
536
  return `${namespace.toLowerCase()}.min.js`;
555
537
  }
556
538
  function buildStdlibNamespace(stdlibRoot, directoryName) {
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()) {
539
+ const namespaceRoot = import_node_path2.default.join(stdlibRoot, directoryName);
540
+ if (!import_node_fs2.default.existsSync(namespaceRoot) || !import_node_fs2.default.statSync(namespaceRoot).isDirectory()) {
559
541
  throw new Error(`'${namespaceRoot}' is not a plantuml-stdlib namespace directory.`);
560
542
  }
561
543
  const namespace = directoryName.toLowerCase();
@@ -576,7 +558,7 @@ function buildStdlibNamespace(stdlibRoot, directoryName) {
576
558
  ` var J = (w.PLANTUML_STDLIB_JSON[${quoted}] = w.PLANTUML_STDLIB_JSON[${quoted}] || {});`
577
559
  ];
578
560
  for (const file of files) {
579
- const text = import_node_fs3.default.readFileSync(file.absolutePath, "utf8");
561
+ const text = import_node_fs2.default.readFileSync(file.absolutePath, "utf8");
580
562
  const bare = withoutExtension(file.key);
581
563
  const target = file.extension === ".puml" ? "P" : "J";
582
564
  const value = file.extension === ".puml" ? toJsLiteral(unwrapDocument(text.split(/\r\n|\r|\n/))) : toJsonLiteral(text, file.absolutePath);
@@ -604,16 +586,16 @@ function buildStdlibNamespace(stdlibRoot, directoryName) {
604
586
 
605
587
  // src/stdlib.ts
606
588
  function vendoredDirectory(currentDir2) {
607
- return import_node_path4.default.join(currentDir2, "..", "assets", "stdlib");
589
+ return import_node_path3.default.join(currentDir2, "..", "assets", "stdlib");
608
590
  }
609
591
  function readVendoredManifest(directory) {
610
- const manifestPath = import_node_path4.default.join(directory, "manifest.json");
611
- if (!import_node_fs4.default.existsSync(manifestPath)) {
592
+ const manifestPath = import_node_path3.default.join(directory, "manifest.json");
593
+ if (!import_node_fs3.default.existsSync(manifestPath)) {
612
594
  throw new Error(
613
595
  `[${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.`
614
596
  );
615
597
  }
616
- return JSON.parse(import_node_fs4.default.readFileSync(manifestPath, "utf8"));
598
+ return JSON.parse(import_node_fs3.default.readFileSync(manifestPath, "utf8"));
617
599
  }
618
600
  function digest(...parts) {
619
601
  const hash = import_node_crypto.default.createHash("sha256");
@@ -623,13 +605,13 @@ function digest(...parts) {
623
605
  function directoryStamp(directory) {
624
606
  const parts = [];
625
607
  const walk = (current) => {
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);
608
+ for (const entry of import_node_fs3.default.readdirSync(current, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
609
+ const absolute = import_node_path3.default.join(current, entry.name);
628
610
  if (entry.isDirectory()) {
629
611
  walk(absolute);
630
612
  continue;
631
613
  }
632
- const stats = import_node_fs4.default.statSync(absolute);
614
+ const stats = import_node_fs3.default.statSync(absolute);
633
615
  parts.push(`${absolute}:${stats.size}:${stats.mtimeMs}`);
634
616
  }
635
617
  };
@@ -638,10 +620,10 @@ function directoryStamp(directory) {
638
620
  }
639
621
  function findNamespaceDirectory(sourceRoots, namespace) {
640
622
  for (const root of sourceRoots) {
641
- if (!import_node_fs4.default.existsSync(root)) continue;
642
- for (const entry of import_node_fs4.default.readdirSync(root, { withFileTypes: true })) {
623
+ if (!import_node_fs3.default.existsSync(root)) continue;
624
+ for (const entry of import_node_fs3.default.readdirSync(root, { withFileTypes: true })) {
643
625
  if (entry.isDirectory() && entry.name.toLowerCase() === namespace) {
644
- return import_node_path4.default.join(root, entry.name);
626
+ return import_node_path3.default.join(root, entry.name);
645
627
  }
646
628
  }
647
629
  }
@@ -663,8 +645,8 @@ function resolveStdlibAssets({
663
645
  if (namespace in namespaces) return;
664
646
  const entry = vendoredNamespaces[namespace];
665
647
  if (!entry) return;
666
- const bundlePath = import_node_path4.default.join(directory, entry.file);
667
- if (!import_node_fs4.default.existsSync(bundlePath)) {
648
+ const bundlePath = import_node_path3.default.join(directory, entry.file);
649
+ if (!import_node_fs3.default.existsSync(bundlePath)) {
668
650
  throw new Error(
669
651
  `[${PLUGIN_NAME}] The vendored standard library manifest lists '${entry.file}', but '${bundlePath}' does not exist. Reinstall the plugin.`
670
652
  );
@@ -688,8 +670,8 @@ function resolveStdlibAssets({
688
670
  }
689
671
  }
690
672
  for (const namespace of selected) addVendored(namespace);
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");
673
+ const sourceRoots = options.source.map((entry) => import_node_path3.default.resolve(siteDir, entry));
674
+ const generatedDir = import_node_path3.default.join(cacheDir ?? import_node_path3.default.join(siteDir, ".docusaurus"), "plantuml-stdlib");
693
675
  const stamps = [];
694
676
  for (const namespace of options.include) {
695
677
  const namespaceDir = findNamespaceDirectory(sourceRoots, namespace);
@@ -700,12 +682,12 @@ function resolveStdlibAssets({
700
682
  }
701
683
  const stamp = directoryStamp(namespaceDir);
702
684
  stamps.push(`${namespace}:${stamp}`);
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(
685
+ const cached = import_node_path3.default.join(generatedDir, stamp, stdlibBundleFileName(namespace));
686
+ if (!import_node_fs3.default.existsSync(cached)) {
687
+ const bundle = buildStdlibNamespace(import_node_path3.default.dirname(namespaceDir), import_node_path3.default.basename(namespaceDir));
688
+ import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(cached), { recursive: true });
689
+ import_node_fs3.default.writeFileSync(cached, bundle.script, "utf8");
690
+ import_node_fs3.default.writeFileSync(
709
691
  `${cached}.json`,
710
692
  JSON.stringify({
711
693
  dependencies: bundle.dependencies,
@@ -714,9 +696,9 @@ function resolveStdlibAssets({
714
696
  "utf8"
715
697
  );
716
698
  }
717
- const meta = JSON.parse(import_node_fs4.default.readFileSync(`${cached}.json`, "utf8"));
699
+ const meta = JSON.parse(import_node_fs3.default.readFileSync(`${cached}.json`, "utf8"));
718
700
  const previous = files.findIndex(
719
- (file) => import_node_path4.default.basename(file) === stdlibBundleFileName(namespace)
701
+ (file) => import_node_path3.default.basename(file) === stdlibBundleFileName(namespace)
720
702
  );
721
703
  if (previous !== -1) files.splice(previous, 1);
722
704
  files.push(cached);
@@ -735,7 +717,7 @@ function resolveStdlibAssets({
735
717
  }
736
718
 
737
719
  // src/index.ts
738
- var currentDir = import_node_path5.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
720
+ var currentDir = import_node_path4.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
739
721
  function createCopyPlugin(currentBundler, patterns) {
740
722
  if (currentBundler?.name === "rspack") {
741
723
  const { CopyRspackPlugin } = currentBundler.instance;
@@ -762,7 +744,7 @@ function plantumlPlugin(context, options = {}) {
762
744
  return {
763
745
  name: PLUGIN_NAME,
764
746
  getThemePath() {
765
- return import_node_path5.default.join(currentDir, "theme");
747
+ return import_node_path4.default.join(currentDir, "theme");
766
748
  },
767
749
  contentLoaded({ actions }) {
768
750
  const globalData = {
@@ -777,14 +759,8 @@ function plantumlPlugin(context, options = {}) {
777
759
  if (isServer) {
778
760
  return {};
779
761
  }
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
- });
786
762
  const patterns = core.files.map((absolutePath) => ({
787
- from: import_node_path5.default.basename(absolutePath) === PLANTUML_MODULE_FILENAME ? engine : absolutePath,
763
+ from: absolutePath,
788
764
  to: `${assetsDir}/[name][ext]`,
789
765
  // These files are already minified upstream; re-processing 8 MB is wasted work.
790
766
  info: { minimized: true }
package/dist/index.d.cts CHANGED
@@ -108,6 +108,20 @@ interface PlantUmlPluginOptions {
108
108
  renderTimeoutMs?: number;
109
109
  /** Maximum number of cached SVG entries kept per browsing session. */
110
110
  cacheMaxEntries?: number;
111
+ /**
112
+ * Refuse to render a PlantUML diagram wider or taller than this many points.
113
+ *
114
+ * The engine measures the laid-out diagram before serializing it and reports anything
115
+ * larger through its error callback. Its own default is 8192; this plugin raises it to
116
+ * 32768, which is high enough that a real architecture diagram or release runbook does not
117
+ * hit it. `0` removes the check entirely.
118
+ *
119
+ * Raising it is not free: the resulting SVG is parsed synchronously twice on its way into
120
+ * the page, so a diagram of many thousands of points can stall the tab and may need
121
+ * `renderTimeoutMs` raised with it. Graphviz fences are bounded by
122
+ * `graphviz.maxSourceBytes` instead — this option does not apply to them.
123
+ */
124
+ maxSvgSize?: number;
111
125
  /**
112
126
  * Let readers zoom and pan rendered diagrams, with a small control toolbar.
113
127
  * Override for a single fence with `zoom` or `zoom=false` in its metastring.
package/dist/index.js CHANGED
@@ -2,8 +2,7 @@ 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, PLANTUML_MODULE_FILENAME, PLUGIN_NAME, } from './constants.js';
6
- import { resolvePatchedEngine } from './enginePatch.js';
5
+ import { assetsDirForVersion, DEFAULT_PLUGIN_ID, PLUGIN_NAME } from './constants.js';
7
6
  import { resolveOptions, } from './options.js';
8
7
  import { resolveStdlibAssets } from './stdlib.js';
9
8
  export { DEFAULT_OPTIONS, PlantUmlOptionsError, resolveOptions } from './options.js';
@@ -63,18 +62,8 @@ export default function plantumlPlugin(context, options = {}) {
63
62
  if (isServer) {
64
63
  return {};
65
64
  }
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
- });
76
65
  const patterns = core.files.map((absolutePath) => ({
77
- from: path.basename(absolutePath) === PLANTUML_MODULE_FILENAME ? engine : absolutePath,
66
+ from: absolutePath,
78
67
  to: `${assetsDir}/[name][ext]`,
79
68
  // These files are already minified upstream; re-processing 8 MB is wasted work.
80
69
  info: { minimized: true },
package/dist/options.d.ts CHANGED
@@ -105,6 +105,20 @@ export interface PlantUmlPluginOptions {
105
105
  renderTimeoutMs?: number;
106
106
  /** Maximum number of cached SVG entries kept per browsing session. */
107
107
  cacheMaxEntries?: number;
108
+ /**
109
+ * Refuse to render a PlantUML diagram wider or taller than this many points.
110
+ *
111
+ * The engine measures the laid-out diagram before serializing it and reports anything
112
+ * larger through its error callback. Its own default is 8192; this plugin raises it to
113
+ * 32768, which is high enough that a real architecture diagram or release runbook does not
114
+ * hit it. `0` removes the check entirely.
115
+ *
116
+ * Raising it is not free: the resulting SVG is parsed synchronously twice on its way into
117
+ * the page, so a diagram of many thousands of points can stall the tab and may need
118
+ * `renderTimeoutMs` raised with it. Graphviz fences are bounded by
119
+ * `graphviz.maxSourceBytes` instead — this option does not apply to them.
120
+ */
121
+ maxSvgSize?: number;
108
122
  /**
109
123
  * Let readers zoom and pan rendered diagrams, with a small control toolbar.
110
124
  * Override for a single fence with `zoom` or `zoom=false` in its metastring.
package/dist/options.js CHANGED
@@ -42,6 +42,7 @@ export const DEFAULT_OPTIONS = {
42
42
  showSourceOnError: true,
43
43
  renderTimeoutMs: 20_000,
44
44
  cacheMaxEntries: 50,
45
+ maxSvgSize: 32_768,
45
46
  zoom: true,
46
47
  showSource: true,
47
48
  graphviz: DEFAULT_GRAPHVIZ_OPTIONS,
@@ -240,6 +241,24 @@ function validateCacheMaxEntries(value) {
240
241
  }
241
242
  return value;
242
243
  }
244
+ /**
245
+ * Validates `maxSvgSize`, which is a size in PlantUML points and not a byte count.
246
+ *
247
+ * `0` is accepted and means "no ceiling" — that is the engine's own convention for the
248
+ * option, and it is the only value that is not simply a size. Negative numbers are rejected
249
+ * rather than passed through: the engine treats a negative as "unspecified" and falls back to
250
+ * its 8192 default, so `-1` would silently mean something quite different from what it reads
251
+ * like.
252
+ */
253
+ function validateMaxSvgSize(value) {
254
+ if (value === undefined)
255
+ return DEFAULT_OPTIONS.maxSvgSize;
256
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
257
+ fail('options.maxSvgSize must be a non-negative integer (0 disables the ceiling), ' +
258
+ `received ${JSON.stringify(value)}.`);
259
+ }
260
+ return value;
261
+ }
243
262
  /**
244
263
  * Validates raw plugin options and applies defaults.
245
264
  *
@@ -267,6 +286,7 @@ export function resolveOptions(rawOptions) {
267
286
  'showSourceOnError',
268
287
  'renderTimeoutMs',
269
288
  'cacheMaxEntries',
289
+ 'maxSvgSize',
270
290
  'zoom',
271
291
  'showSource',
272
292
  'graphviz',
@@ -309,6 +329,7 @@ export function resolveOptions(rawOptions) {
309
329
  showSourceOnError: validateBoolean(rawOptions.showSourceOnError, 'showSourceOnError', DEFAULT_OPTIONS.showSourceOnError),
310
330
  renderTimeoutMs: validateRenderTimeout(rawOptions.renderTimeoutMs),
311
331
  cacheMaxEntries: validateCacheMaxEntries(rawOptions.cacheMaxEntries),
332
+ maxSvgSize: validateMaxSvgSize(rawOptions.maxSvgSize),
312
333
  zoom: validateBoolean(rawOptions.zoom, 'zoom', DEFAULT_OPTIONS.zoom),
313
334
  showSource: validateBoolean(rawOptions.showSource, 'showSource', DEFAULT_OPTIONS.showSource),
314
335
  };
@@ -9,6 +9,14 @@ export interface CacheKeyInput {
9
9
  * invalidates the entries whose pictures it shaped. `null` when it is switched off.
10
10
  */
11
11
  stdlibRevision: string | null;
12
+ /**
13
+ * The size ceiling the entry was rendered under.
14
+ *
15
+ * Part of the key because the option can be *lowered*, and `session` mode outlives a
16
+ * rebuild: without it, a site that tightened `maxSvgSize` would keep serving readers the
17
+ * oversized diagram their tab had already cached.
18
+ */
19
+ maxSvgSize: number;
12
20
  }
13
21
  export interface DiagramCache {
14
22
  readonly mode: CacheMode;
@@ -18,7 +26,7 @@ export interface DiagramCache {
18
26
  /** Number of entries currently held. Exposed for tests and the documented size limit. */
19
27
  size(): number;
20
28
  }
21
- export declare function computeCacheKey({ source, dark, sanitized, coreVersion, stdlibRevision, }: CacheKeyInput): string;
29
+ export declare function computeCacheKey({ source, dark, sanitized, coreVersion, stdlibRevision, maxSvgSize, }: CacheKeyInput): string;
22
30
  export interface GraphvizCacheKeyInput {
23
31
  source: string;
24
32
  /** Graphviz layout engine; `dot` and `neato` draw different pictures from one source. */
@@ -15,12 +15,13 @@ function hash(input) {
15
15
  }
16
16
  return value.toString(36).padStart(7, '0');
17
17
  }
18
- export function computeCacheKey({ source, dark, sanitized, coreVersion, stdlibRevision, }) {
18
+ export function computeCacheKey({ source, dark, sanitized, coreVersion, stdlibRevision, maxSvgSize, }) {
19
19
  const mode = dark ? 'dark' : 'light';
20
20
  const clean = sanitized ? 'san' : 'raw';
21
21
  // The length guards against the (astronomically unlikely) 32-bit hash collision between
22
22
  // two different sources that happen to share every other key component.
23
- return `${coreVersion}|${stdlibRevision ?? 'nostd'}|${mode}|${clean}|${source.length}|${hash(source)}`;
23
+ return (`${coreVersion}|${stdlibRevision ?? 'nostd'}|${mode}|${clean}|max${maxSvgSize}|` +
24
+ `${source.length}|${hash(source)}`);
24
25
  }
25
26
  /**
26
27
  * Cache key for a Graphviz diagram.
@@ -6,6 +6,8 @@ export interface RenderDiagramRequest {
6
6
  dark: boolean;
7
7
  sanitize: boolean;
8
8
  timeoutMs: number;
9
+ /** Size ceiling in PlantUML points, passed straight to the engine. `0` disables it. */
10
+ maxSvgSize: number;
9
11
  assetsBaseUrl: string;
10
12
  coreVersion: string;
11
13
  /** Standard library assets for this site, or `null` when the feature is switched off. */
@@ -9,8 +9,12 @@ import { loadStdlibForSource } from './stdlibLoader.js';
9
9
  * This was verified against the installed `@plantuml/core` rather than assumed: the
10
10
  * function's arity is 4 and `{dark: true}` produces different fill colours from `{dark:
11
11
  * false}`, which itself is byte-identical to omitting the argument.
12
+ *
13
+ * `maxSvgSize` is always passed rather than omitted when it matches a default. The engine's
14
+ * own default (8192) is lower than this plugin's (32768), so omitting it would quietly
15
+ * tighten the ceiling.
12
16
  */
13
- function renderWithEngine(engine, source, dark) {
17
+ function renderWithEngine(engine, source, dark, maxSvgSize) {
14
18
  return new Promise((resolve, reject) => {
15
19
  let settled = false;
16
20
  const lines = source.split(/\r\n|\r|\n/);
@@ -25,7 +29,7 @@ function renderWithEngine(engine, source, dark) {
25
29
  return;
26
30
  settled = true;
27
31
  reject(new PlantUmlError('engine', describeEngineError(raw)));
28
- }, { dark });
32
+ }, { dark, maxSvgSize });
29
33
  }
30
34
  catch (error) {
31
35
  if (settled)
@@ -42,13 +46,14 @@ function renderWithEngine(engine, source, dark) {
42
46
  * {@link enqueueRender} because the engine cannot render two diagrams at once.
43
47
  */
44
48
  export async function renderDiagram(request) {
45
- const { source, dark, sanitize, timeoutMs, cache, coreVersion, stdlib, signal, onPhase } = request;
49
+ const { source, dark, sanitize, timeoutMs, maxSvgSize, cache, coreVersion, stdlib, signal, onPhase, } = request;
46
50
  const cacheKey = computeCacheKey({
47
51
  source,
48
52
  dark,
49
53
  sanitized: sanitize,
50
54
  coreVersion,
51
55
  stdlibRevision: stdlib?.manifest.revision ?? null,
56
+ maxSvgSize,
52
57
  });
53
58
  const cached = cache.get(cacheKey);
54
59
  if (cached !== undefined)
@@ -70,7 +75,7 @@ export async function renderDiagram(request) {
70
75
  ]);
71
76
  onPhase?.('rendering');
72
77
  const svg = await enqueueRender(async () => {
73
- const raw = await renderWithEngine(engine, source, dark);
78
+ const raw = await renderWithEngine(engine, source, dark, maxSvgSize);
74
79
  // Invalid PlantUML is reported as a successfully rendered "error picture", not through
75
80
  // the error callback, so a rendered SVG still has to be inspected before it is trusted.
76
81
  const diagramError = detectDiagramError(raw);
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * The public surface of `@plantuml/core@1.2026.x`, verified against the installed package.
3
3
  *
4
- * `plantuml.js` exports exactly two functions. Both take an optional trailing options
5
- * object whose only recognized member is `dark`; see `docs/architecture.md` for how this
6
- * was established and `tests/e2e/engine-contract.spec.ts` for the regression guard.
4
+ * `plantuml.js` exports exactly two functions. Both take an optional trailing options object
5
+ * whose recognized members are `dark` and `maxSvgSize`; see `docs/architecture.md` for how
6
+ * this was established and `tests/e2e/engine-contract.spec.ts` for the regression guard.
7
7
  */
8
8
  export interface PlantUmlCoreModule {
9
9
  render(lines: string[], targetId: string, options?: PlantUmlRenderOptions): void;
@@ -11,6 +11,15 @@ export interface PlantUmlCoreModule {
11
11
  }
12
12
  export interface PlantUmlRenderOptions {
13
13
  dark?: boolean;
14
+ /**
15
+ * Ceiling, in PlantUML points, on the laid-out diagram's width and height.
16
+ *
17
+ * Added in `@plantuml/core@1.2026.8`; `src/assets.ts` refuses to build against anything
18
+ * older, because an engine that does not know the option ignores it silently. `0` disables
19
+ * the check; a *negative* value is treated by the engine as unspecified and falls back to
20
+ * its own 8192 default, which is why `options.maxSvgSize` rejects negatives outright.
21
+ */
22
+ maxSvgSize?: number;
14
23
  }
15
24
  export type DiagramStatus = 'idle' | 'loading' | 'rendering' | 'ready' | 'error';
16
25
  /** Which engine renders a given fence. */
@@ -233,7 +233,12 @@ export default function PlantUmlDiagram({ source, title, language, engine = 'pla
233
233
  transparentBackground: options.graphviz.transparentBackground,
234
234
  maxSourceBytes: options.graphviz.maxSourceBytes,
235
235
  })
236
- : renderDiagram({ ...shared, dark: renderDark, stdlib: config.stdlib });
236
+ : renderDiagram({
237
+ ...shared,
238
+ dark: renderDark,
239
+ stdlib: config.stdlib,
240
+ maxSvgSize: options.maxSvgSize,
241
+ });
237
242
  void pending
238
243
  .then((svg) => commit({ status: 'ready', svg, error: null }))
239
244
  .catch((error) => {
@@ -258,6 +263,7 @@ export default function PlantUmlDiagram({ source, title, language, engine = 'pla
258
263
  config?.options.renderTimeoutMs,
259
264
  config?.options.cache,
260
265
  config?.options.cacheMaxEntries,
266
+ config?.options.maxSvgSize,
261
267
  config?.options.graphviz.transparentBackground,
262
268
  config?.options.graphviz.maxSourceBytes,
263
269
  ]);