@matfsw/docusaurus-plantuml-plugin 1.2.0 → 1.3.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 +65 -1
- package/README.md +144 -18
- package/assets/stdlib/LICENSES.md +21 -0
- package/assets/stdlib/archimate.min.js +22 -0
- package/assets/stdlib/azure.min.js +305 -0
- package/assets/stdlib/c4.min.js +47 -0
- package/assets/stdlib/cloudinsight.min.js +93 -0
- package/assets/stdlib/eip.min.js +14 -0
- package/assets/stdlib/k8s.min.js +52 -0
- package/assets/stdlib/kubernetes.min.js +17 -0
- package/assets/stdlib/manifest.json +112 -0
- package/assets/stdlib/office.min.js +557 -0
- package/dist/index.cjs +380 -7
- package/dist/index.d.cts +86 -3
- package/dist/index.d.ts +12 -2
- package/dist/index.js +20 -1
- package/dist/options.d.ts +52 -1
- package/dist/options.js +81 -0
- package/dist/runtime/assetLoader.d.ts +13 -1
- package/dist/runtime/assetLoader.js +36 -13
- package/dist/runtime/cache.d.ts +6 -1
- package/dist/runtime/cache.js +2 -2
- package/dist/runtime/errors.d.ts +3 -1
- package/dist/runtime/errors.js +27 -1
- package/dist/runtime/renderer.d.ts +3 -0
- package/dist/runtime/renderer.js +21 -7
- package/dist/runtime/stdlibLoader.d.ts +31 -0
- package/dist/runtime/stdlibLoader.js +99 -0
- package/dist/stdlib.d.ts +35 -0
- package/dist/stdlib.js +163 -0
- package/dist/stdlibBundle.d.ts +60 -0
- package/dist/stdlibBundle.js +218 -0
- package/dist/stdlibShared.d.ts +51 -0
- package/dist/stdlibShared.js +54 -0
- package/dist/theme/PlantUmlDiagram/index.js +2 -1
- package/dist/theme/usePlantUmlConfig.d.ts +8 -0
- package/dist/theme/usePlantUmlConfig.js +14 -8
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import CopyWebpackPlugin from 'copy-webpack-plugin';
|
|
|
4
4
|
import { locatePlantUmlCore } from './assets.js';
|
|
5
5
|
import { assetsDirForVersion, DEFAULT_PLUGIN_ID, PLUGIN_NAME } from './constants.js';
|
|
6
6
|
import { resolveOptions, } from './options.js';
|
|
7
|
+
import { resolveStdlibAssets } from './stdlib.js';
|
|
7
8
|
export { DEFAULT_OPTIONS, PlantUmlOptionsError, resolveOptions } from './options.js';
|
|
8
9
|
export { PLUGIN_ID, PLUGIN_NAME } from './constants.js';
|
|
9
10
|
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -25,12 +26,22 @@ export function createCopyPlugin(currentBundler, patterns) {
|
|
|
25
26
|
}
|
|
26
27
|
return new CopyWebpackPlugin({ patterns });
|
|
27
28
|
}
|
|
28
|
-
export default function plantumlPlugin(
|
|
29
|
+
export default function plantumlPlugin(context, options = {}) {
|
|
29
30
|
// Docusaurus already ran `validateOptions`, but the plugin function is also callable
|
|
30
31
|
// directly (and from JS configs that skip validation), so normalize defensively.
|
|
31
32
|
const resolved = resolveOptions(options);
|
|
32
33
|
const core = locatePlantUmlCore();
|
|
33
34
|
const assetsDir = assetsDirForVersion(core.version);
|
|
35
|
+
const stdlib = resolveStdlibAssets({
|
|
36
|
+
options: resolved.stdlib,
|
|
37
|
+
currentDir,
|
|
38
|
+
siteDir: context.siteDir,
|
|
39
|
+
cacheDir: context.generatedFilesDir,
|
|
40
|
+
});
|
|
41
|
+
// The revision is part of the directory name, not just of the cache key: refreshing the
|
|
42
|
+
// standard library without moving the URL would leave readers on a cached bundle from
|
|
43
|
+
// before the refresh, and the mismatch would surface as an unresolved include.
|
|
44
|
+
const stdlibDir = stdlib ? `${assetsDir}/stdlib-${stdlib.manifest.revision}` : null;
|
|
34
45
|
return {
|
|
35
46
|
name: PLUGIN_NAME,
|
|
36
47
|
getThemePath() {
|
|
@@ -41,6 +52,7 @@ export default function plantumlPlugin(_context, options = {}) {
|
|
|
41
52
|
options: resolved,
|
|
42
53
|
assetsDir,
|
|
43
54
|
coreVersion: core.version,
|
|
55
|
+
stdlib: stdlib && stdlibDir ? { dir: stdlibDir, manifest: stdlib.manifest } : null,
|
|
44
56
|
};
|
|
45
57
|
actions.setGlobalData(globalData);
|
|
46
58
|
},
|
|
@@ -56,6 +68,13 @@ export default function plantumlPlugin(_context, options = {}) {
|
|
|
56
68
|
// These files are already minified upstream; re-processing 8 MB is wasted work.
|
|
57
69
|
info: { minimized: true },
|
|
58
70
|
}));
|
|
71
|
+
if (stdlib && stdlibDir) {
|
|
72
|
+
patterns.push(...stdlib.files.map((absolutePath) => ({
|
|
73
|
+
from: absolutePath,
|
|
74
|
+
to: `${stdlibDir}/[name][ext]`,
|
|
75
|
+
info: { minimized: true },
|
|
76
|
+
})));
|
|
77
|
+
}
|
|
59
78
|
// Optional chaining, not laziness: `currentBundler` is absent on Docusaurus 3.5.x.
|
|
60
79
|
return { plugins: [createCopyPlugin(utils?.currentBundler, patterns)] };
|
|
61
80
|
},
|
package/dist/options.d.ts
CHANGED
|
@@ -39,6 +39,50 @@ export type ResolvedGraphvizOptions = Required<Omit<GraphvizOptions, 'languages'
|
|
|
39
39
|
/** Normalized to lower case; matching is case-insensitive. */
|
|
40
40
|
languages: string[];
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* PlantUML standard library support: `!include <C4/C4_Container>` and friends.
|
|
44
|
+
*
|
|
45
|
+
* On by default and normally not worth configuring. The two lists exist for the namespaces
|
|
46
|
+
* this package does not vendor — the standard library is 265 MB in full, and several of its
|
|
47
|
+
* namespaces carry no upstream licence — which a site can still opt into from its own
|
|
48
|
+
* checkout. See `assets/stdlib/LICENSES.md` for what is vendored and under what terms.
|
|
49
|
+
*/
|
|
50
|
+
export interface StdlibOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Resolve `!include <namespace/…>` at all. `false` (or `stdlib: false`) leaves such
|
|
53
|
+
* diagrams to fail in the engine, exactly as they did before this feature existed.
|
|
54
|
+
*/
|
|
55
|
+
enabled?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Extra namespaces to make available, beyond the vendored ones — `['aws', 'tupadr3']`.
|
|
58
|
+
* Each must be found in one of {@link StdlibOptions.source}, and the build fails naming
|
|
59
|
+
* the namespace if it is not.
|
|
60
|
+
*/
|
|
61
|
+
include?: string[];
|
|
62
|
+
/**
|
|
63
|
+
* Directories to resolve {@link StdlibOptions.include} from: the `stdlib/` directory of a
|
|
64
|
+
* `plantuml-stdlib` checkout, relative to the site directory or absolute. Searched in
|
|
65
|
+
* order, before the vendored bundles, so a namespace can also be replaced with a newer one
|
|
66
|
+
* by naming it in `include`.
|
|
67
|
+
*/
|
|
68
|
+
source?: string | string[];
|
|
69
|
+
/**
|
|
70
|
+
* Narrow the vendored namespaces emitted into the build, for sites that care more about
|
|
71
|
+
* build output size than about which diagrams keep working. Dependencies of the named
|
|
72
|
+
* namespaces are kept regardless. Defaults to all of them; a reader only downloads the
|
|
73
|
+
* ones a page uses either way.
|
|
74
|
+
*/
|
|
75
|
+
namespaces?: string[];
|
|
76
|
+
}
|
|
77
|
+
/** Standard library options after validation, with every default applied. */
|
|
78
|
+
export interface ResolvedStdlibOptions {
|
|
79
|
+
enabled: boolean;
|
|
80
|
+
/** Normalized to lower case. */
|
|
81
|
+
include: string[];
|
|
82
|
+
source: string[];
|
|
83
|
+
/** Normalized to lower case. `null` means "every vendored namespace". */
|
|
84
|
+
namespaces: string[] | null;
|
|
85
|
+
}
|
|
42
86
|
/**
|
|
43
87
|
* Options accepted by the plugin in `docusaurus.config.*`.
|
|
44
88
|
*
|
|
@@ -73,16 +117,23 @@ export interface PlantUmlPluginOptions {
|
|
|
73
117
|
showSource?: boolean;
|
|
74
118
|
/** Graphviz/DOT diagram support. See {@link GraphvizOptions}. */
|
|
75
119
|
graphviz?: GraphvizOptions;
|
|
120
|
+
/**
|
|
121
|
+
* PlantUML standard library support. See {@link StdlibOptions}. `false` is shorthand for
|
|
122
|
+
* `{enabled: false}`.
|
|
123
|
+
*/
|
|
124
|
+
stdlib?: StdlibOptions | false;
|
|
76
125
|
/** Docusaurus plugin instance id, used when the plugin is registered more than once. */
|
|
77
126
|
id?: string;
|
|
78
127
|
}
|
|
79
128
|
/** Plugin options after validation, with every default applied. */
|
|
80
|
-
export type ResolvedPlantUmlOptions = Required<Omit<PlantUmlPluginOptions, 'id' | 'languages' | 'graphviz'>> & {
|
|
129
|
+
export type ResolvedPlantUmlOptions = Required<Omit<PlantUmlPluginOptions, 'id' | 'languages' | 'graphviz' | 'stdlib'>> & {
|
|
81
130
|
/** Normalized to lower case; matching is case-insensitive. */
|
|
82
131
|
languages: string[];
|
|
83
132
|
graphviz: ResolvedGraphvizOptions;
|
|
133
|
+
stdlib: ResolvedStdlibOptions;
|
|
84
134
|
};
|
|
85
135
|
export declare const DEFAULT_GRAPHVIZ_OPTIONS: ResolvedGraphvizOptions;
|
|
136
|
+
export declare const DEFAULT_STDLIB_OPTIONS: ResolvedStdlibOptions;
|
|
86
137
|
export declare const DEFAULT_OPTIONS: ResolvedPlantUmlOptions;
|
|
87
138
|
/** Thrown for invalid plugin options so Docusaurus fails the build with a useful message. */
|
|
88
139
|
export declare class PlantUmlOptionsError extends Error {
|
package/dist/options.js
CHANGED
|
@@ -27,6 +27,12 @@ export const DEFAULT_GRAPHVIZ_OPTIONS = {
|
|
|
27
27
|
maxSourceBytes: 100_000,
|
|
28
28
|
transparentBackground: true,
|
|
29
29
|
};
|
|
30
|
+
export const DEFAULT_STDLIB_OPTIONS = {
|
|
31
|
+
enabled: true,
|
|
32
|
+
include: [],
|
|
33
|
+
source: [],
|
|
34
|
+
namespaces: null,
|
|
35
|
+
};
|
|
30
36
|
export const DEFAULT_OPTIONS = {
|
|
31
37
|
languages: ['plantuml', 'puml'],
|
|
32
38
|
theme: 'auto',
|
|
@@ -39,6 +45,7 @@ export const DEFAULT_OPTIONS = {
|
|
|
39
45
|
zoom: true,
|
|
40
46
|
showSource: true,
|
|
41
47
|
graphviz: DEFAULT_GRAPHVIZ_OPTIONS,
|
|
48
|
+
stdlib: DEFAULT_STDLIB_OPTIONS,
|
|
42
49
|
};
|
|
43
50
|
const CACHE_MODES = ['none', 'memory', 'session'];
|
|
44
51
|
const THEMES = ['auto', 'light', 'dark'];
|
|
@@ -140,6 +147,76 @@ function validateGraphviz(value) {
|
|
|
140
147
|
transparentBackground: validateBoolean(value.transparentBackground, 'graphviz.transparentBackground', DEFAULT_GRAPHVIZ_OPTIONS.transparentBackground),
|
|
141
148
|
};
|
|
142
149
|
}
|
|
150
|
+
/** Shared by `stdlib.include` and `stdlib.namespaces`, which are both lists of namespaces. */
|
|
151
|
+
function validateNamespaceList(value, key) {
|
|
152
|
+
if (!Array.isArray(value)) {
|
|
153
|
+
fail(`options.${key} must be an array of strings, received ${JSON.stringify(value)}.`);
|
|
154
|
+
}
|
|
155
|
+
const normalized = value.map((entry, index) => {
|
|
156
|
+
if (typeof entry !== 'string' || entry.trim() === '') {
|
|
157
|
+
fail(`options.${key}[${index}] must be a non-empty string, received ${JSON.stringify(entry)}.`);
|
|
158
|
+
}
|
|
159
|
+
// Namespaces are matched case-insensitively: the engine lower-cases `<C4/…>` before
|
|
160
|
+
// looking it up, so `'C4'` and `'c4'` have to mean the same thing here too.
|
|
161
|
+
return entry.trim().toLowerCase();
|
|
162
|
+
});
|
|
163
|
+
const duplicates = normalized.filter((entry, index) => normalized.indexOf(entry) !== index);
|
|
164
|
+
if (duplicates.length > 0) {
|
|
165
|
+
fail(`options.${key} contains duplicate entries: ${quoteList([...new Set(duplicates)])}.`);
|
|
166
|
+
}
|
|
167
|
+
return normalized;
|
|
168
|
+
}
|
|
169
|
+
function validateStdlibSource(value) {
|
|
170
|
+
if (value === undefined)
|
|
171
|
+
return [];
|
|
172
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
173
|
+
return entries.map((entry, index) => {
|
|
174
|
+
if (typeof entry !== 'string' || entry.trim() === '') {
|
|
175
|
+
const where = Array.isArray(value) ? `stdlib.source[${index}]` : 'stdlib.source';
|
|
176
|
+
fail(`options.${where} must be a non-empty string, received ${JSON.stringify(entry)}.`);
|
|
177
|
+
}
|
|
178
|
+
return entry.trim();
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Validates the nested `stdlib` group, accepting `false` as shorthand for `{enabled: false}`.
|
|
183
|
+
*/
|
|
184
|
+
function validateStdlib(value) {
|
|
185
|
+
if (value === undefined)
|
|
186
|
+
return { ...DEFAULT_STDLIB_OPTIONS, include: [], source: [] };
|
|
187
|
+
if (value === false)
|
|
188
|
+
return { ...DEFAULT_STDLIB_OPTIONS, enabled: false, include: [], source: [] };
|
|
189
|
+
if (!isPlainObject(value)) {
|
|
190
|
+
fail(`options.stdlib must be an object or false, received ${JSON.stringify(value)}.`);
|
|
191
|
+
}
|
|
192
|
+
const allowedKeys = ['enabled', 'include', 'source', 'namespaces'];
|
|
193
|
+
const unknownKeys = Object.keys(value).filter((key) => !allowedKeys.includes(key));
|
|
194
|
+
if (unknownKeys.length > 0) {
|
|
195
|
+
fail(`Unknown option${unknownKeys.length > 1 ? 's' : ''} ` +
|
|
196
|
+
`${quoteList(unknownKeys.map((key) => `stdlib.${key}`))}. ` +
|
|
197
|
+
`Supported options: ${quoteList(allowedKeys.map((key) => `stdlib.${key}`))}.`);
|
|
198
|
+
}
|
|
199
|
+
const include = value.include === undefined ? [] : validateNamespaceList(value.include, 'stdlib.include');
|
|
200
|
+
const source = validateStdlibSource(value.source);
|
|
201
|
+
const namespaces = value.namespaces === undefined
|
|
202
|
+
? null
|
|
203
|
+
: validateNamespaceList(value.namespaces, 'stdlib.namespaces');
|
|
204
|
+
if (namespaces !== null && namespaces.length === 0) {
|
|
205
|
+
fail('options.stdlib.namespaces must name at least one namespace. To turn the standard ' +
|
|
206
|
+
'library off entirely, use `stdlib: false`.');
|
|
207
|
+
}
|
|
208
|
+
if (include.length > 0 && source.length === 0) {
|
|
209
|
+
fail(`options.stdlib.include names ${quoteList(include)}, but options.stdlib.source is not ` +
|
|
210
|
+
'set, so there is nowhere to read them from. Point it at the `stdlib` directory of a ' +
|
|
211
|
+
'plantuml-stdlib checkout.');
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
enabled: validateBoolean(value.enabled, 'stdlib.enabled', DEFAULT_STDLIB_OPTIONS.enabled),
|
|
215
|
+
include,
|
|
216
|
+
source,
|
|
217
|
+
namespaces,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
143
220
|
function validateRenderTimeout(value) {
|
|
144
221
|
if (value === undefined)
|
|
145
222
|
return DEFAULT_OPTIONS.renderTimeoutMs;
|
|
@@ -175,6 +252,7 @@ export function resolveOptions(rawOptions) {
|
|
|
175
252
|
...DEFAULT_OPTIONS,
|
|
176
253
|
languages: [...DEFAULT_OPTIONS.languages],
|
|
177
254
|
graphviz: { ...DEFAULT_GRAPHVIZ_OPTIONS, languages: [...DEFAULT_GRAPHVIZ_OPTIONS.languages] },
|
|
255
|
+
stdlib: { ...DEFAULT_STDLIB_OPTIONS, include: [], source: [] },
|
|
178
256
|
};
|
|
179
257
|
}
|
|
180
258
|
if (!isPlainObject(rawOptions)) {
|
|
@@ -192,6 +270,7 @@ export function resolveOptions(rawOptions) {
|
|
|
192
270
|
'zoom',
|
|
193
271
|
'showSource',
|
|
194
272
|
'graphviz',
|
|
273
|
+
'stdlib',
|
|
195
274
|
// Docusaurus injects `id` for multi-instance plugins.
|
|
196
275
|
'id',
|
|
197
276
|
]);
|
|
@@ -209,6 +288,7 @@ export function resolveOptions(rawOptions) {
|
|
|
209
288
|
}
|
|
210
289
|
const languages = validateLanguages(rawOptions.languages, 'languages', DEFAULT_OPTIONS.languages);
|
|
211
290
|
const graphviz = validateGraphviz(rawOptions.graphviz);
|
|
291
|
+
const stdlib = validateStdlib(rawOptions.stdlib);
|
|
212
292
|
// A fence has exactly one language, so a language claimed by both engines would make the
|
|
213
293
|
// rendered output depend on the order the wrapper happens to check them in. Reject it.
|
|
214
294
|
if (graphviz.enabled) {
|
|
@@ -221,6 +301,7 @@ export function resolveOptions(rawOptions) {
|
|
|
221
301
|
return {
|
|
222
302
|
languages,
|
|
223
303
|
graphviz,
|
|
304
|
+
stdlib,
|
|
224
305
|
theme: theme ?? DEFAULT_OPTIONS.theme,
|
|
225
306
|
lazy: validateBoolean(rawOptions.lazy, 'lazy', DEFAULT_OPTIONS.lazy),
|
|
226
307
|
cache: cache ?? DEFAULT_OPTIONS.cache,
|
|
@@ -10,6 +10,17 @@ export interface LoadRuntimeOptions {
|
|
|
10
10
|
*/
|
|
11
11
|
importModule?: (url: string) => Promise<unknown>;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Injects a classic script once, reusing an existing tag for the same URL.
|
|
15
|
+
*
|
|
16
|
+
* Client-side navigation re-mounts diagram components but does not reload the document, so
|
|
17
|
+
* the marker attribute is what keeps a second `<script>` from ever being appended. The URL is
|
|
18
|
+
* the marker's value, because the standard library loader adds a tag per namespace and a
|
|
19
|
+
* bare marker could not tell them apart.
|
|
20
|
+
*
|
|
21
|
+
* @param label how the asset is named in an error message, e.g. `Graphviz runtime`
|
|
22
|
+
*/
|
|
23
|
+
export declare function loadClassicScript(url: string, timeoutMs: number, label: string): Promise<void>;
|
|
13
24
|
/**
|
|
14
25
|
* Resolves a ready-to-use Graphviz instance, loading `viz-global.js` on first use.
|
|
15
26
|
*
|
|
@@ -29,5 +40,6 @@ export declare function loadPlantUmlRuntime(options: LoadRuntimeOptions): Promis
|
|
|
29
40
|
export declare function isRuntimeRequested(): boolean;
|
|
30
41
|
/** Whether the Graphviz runtime has been requested on this page. For tests and diagnostics. */
|
|
31
42
|
export declare function isVizRuntimeRequested(): boolean;
|
|
32
|
-
|
|
43
|
+
export declare function onRuntimeReset(hook: () => void): void;
|
|
44
|
+
/** Test-only: forgets every cached runtime and removes the injected script tags. */
|
|
33
45
|
export declare function resetRuntimeLoader(): void;
|
|
@@ -28,31 +28,41 @@ function defaultImportModule(url) {
|
|
|
28
28
|
return import(/* webpackIgnore: true */ url);
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Injects
|
|
31
|
+
* Injects a classic script once, reusing an existing tag for the same URL.
|
|
32
32
|
*
|
|
33
33
|
* Client-side navigation re-mounts diagram components but does not reload the document, so
|
|
34
|
-
* the marker attribute is what keeps a second `<script>` from ever being appended.
|
|
34
|
+
* the marker attribute is what keeps a second `<script>` from ever being appended. The URL is
|
|
35
|
+
* the marker's value, because the standard library loader adds a tag per namespace and a
|
|
36
|
+
* bare marker could not tell them apart.
|
|
37
|
+
*
|
|
38
|
+
* @param label how the asset is named in an error message, e.g. `Graphviz runtime`
|
|
35
39
|
*/
|
|
36
|
-
function
|
|
37
|
-
const existing = document.querySelector(`script[${SCRIPT_MARKER}]`);
|
|
40
|
+
export function loadClassicScript(url, timeoutMs, label) {
|
|
41
|
+
const existing = document.querySelector(`script[${SCRIPT_MARKER}=${JSON.stringify(url)}]`);
|
|
38
42
|
if (existing) {
|
|
39
43
|
if (existing.dataset.plantumlRuntimeState === 'loaded')
|
|
40
44
|
return Promise.resolve();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
// A failed tag is discarded rather than reused. The caller that reaches this point has
|
|
46
|
+
// already dropped its cached promise in order to retry, and reusing the dead tag would
|
|
47
|
+
// make that retry impossible — a single flaky response would poison the page.
|
|
48
|
+
if (existing.dataset.plantumlRuntimeState === 'error')
|
|
49
|
+
existing.remove();
|
|
50
|
+
else
|
|
51
|
+
return waitForScript(existing, url, timeoutMs, label);
|
|
45
52
|
}
|
|
46
53
|
const script = document.createElement('script');
|
|
47
|
-
script.setAttribute(SCRIPT_MARKER,
|
|
54
|
+
script.setAttribute(SCRIPT_MARKER, url);
|
|
48
55
|
script.dataset.plantumlRuntimeState = 'loading';
|
|
49
56
|
script.async = false;
|
|
50
57
|
script.src = url;
|
|
51
|
-
const promise = waitForScript(script, url, timeoutMs);
|
|
58
|
+
const promise = waitForScript(script, url, timeoutMs, label);
|
|
52
59
|
document.head.appendChild(script);
|
|
53
60
|
return promise;
|
|
54
61
|
}
|
|
55
|
-
function
|
|
62
|
+
function loadVizGlobal(url, timeoutMs) {
|
|
63
|
+
return loadClassicScript(url, timeoutMs, 'Graphviz runtime');
|
|
64
|
+
}
|
|
65
|
+
function waitForScript(script, url, timeoutMs, label) {
|
|
56
66
|
return new Promise((resolve, reject) => {
|
|
57
67
|
let done = false;
|
|
58
68
|
const finish = (fn) => {
|
|
@@ -70,7 +80,7 @@ function waitForScript(script, url, timeoutMs) {
|
|
|
70
80
|
};
|
|
71
81
|
const onError = () => {
|
|
72
82
|
script.dataset.plantumlRuntimeState = 'error';
|
|
73
|
-
finish(() => reject(new PlantUmlError('load', `Failed to load the
|
|
83
|
+
finish(() => reject(new PlantUmlError('load', `Failed to load the ${label} from ${url}. ` +
|
|
74
84
|
'Check that the asset is served under your Docusaurus baseUrl with a JavaScript MIME type.')));
|
|
75
85
|
};
|
|
76
86
|
const timer = setTimeout(() => {
|
|
@@ -175,10 +185,23 @@ export function isRuntimeRequested() {
|
|
|
175
185
|
export function isVizRuntimeRequested() {
|
|
176
186
|
return cachedViz !== null;
|
|
177
187
|
}
|
|
178
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* Registers a callback for {@link resetRuntimeLoader}.
|
|
190
|
+
*
|
|
191
|
+
* The standard library loader keeps its own per-namespace cache but shares the script tags
|
|
192
|
+
* this module injects, so a reset that removed the tags without clearing that cache would
|
|
193
|
+
* leave it claiming namespaces are loaded when their globals are gone. The dependency points
|
|
194
|
+
* this way — loader to registry — so that `assetLoader` needs no import of its callers.
|
|
195
|
+
*/
|
|
196
|
+
const resetHooks = [];
|
|
197
|
+
export function onRuntimeReset(hook) {
|
|
198
|
+
resetHooks.push(hook);
|
|
199
|
+
}
|
|
200
|
+
/** Test-only: forgets every cached runtime and removes the injected script tags. */
|
|
179
201
|
export function resetRuntimeLoader() {
|
|
180
202
|
cachedRuntime = null;
|
|
181
203
|
cachedViz = null;
|
|
204
|
+
resetHooks.forEach((hook) => hook());
|
|
182
205
|
if (typeof document !== 'undefined') {
|
|
183
206
|
document.querySelectorAll(`script[${SCRIPT_MARKER}]`).forEach((node) => node.remove());
|
|
184
207
|
}
|
package/dist/runtime/cache.d.ts
CHANGED
|
@@ -4,6 +4,11 @@ export interface CacheKeyInput {
|
|
|
4
4
|
dark: boolean;
|
|
5
5
|
sanitized: boolean;
|
|
6
6
|
coreVersion: string;
|
|
7
|
+
/**
|
|
8
|
+
* Identifies the standard library the diagram was rendered against, so refreshing it
|
|
9
|
+
* invalidates the entries whose pictures it shaped. `null` when it is switched off.
|
|
10
|
+
*/
|
|
11
|
+
stdlibRevision: string | null;
|
|
7
12
|
}
|
|
8
13
|
export interface DiagramCache {
|
|
9
14
|
readonly mode: CacheMode;
|
|
@@ -13,7 +18,7 @@ export interface DiagramCache {
|
|
|
13
18
|
/** Number of entries currently held. Exposed for tests and the documented size limit. */
|
|
14
19
|
size(): number;
|
|
15
20
|
}
|
|
16
|
-
export declare function computeCacheKey({ source, dark, sanitized, coreVersion }: CacheKeyInput): string;
|
|
21
|
+
export declare function computeCacheKey({ source, dark, sanitized, coreVersion, stdlibRevision, }: CacheKeyInput): string;
|
|
17
22
|
export interface GraphvizCacheKeyInput {
|
|
18
23
|
source: string;
|
|
19
24
|
/** Graphviz layout engine; `dot` and `neato` draw different pictures from one source. */
|
package/dist/runtime/cache.js
CHANGED
|
@@ -15,12 +15,12 @@ function hash(input) {
|
|
|
15
15
|
}
|
|
16
16
|
return value.toString(36).padStart(7, '0');
|
|
17
17
|
}
|
|
18
|
-
export function computeCacheKey({ source, dark, sanitized, coreVersion }) {
|
|
18
|
+
export function computeCacheKey({ source, dark, sanitized, coreVersion, stdlibRevision, }) {
|
|
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}|${mode}|${clean}|${source.length}|${hash(source)}`;
|
|
23
|
+
return `${coreVersion}|${stdlibRevision ?? 'nostd'}|${mode}|${clean}|${source.length}|${hash(source)}`;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* Cache key for a Graphviz diagram.
|
package/dist/runtime/errors.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { VizRenderError } from './types.js';
|
|
2
|
-
export type PlantUmlErrorKind = 'load' | 'engine' | 'diagram' | 'syntax' | 'timeout' | 'config' | 'aborted' | 'too-large'
|
|
2
|
+
export type PlantUmlErrorKind = 'load' | 'engine' | 'diagram' | 'syntax' | 'timeout' | 'config' | 'aborted' | 'too-large'
|
|
3
|
+
/** A `!include <namespace/…>` the site cannot resolve. */
|
|
4
|
+
| 'stdlib';
|
|
3
5
|
/** Every failure surfaced to the UI is one of these, so the panel can explain what broke. */
|
|
4
6
|
export declare class PlantUmlError extends Error {
|
|
5
7
|
readonly kind: PlantUmlErrorKind;
|
package/dist/runtime/errors.js
CHANGED
|
@@ -18,6 +18,24 @@ const BOILERPLATE_PATTERNS = [
|
|
|
18
18
|
/^\s*$/,
|
|
19
19
|
];
|
|
20
20
|
const NOT_SUPPORTED_HEADING = 'Diagram not supported by this release of PlantUML';
|
|
21
|
+
/**
|
|
22
|
+
* Failures raised by PlantUML's *preprocessor* rather than by its parser.
|
|
23
|
+
*
|
|
24
|
+
* These matter far more now that `!include <namespace/…>` is resolvable. Calling a macro the
|
|
25
|
+
* included version does not define, naming a file that is not in the namespace, and an
|
|
26
|
+
* include the preprocessor could not resolve at all are the three likeliest ways a standard
|
|
27
|
+
* library diagram goes wrong. *None* of them produces the `Syntax Error?` marker below — the
|
|
28
|
+
* picture says only `Function not found RelIndex`, `cannot include <…>` or `Fatal parsing
|
|
29
|
+
* error` — so without this signature the engine's error card was passed through as a
|
|
30
|
+
* successful render.
|
|
31
|
+
*/
|
|
32
|
+
const PREPROCESSOR_FAILURE_PATTERN = /^\s*(?:function not found|cannot include|fatal parsing error)\b/i;
|
|
33
|
+
/**
|
|
34
|
+
* The header PlantUML puts above the source listing in every error picture. Required to
|
|
35
|
+
* co-occur with the line above, so that a diagram merely containing the words "cannot
|
|
36
|
+
* include" in a note is not mistaken for a failure.
|
|
37
|
+
*/
|
|
38
|
+
const SOURCE_LISTING_PATTERN = /^\[From .+\(line \d+\)/;
|
|
21
39
|
function textNodesOf(svg) {
|
|
22
40
|
if (typeof DOMParser === 'undefined')
|
|
23
41
|
return [];
|
|
@@ -49,8 +67,16 @@ export function detectDiagramError(svg) {
|
|
|
49
67
|
const isEmptyDescription = texts.some((text) => /Empty description/.test(text) && text.includes('(Assumed diagram type:'));
|
|
50
68
|
const isUnsupported = texts.some((text) => text.includes(NOT_SUPPORTED_HEADING)) &&
|
|
51
69
|
texts.some((text) => text.includes('is not recognized.'));
|
|
52
|
-
|
|
70
|
+
const preprocessorFailures = texts.filter((text) => PREPROCESSOR_FAILURE_PATTERN.test(text));
|
|
71
|
+
const isPreprocessorError = preprocessorFailures.length > 0 && texts.some((text) => SOURCE_LISTING_PATTERN.test(text));
|
|
72
|
+
if (!isSyntaxError && !isEmptyDescription && !isUnsupported && !isPreprocessorError)
|
|
53
73
|
return null;
|
|
74
|
+
// A preprocessor failure lists the *expanded* source, which for a standard library macro
|
|
75
|
+
// runs to thousands of lines. The failure itself is the only part worth showing; the
|
|
76
|
+
// reader still has the original fence a click away in the source view.
|
|
77
|
+
if (isPreprocessorError && !isSyntaxError) {
|
|
78
|
+
return preprocessorFailures.map((text) => text.trim()).join('\n');
|
|
79
|
+
}
|
|
54
80
|
const meaningful = texts
|
|
55
81
|
.map((text) => text.replace(/\s+$/, ''))
|
|
56
82
|
.filter((text) => !BOILERPLATE_PATTERNS.some((pattern) => pattern.test(text)));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type LoadRuntimeOptions } from './assetLoader.js';
|
|
2
2
|
import { type DiagramCache } from './cache.js';
|
|
3
|
+
import { type StdlibRuntime } from './stdlibLoader.js';
|
|
3
4
|
export interface RenderDiagramRequest {
|
|
4
5
|
source: string;
|
|
5
6
|
dark: boolean;
|
|
@@ -7,6 +8,8 @@ export interface RenderDiagramRequest {
|
|
|
7
8
|
timeoutMs: number;
|
|
8
9
|
assetsBaseUrl: string;
|
|
9
10
|
coreVersion: string;
|
|
11
|
+
/** Standard library assets for this site, or `null` when the feature is switched off. */
|
|
12
|
+
stdlib: StdlibRuntime | null;
|
|
10
13
|
cache: DiagramCache;
|
|
11
14
|
signal?: AbortSignal;
|
|
12
15
|
/** Reports which long-running phase is in progress, so the UI can show an accurate state. */
|
package/dist/runtime/renderer.js
CHANGED
|
@@ -3,6 +3,7 @@ import { computeCacheKey } from './cache.js';
|
|
|
3
3
|
import { describeEngineError, detectDiagramError, PlantUmlError } from './errors.js';
|
|
4
4
|
import { enqueueRender } from './queue.js';
|
|
5
5
|
import { sanitizeSvgMarkup } from './sanitize.js';
|
|
6
|
+
import { loadStdlibForSource } from './stdlibLoader.js';
|
|
6
7
|
/**
|
|
7
8
|
* `renderToString` takes its options as a *fourth* argument, after the two callbacks.
|
|
8
9
|
* This was verified against the installed `@plantuml/core` rather than assumed: the
|
|
@@ -41,19 +42,32 @@ function renderWithEngine(engine, source, dark) {
|
|
|
41
42
|
* {@link enqueueRender} because the engine cannot render two diagrams at once.
|
|
42
43
|
*/
|
|
43
44
|
export async function renderDiagram(request) {
|
|
44
|
-
const { source, dark, sanitize, timeoutMs, cache, coreVersion, signal, onPhase } = request;
|
|
45
|
-
const cacheKey = computeCacheKey({
|
|
45
|
+
const { source, dark, sanitize, timeoutMs, cache, coreVersion, stdlib, signal, onPhase } = request;
|
|
46
|
+
const cacheKey = computeCacheKey({
|
|
47
|
+
source,
|
|
48
|
+
dark,
|
|
49
|
+
sanitized: sanitize,
|
|
50
|
+
coreVersion,
|
|
51
|
+
stdlibRevision: stdlib?.manifest.revision ?? null,
|
|
52
|
+
});
|
|
46
53
|
const cached = cache.get(cacheKey);
|
|
47
54
|
if (cached !== undefined)
|
|
48
55
|
return cached;
|
|
49
56
|
// Loading happens outside the queue: it is shared, idempotent work, and holding the
|
|
50
57
|
// single render slot for an 8 MB download would stall every other diagram on the page.
|
|
58
|
+
//
|
|
59
|
+
// The standard library loads alongside the engine rather than after it. The two are
|
|
60
|
+
// independent downloads, and the engine cannot be asked for a diagram whose includes are
|
|
61
|
+
// not resolvable yet — by the time it looks, the namespaces have to be in place already.
|
|
51
62
|
onPhase?.('loading');
|
|
52
|
-
const engine = await
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
const [engine] = await Promise.all([
|
|
64
|
+
loadPlantUmlRuntime({
|
|
65
|
+
assetsBaseUrl: request.assetsBaseUrl,
|
|
66
|
+
timeoutMs,
|
|
67
|
+
importModule: request.importModule,
|
|
68
|
+
}),
|
|
69
|
+
loadStdlibForSource({ source, stdlib, timeoutMs }),
|
|
70
|
+
]);
|
|
57
71
|
onPhase?.('rendering');
|
|
58
72
|
const svg = await enqueueRender(async () => {
|
|
59
73
|
const raw = await renderWithEngine(engine, source, dark);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type StdlibRuntimeManifest } from '../stdlibShared.js';
|
|
2
|
+
export interface StdlibRuntime {
|
|
3
|
+
/** Absolute, `baseUrl`-prefixed URL of the directory holding the namespace bundles. */
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
manifest: StdlibRuntimeManifest;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The namespaces a source needs, in load order, including what those namespaces need in turn.
|
|
9
|
+
*
|
|
10
|
+
* Dependencies come from the manifest rather than from parsing the library sources in the
|
|
11
|
+
* browser: `k8s` includes `<c4/…>` from its own `Common.puml`, and the engine would ask for
|
|
12
|
+
* it mid-render, far too late to fetch anything. Example-only dependencies are pulled in
|
|
13
|
+
* solely when the diagram includes something from `_examples_/` — C4's examples reference
|
|
14
|
+
* `office`, and charging every C4 diagram 160 KB for that would be absurd.
|
|
15
|
+
*
|
|
16
|
+
* A dependency the site does not provide is skipped rather than raised: it may well sit
|
|
17
|
+
* behind an `!if` the diagram never takes, and failing here would break diagrams that work.
|
|
18
|
+
*/
|
|
19
|
+
export declare function collectStdlibNamespaces(source: string, manifest: StdlibRuntimeManifest): string[];
|
|
20
|
+
export interface LoadStdlibOptions {
|
|
21
|
+
source: string;
|
|
22
|
+
stdlib: StdlibRuntime | null;
|
|
23
|
+
timeoutMs: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Loads whatever the source needs from the standard library. Resolves immediately when it
|
|
27
|
+
* needs nothing, which is the common case and must stay free.
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadStdlibForSource({ source, stdlib, timeoutMs, }: LoadStdlibOptions): Promise<void>;
|
|
30
|
+
/** Whether a namespace bundle has been requested on this page. For tests and diagnostics. */
|
|
31
|
+
export declare function isStdlibNamespaceRequested(url: string): boolean;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { findStdlibIncludes } from '../stdlibShared.js';
|
|
2
|
+
import { loadClassicScript, onRuntimeReset } from './assetLoader.js';
|
|
3
|
+
import { PlantUmlError } from './errors.js';
|
|
4
|
+
/** One in-flight or settled load per bundle URL, shared by every diagram on the page. */
|
|
5
|
+
const loaded = new Map();
|
|
6
|
+
onRuntimeReset(() => loaded.clear());
|
|
7
|
+
function bundleFileName(namespace) {
|
|
8
|
+
return `${namespace}.min.js`;
|
|
9
|
+
}
|
|
10
|
+
function joinUrl(base, file) {
|
|
11
|
+
return `${base.replace(/\/+$/, '')}/${file}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Tells the engine the namespace bundle is already in the document.
|
|
15
|
+
*
|
|
16
|
+
* The key is the bare file name because that is exactly the `src` the engine would have set,
|
|
17
|
+
* and its bookkeeping is keyed by that string rather than by the resolved URL.
|
|
18
|
+
*/
|
|
19
|
+
function markLoadedForEngine(namespace) {
|
|
20
|
+
const engineWindow = window;
|
|
21
|
+
const state = (engineWindow.__pl_script_state ??= Object.create(null));
|
|
22
|
+
state[bundleFileName(namespace)] = { state: 'loaded', ok: [], err: [] };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The namespaces a source needs, in load order, including what those namespaces need in turn.
|
|
26
|
+
*
|
|
27
|
+
* Dependencies come from the manifest rather than from parsing the library sources in the
|
|
28
|
+
* browser: `k8s` includes `<c4/…>` from its own `Common.puml`, and the engine would ask for
|
|
29
|
+
* it mid-render, far too late to fetch anything. Example-only dependencies are pulled in
|
|
30
|
+
* solely when the diagram includes something from `_examples_/` — C4's examples reference
|
|
31
|
+
* `office`, and charging every C4 diagram 160 KB for that would be absurd.
|
|
32
|
+
*
|
|
33
|
+
* A dependency the site does not provide is skipped rather than raised: it may well sit
|
|
34
|
+
* behind an `!if` the diagram never takes, and failing here would break diagrams that work.
|
|
35
|
+
*/
|
|
36
|
+
export function collectStdlibNamespaces(source, manifest) {
|
|
37
|
+
const references = findStdlibIncludes(source);
|
|
38
|
+
const ordered = [];
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const visit = (namespace, withExamples, direct) => {
|
|
41
|
+
const entry = manifest.namespaces[namespace];
|
|
42
|
+
if (!entry) {
|
|
43
|
+
if (!direct)
|
|
44
|
+
return;
|
|
45
|
+
throw new PlantUmlError('stdlib', `This diagram includes <${namespace}/…>, but the '${namespace}' standard library ` +
|
|
46
|
+
"namespace is not available on this site. Add it to the plugin's " +
|
|
47
|
+
`\`stdlib.include\` option and point \`stdlib.source\` at a plantuml-stdlib ` +
|
|
48
|
+
'checkout that contains it.');
|
|
49
|
+
}
|
|
50
|
+
if (seen.has(namespace))
|
|
51
|
+
return;
|
|
52
|
+
seen.add(namespace);
|
|
53
|
+
for (const dependency of entry.dependencies)
|
|
54
|
+
visit(dependency, false, false);
|
|
55
|
+
if (withExamples) {
|
|
56
|
+
for (const dependency of entry.exampleDependencies)
|
|
57
|
+
visit(dependency, false, false);
|
|
58
|
+
}
|
|
59
|
+
ordered.push(namespace);
|
|
60
|
+
};
|
|
61
|
+
for (const reference of references) {
|
|
62
|
+
visit(reference.namespace, reference.fromExamples, true);
|
|
63
|
+
}
|
|
64
|
+
return ordered;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Loads whatever the source needs from the standard library. Resolves immediately when it
|
|
68
|
+
* needs nothing, which is the common case and must stay free.
|
|
69
|
+
*/
|
|
70
|
+
export async function loadStdlibForSource({ source, stdlib, timeoutMs, }) {
|
|
71
|
+
const references = findStdlibIncludes(source);
|
|
72
|
+
if (references.length === 0)
|
|
73
|
+
return;
|
|
74
|
+
if (!stdlib) {
|
|
75
|
+
const names = [...new Set(references.map((reference) => reference.namespace))];
|
|
76
|
+
throw new PlantUmlError('stdlib', `This diagram includes ${names.map((name) => `<${name}/…>`).join(', ')} from the ` +
|
|
77
|
+
'PlantUML standard library, which is switched off for this site (`stdlib: false`).');
|
|
78
|
+
}
|
|
79
|
+
const namespaces = collectStdlibNamespaces(source, stdlib.manifest);
|
|
80
|
+
await Promise.all(namespaces.map(async (namespace) => {
|
|
81
|
+
const url = joinUrl(stdlib.baseUrl, bundleFileName(namespace));
|
|
82
|
+
let pending = loaded.get(url);
|
|
83
|
+
if (!pending) {
|
|
84
|
+
pending = loadClassicScript(url, timeoutMs, `standard library namespace '${namespace}'`)
|
|
85
|
+
.then(() => markLoadedForEngine(namespace))
|
|
86
|
+
.catch((error) => {
|
|
87
|
+
// Clearing the entry lets a later diagram retry rather than inherit the failure.
|
|
88
|
+
loaded.delete(url);
|
|
89
|
+
throw error;
|
|
90
|
+
});
|
|
91
|
+
loaded.set(url, pending);
|
|
92
|
+
}
|
|
93
|
+
await pending;
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
/** Whether a namespace bundle has been requested on this page. For tests and diagnostics. */
|
|
97
|
+
export function isStdlibNamespaceRequested(url) {
|
|
98
|
+
return loaded.has(url);
|
|
99
|
+
}
|