@riebeckite/plugin-mermaid 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,4 +81,5 @@ export default defineConfig({
81
81
 
82
82
  ## See also
83
83
 
84
- - [Plugin guide](../../../docs/en/reference/plugin-api.md)
84
+ - [Plugin guide](../../../docs/en/docs/reference/plugin-api.md)
85
+
package/README_ja.md CHANGED
@@ -56,4 +56,5 @@ export default defineConfig({
56
56
 
57
57
  ## 関連資料
58
58
 
59
- - [プラグインシステム](../../../docs/ja/reference/plugin-api.md)
59
+ - [プラグインシステム](../../../docs/ja/docs/reference/plugin-api.md)
60
+
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import type { MermaidOptions } from "./src/types.js";
2
- export type { MermaidClientOptions, MermaidOptions, MermaidRenderMode, MermaidTheme, } from "./src/types.js";
2
+ export type { MermaidClientOptions, MermaidOptions, MermaidRenderMode, MermaidRenderSession, MermaidTheme, } from "./src/types.js";
3
3
  export declare function mermaid(options?: MermaidOptions): import("@riebeckite/core").RiebeckitePlugin<MermaidOptions>;
package/dist/index.js CHANGED
@@ -63,175 +63,12 @@ var init_hast = __esm({
63
63
  }
64
64
  });
65
65
 
66
- // src/render-static.ts
67
- var render_static_exports = {};
68
- __export(render_static_exports, {
69
- renderMermaidStaticSvg: () => renderMermaidStaticSvg
70
- });
71
- import { access, mkdtemp, rm, writeFile } from "node:fs/promises";
72
- import { tmpdir } from "node:os";
73
- import { join } from "node:path";
74
- import { fileURLToPath } from "node:url";
75
- async function renderWithBrowser(scriptPath) {
76
- const browser = await launchBrowser();
77
- const page = await browser.newPage();
78
- try {
79
- page.setDefaultTimeout(RENDER_TIMEOUT_MS);
80
- await page.setContent("<!doctype html><html><body></body></html>");
81
- await page.addScriptTag({ path: await resolveMermaidScriptPath() });
82
- await page.addScriptTag({ path: scriptPath });
83
- const result = await page.evaluate(() => {
84
- return globalThis.__rrMermaidResult;
85
- });
86
- return normalizeRenderResult(result);
87
- } catch (error) {
88
- return {
89
- ok: false,
90
- kind: "renderer-error",
91
- message: formatError(error)
92
- };
93
- } finally {
94
- await page.close();
95
- await browser.close();
96
- }
97
- }
98
- async function resolveMermaidScriptPath() {
99
- const candidates = [
100
- resolveWithImportMeta(),
101
- fileURLToPath(
102
- new URL(`../node_modules/${MERMAID_SCRIPT_SUBPATH}`, import.meta.url)
103
- ),
104
- join(process.cwd(), "node_modules", MERMAID_SCRIPT_SUBPATH),
105
- join(
106
- process.cwd(),
107
- "..",
108
- "..",
109
- "packages",
110
- "plugin-mermaid",
111
- "node_modules",
112
- MERMAID_SCRIPT_SUBPATH
113
- )
114
- ].filter((path) => typeof path === "string");
115
- for (const candidate of candidates) {
116
- if (await fileExists(candidate)) return candidate;
117
- }
118
- return candidates.at(0) ?? MERMAID_SCRIPT_SUBPATH;
119
- }
120
- function resolveWithImportMeta() {
121
- const resolver = import.meta.resolve;
122
- if (typeof resolver !== "function") return void 0;
123
- return fileURLToPath(resolver(MERMAID_SCRIPT_SUBPATH));
124
- }
125
- async function fileExists(path) {
126
- try {
127
- await access(path);
128
- return true;
129
- } catch {
130
- return false;
131
- }
132
- }
133
- async function launchBrowser() {
134
- const puppeteer = await importPuppeteer();
135
- return puppeteer.default.launch({
136
- headless: "shell",
137
- args: ["--no-sandbox", "--disable-setuid-sandbox"]
138
- });
139
- }
140
- async function importPuppeteer() {
141
- const importModule = new Function(
142
- "specifier",
143
- "return import(specifier)"
144
- );
145
- return importModule("puppeteer");
146
- }
147
- function buildRenderScript(id, source, theme) {
148
- return `
149
- (async () => {
150
- try {
151
- mermaid.initialize({
152
- startOnLoad: false,
153
- securityLevel: "strict",
154
- theme: ${JSON.stringify(theme)},
155
- });
156
- const { svg } = await mermaid.render(
157
- ${JSON.stringify(id)},
158
- ${JSON.stringify(source)},
159
- );
160
- globalThis.__rrMermaidResult = { ok: true, svg };
161
- } catch (error) {
162
- globalThis.__rrMermaidResult = {
163
- ok: false,
164
- kind: "invalid-diagram",
165
- message: error instanceof Error ? error.message : String(error),
166
- };
167
- }
168
- })();
169
- `;
170
- }
171
- function normalizeRenderResult(value) {
172
- if (!value || typeof value !== "object") {
173
- return {
174
- ok: false,
175
- kind: "renderer-error",
176
- message: "Mermaid renderer returned no result."
177
- };
178
- }
179
- const result = value;
180
- if (result.ok === true && typeof result.svg === "string") {
181
- return { ok: true, svg: result.svg };
182
- }
183
- if (result.ok === false) {
184
- return {
185
- ok: false,
186
- kind: normalizeErrorKind(result.kind),
187
- message: typeof result.message === "string" ? result.message : "Mermaid renderer failed."
188
- };
189
- }
190
- return {
191
- ok: false,
192
- kind: "renderer-error",
193
- message: "Mermaid renderer returned an invalid result."
194
- };
195
- }
196
- function normalizeErrorKind(value) {
197
- return value === "invalid-diagram" || value === "renderer-error" ? value : "renderer-error";
198
- }
199
- async function enqueueMermaidRender(render) {
200
- const current = renderQueue.then(render, render);
201
- renderQueue = current.catch(() => void 0);
202
- return current;
203
- }
204
- function formatError(error) {
205
- if (error instanceof Error) return error.message;
206
- return String(error);
207
- }
208
- var RENDER_TIMEOUT_MS, MERMAID_SCRIPT_SUBPATH, renderQueue, renderMermaidStaticSvg;
209
- var init_render_static = __esm({
210
- "src/render-static.ts"() {
211
- RENDER_TIMEOUT_MS = 3e4;
212
- MERMAID_SCRIPT_SUBPATH = "mermaid/dist/mermaid.min.js";
213
- renderQueue = Promise.resolve();
214
- renderMermaidStaticSvg = async (id, source, theme) => {
215
- return enqueueMermaidRender(async () => {
216
- const workDir = await mkdtemp(join(tmpdir(), "riebeckite-mermaid-"));
217
- const scriptPath = join(workDir, "render-mermaid.js");
218
- try {
219
- await writeFile(scriptPath, buildRenderScript(id, source, theme), "utf8");
220
- return await renderWithBrowser(scriptPath);
221
- } finally {
222
- await rm(workDir, { force: true, recursive: true });
223
- }
224
- });
225
- };
226
- }
227
- });
228
-
229
66
  // src/rehype.ts
230
67
  var rehype_exports = {};
231
68
  __export(rehype_exports, {
232
69
  rehypeMermaid: () => rehypeMermaid
233
70
  });
234
- function rehypeMermaid(options = {}) {
71
+ function rehypeMermaid(options = {}, session) {
235
72
  const renderMode = options.render ?? "build";
236
73
  const theme = options.theme ?? DEFAULT_THEME;
237
74
  return async (tree, file) => {
@@ -243,7 +80,8 @@ function rehypeMermaid(options = {}) {
243
80
  render: renderMode,
244
81
  theme,
245
82
  caption: options.caption !== false,
246
- fallback: options.fallback !== false
83
+ fallback: options.fallback !== false,
84
+ session
247
85
  })
248
86
  );
249
87
  });
@@ -255,7 +93,7 @@ async function replaceMermaidBlock(parent, index, pre, file, options) {
255
93
  const source = code ? getTextContent(code).trim() : getTextContent(pre).trim();
256
94
  const caption = options.caption ? extractCaption(source, pre, code) : null;
257
95
  const id = `rr-mermaid-${hashSource(source)}`;
258
- const renderResult = shouldRenderAtBuild(options.render) ? await renderStaticSvg(id, source, options.theme, file) : null;
96
+ const renderResult = shouldRenderAtBuild(options.render) ? await renderStaticSvg(id, source, options.theme, file, options.session) : null;
259
97
  const staticSvg = renderResult?.ok ? renderResult.svg : null;
260
98
  parent.children = parent.children ?? [];
261
99
  parent.children[index] = buildFigure({
@@ -307,10 +145,9 @@ function buildFigure(input) {
307
145
  children
308
146
  );
309
147
  }
310
- async function renderStaticSvg(id, source, theme, file) {
148
+ async function renderStaticSvg(id, source, theme, file, session) {
311
149
  try {
312
- const { renderMermaidStaticSvg: renderMermaidStaticSvg2 } = await Promise.resolve().then(() => (init_render_static(), render_static_exports));
313
- const result = await renderMermaidStaticSvg2(
150
+ const result = await session.render(
314
151
  id,
315
152
  source,
316
153
  selectTheme(theme, "light")
@@ -390,14 +227,243 @@ import {
390
227
  createStyleAsset,
391
228
  definePlugin
392
229
  } from "@riebeckite/core";
230
+
231
+ // src/render-static.ts
232
+ import { access } from "node:fs/promises";
233
+ import { join } from "node:path";
234
+ import { fileURLToPath } from "node:url";
235
+ var RENDER_TIMEOUT_MS = 3e4;
236
+ var MERMAID_SCRIPT_SUBPATH = "mermaid/dist/mermaid.min.js";
237
+ function createMermaidRenderSession(dependencies = {}) {
238
+ const launchBrowser = dependencies.launch ?? defaultLaunchBrowser;
239
+ let state = null;
240
+ let queue = Promise.resolve();
241
+ let exitListenerAttached = false;
242
+ const handleExit = () => {
243
+ const browser = state?.browser;
244
+ state = null;
245
+ if (!browser) return;
246
+ try {
247
+ browser.process()?.kill();
248
+ } catch {
249
+ }
250
+ };
251
+ function attachExitListener() {
252
+ if (exitListenerAttached) return;
253
+ process.once("exit", handleExit);
254
+ exitListenerAttached = true;
255
+ }
256
+ function detachExitListener() {
257
+ if (!exitListenerAttached) return;
258
+ process.removeListener("exit", handleExit);
259
+ exitListenerAttached = false;
260
+ }
261
+ async function ensureSession() {
262
+ if (state) return state;
263
+ const browser = await launchBrowser();
264
+ attachExitListener();
265
+ try {
266
+ const page = await browser.newPage();
267
+ page.setDefaultTimeout(RENDER_TIMEOUT_MS);
268
+ await page.setContent("<!doctype html><html><body></body></html>");
269
+ await page.addScriptTag({ path: await resolveMermaidScriptPath() });
270
+ state = { browser, page };
271
+ return state;
272
+ } catch (error) {
273
+ await teardownBrowser(browser);
274
+ detachExitListener();
275
+ throw error;
276
+ }
277
+ }
278
+ async function teardownBrowser(browser) {
279
+ try {
280
+ await browser.close();
281
+ } catch {
282
+ }
283
+ }
284
+ async function teardown() {
285
+ const current = state;
286
+ state = null;
287
+ detachExitListener();
288
+ if (current) await teardownBrowser(current.browser);
289
+ }
290
+ async function evaluateRender(session, id, source, theme) {
291
+ const result = await session.page.evaluate(
292
+ renderMermaidInPage,
293
+ id,
294
+ source,
295
+ theme
296
+ );
297
+ return normalizeRenderResult(result);
298
+ }
299
+ async function renderWithRecovery(id, source, theme) {
300
+ let established = false;
301
+ try {
302
+ const session = await ensureSession();
303
+ established = true;
304
+ return await evaluateRender(session, id, source, theme);
305
+ } catch (error) {
306
+ await teardown();
307
+ if (!established) {
308
+ return rendererErrorResult(error);
309
+ }
310
+ try {
311
+ const session = await ensureSession();
312
+ return await evaluateRender(session, id, source, theme);
313
+ } catch (retryError) {
314
+ await teardown();
315
+ return rendererErrorResult(retryError);
316
+ }
317
+ }
318
+ }
319
+ function enqueue(task) {
320
+ const run = queue.then(task, task);
321
+ queue = run.then(
322
+ () => void 0,
323
+ () => void 0
324
+ );
325
+ return run;
326
+ }
327
+ return {
328
+ render(id, source, theme) {
329
+ return enqueue(() => renderWithRecovery(id, source, theme));
330
+ },
331
+ dispose() {
332
+ return enqueue(() => teardown());
333
+ }
334
+ };
335
+ }
336
+ async function renderMermaidInPage(id, source, theme) {
337
+ const mermaid2 = globalThis.mermaid;
338
+ if (!mermaid2) {
339
+ return {
340
+ ok: false,
341
+ kind: "renderer-error",
342
+ message: "Mermaid script is not loaded."
343
+ };
344
+ }
345
+ try {
346
+ document.body.innerHTML = "";
347
+ mermaid2.initialize({
348
+ startOnLoad: false,
349
+ securityLevel: "strict",
350
+ theme
351
+ });
352
+ const { svg } = await mermaid2.render(id, source);
353
+ return { ok: true, svg };
354
+ } catch (error) {
355
+ return {
356
+ ok: false,
357
+ kind: "invalid-diagram",
358
+ message: error instanceof Error ? error.message : String(error)
359
+ };
360
+ }
361
+ }
362
+ async function defaultLaunchBrowser() {
363
+ const puppeteer = await importPuppeteer();
364
+ return puppeteer.default.launch({
365
+ headless: "shell",
366
+ args: ["--no-sandbox", "--disable-setuid-sandbox"]
367
+ });
368
+ }
369
+ async function importPuppeteer() {
370
+ const importModule = new Function(
371
+ "specifier",
372
+ "return import(specifier)"
373
+ );
374
+ return importModule("puppeteer");
375
+ }
376
+ async function resolveMermaidScriptPath() {
377
+ const candidates = [
378
+ resolveWithImportMeta(),
379
+ fileURLToPath(
380
+ new URL(`../node_modules/${MERMAID_SCRIPT_SUBPATH}`, import.meta.url)
381
+ ),
382
+ join(process.cwd(), "node_modules", MERMAID_SCRIPT_SUBPATH),
383
+ join(
384
+ process.cwd(),
385
+ "..",
386
+ "..",
387
+ "packages",
388
+ "plugin-mermaid",
389
+ "node_modules",
390
+ MERMAID_SCRIPT_SUBPATH
391
+ )
392
+ ].filter((path) => typeof path === "string");
393
+ for (const candidate of candidates) {
394
+ if (await fileExists(candidate)) return candidate;
395
+ }
396
+ return candidates.at(0) ?? MERMAID_SCRIPT_SUBPATH;
397
+ }
398
+ function resolveWithImportMeta() {
399
+ const resolver = import.meta.resolve;
400
+ if (typeof resolver !== "function") return void 0;
401
+ return fileURLToPath(resolver(MERMAID_SCRIPT_SUBPATH));
402
+ }
403
+ async function fileExists(path) {
404
+ try {
405
+ await access(path);
406
+ return true;
407
+ } catch {
408
+ return false;
409
+ }
410
+ }
411
+ function rendererErrorResult(error) {
412
+ return { ok: false, kind: "renderer-error", message: formatError(error) };
413
+ }
414
+ function normalizeRenderResult(value) {
415
+ if (!value || typeof value !== "object") {
416
+ return {
417
+ ok: false,
418
+ kind: "renderer-error",
419
+ message: "Mermaid renderer returned no result."
420
+ };
421
+ }
422
+ const result = value;
423
+ if (result.ok === true && typeof result.svg === "string") {
424
+ return { ok: true, svg: result.svg };
425
+ }
426
+ if (result.ok === false) {
427
+ return {
428
+ ok: false,
429
+ kind: normalizeErrorKind(result.kind),
430
+ message: typeof result.message === "string" ? result.message : "Mermaid renderer failed."
431
+ };
432
+ }
433
+ return {
434
+ ok: false,
435
+ kind: "renderer-error",
436
+ message: "Mermaid renderer returned an invalid result."
437
+ };
438
+ }
439
+ function normalizeErrorKind(value) {
440
+ return value === "invalid-diagram" || value === "renderer-error" ? value : "renderer-error";
441
+ }
442
+ function formatError(error) {
443
+ if (error instanceof Error) return error.message;
444
+ return String(error);
445
+ }
446
+
447
+ // index.ts
393
448
  function mermaid(options = {}) {
449
+ const session = createMermaidRenderSession();
394
450
  return definePlugin({
395
451
  name: "mermaid",
396
452
  order: -10,
453
+ processedContentCache: {
454
+ version: "mermaid-v1",
455
+ dependencyMode: "none"
456
+ },
397
457
  options,
398
458
  validateOptions: validateMermaidOptions,
399
459
  extendHtmlPipeline: (pipeline) => {
400
- pipeline.use(rehypeMermaidLazy, options);
460
+ pipeline.use(rehypeMermaidLazy, { options, session });
461
+ },
462
+ buildEnd: async () => {
463
+ await session.dispose();
464
+ },
465
+ dispose: async () => {
466
+ await session.dispose();
401
467
  },
402
468
  assets: [createStyleAsset("mermaid")],
403
469
  clientEntries: [createClientEntry("mermaid", "initMermaidDiagrams")]
@@ -428,10 +494,10 @@ function validateMermaidOptions(options) {
428
494
  function isMermaidTheme(value) {
429
495
  return value === void 0 || typeof value === "string" || typeof value === "object" && value !== null && "light" in value && "dark" in value && typeof value.light === "string" && typeof value.dark === "string";
430
496
  }
431
- function rehypeMermaidLazy(options = {}) {
497
+ function rehypeMermaidLazy(settings) {
432
498
  return async (tree, file) => {
433
499
  const { rehypeMermaid: rehypeMermaid2 } = await Promise.resolve().then(() => (init_rehype(), rehype_exports));
434
- const transformer = rehypeMermaid2(options);
500
+ const transformer = rehypeMermaid2(settings.options, settings.session);
435
501
  await transformer(tree, file);
436
502
  };
437
503
  }
@@ -1,2 +1,2 @@
1
- import type { HastNode, MermaidOptions } from "./types.js";
2
- export declare function rehypeMermaid(options?: MermaidOptions): (tree: HastNode, file: unknown) => Promise<void>;
1
+ import type { HastNode, MermaidOptions, MermaidRenderSession } from "./types.js";
2
+ export declare function rehypeMermaid(options: MermaidOptions, session: MermaidRenderSession): (tree: HastNode, file: unknown) => Promise<void>;
@@ -1,2 +1,20 @@
1
- import type { MermaidBuildRenderer } from "./types.js";
2
- export declare const renderMermaidStaticSvg: MermaidBuildRenderer;
1
+ import type { Browser } from "puppeteer";
2
+ import type { MermaidRenderSession } from "./types.js";
3
+ export type MermaidSessionDependencies = {
4
+ /**
5
+ * Overrides browser launch. Tests inject a fake browser so session
6
+ * lifecycle can be asserted without launching Chrome.
7
+ */
8
+ launch?: () => Promise<Browser>;
9
+ };
10
+ /**
11
+ * Creates a build-scoped Mermaid render session.
12
+ *
13
+ * The session owns one browser + one page and injects `mermaid.min.js` a
14
+ * single time; every diagram render reuses that page through
15
+ * `page.evaluate`. Each `mermaid()` plugin instance creates its own
16
+ * session, so resource ownership stays with the plugin rather than a
17
+ * module-global singleton. `dispose` releases the browser and is wired to
18
+ * the plugin `buildEnd`/`dispose` hooks.
19
+ */
20
+ export declare function createMermaidRenderSession(dependencies?: MermaidSessionDependencies): MermaidRenderSession;
@@ -19,6 +19,14 @@ export type MermaidBuildRenderResult = {
19
19
  message: string;
20
20
  };
21
21
  export type MermaidBuildRenderer = (id: string, source: string, theme: string) => Promise<MermaidBuildRenderResult>;
22
+ /**
23
+ * Build-scoped render resource. Owns one browser/page session, serializes
24
+ * renders, and releases the browser on `dispose`.
25
+ */
26
+ export type MermaidRenderSession = {
27
+ render: MermaidBuildRenderer;
28
+ dispose: () => Promise<void>;
29
+ };
22
30
  export type MermaidClientOptions = {
23
31
  theme?: MermaidTheme;
24
32
  mermaid?: MermaidApi;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riebeckite/plugin-mermaid",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "dependencies": {
24
24
  "mermaid": "^11.12.2",
25
25
  "puppeteer": "^25.11.0",
26
- "@riebeckite/core": "0.0.14"
26
+ "@riebeckite/core": "0.0.15"
27
27
  },
28
28
  "publishConfig": {
29
29
  "access": "public"
@@ -58,6 +58,7 @@
58
58
  "./style.css"
59
59
  ],
60
60
  "scripts": {
61
- "build": "node ../../../scripts/build_package.mjs"
61
+ "build": "node ../../../scripts/build_package.mjs",
62
+ "test": "node --import tsx --test \"test/*.test.ts\""
62
63
  }
63
64
  }