@mermaid-js/mermaid-cli 11.12.0 → 11.15.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.
Files changed (76) hide show
  1. package/dist/index.html +0 -2
  2. package/dist-types/src/index.d.ts +16 -1
  3. package/dist-types/src/index.d.ts.map +1 -1
  4. package/dist-types/src/puppeteerIntercept.d.ts +32 -0
  5. package/dist-types/src/puppeteerIntercept.d.ts.map +1 -0
  6. package/dist-types/src/version.d.ts +1 -1
  7. package/package.json +18 -14
  8. package/src/index.js +126 -49
  9. package/src/puppeteerIntercept.js +122 -0
  10. package/src/version.js +1 -1
  11. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  12. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  13. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  15. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  19. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  21. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  25. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  26. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  27. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  28. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  29. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  30. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  31. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  32. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  33. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  34. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  35. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  36. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  37. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  39. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  40. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  41. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  42. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  43. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  52. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  53. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  54. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  55. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  56. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  57. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  58. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  59. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  60. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  61. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  62. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  63. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  64. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  65. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  66. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  67. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  68. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  69. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  70. package/dist/assets/fa-brands-400-Dur5g48u.ttf +0 -0
  71. package/dist/assets/fa-brands-400-O7nZalfM.woff2 +0 -0
  72. package/dist/assets/fa-regular-400-Bf3rG5Nx.ttf +0 -0
  73. package/dist/assets/fa-regular-400-DgEfZSYE.woff2 +0 -0
  74. package/dist/assets/fa-solid-900-BV3CbEM2.ttf +0 -0
  75. package/dist/assets/fa-solid-900-DOQJEhcS.woff2 +0 -0
  76. package/dist/assets/index-Bv5aVo5X.js +0 -38
package/dist/index.html CHANGED
@@ -1,7 +1,5 @@
1
1
  <!doctype html>
2
2
  <html>
3
- <script charset="utf-8" src="./assets/index-Bv5aVo5X.js"></script>
4
-
5
3
  <body>
6
4
  <div id="container"></div>
7
5
  </body>
@@ -56,6 +56,16 @@ export type MarkdownImageProps = {
56
56
  */
57
57
  title?: string | null | undefined;
58
58
  };
59
+ /**
60
+ * - Adapted from `p-limit` package.
61
+ */
62
+ export type Limiter = <Arguments extends unknown[], ReturnType>(function_: (...arguments_: Arguments) => Promise<ReturnType>, ...arguments_: Arguments) => Promise<ReturnType>;
63
+ /**
64
+ * @typedef {<Arguments extends unknown[], ReturnType>(
65
+ * function_: (...arguments_: Arguments) => Promise<ReturnType>,
66
+ * ...arguments_: Arguments
67
+ * ) => Promise<ReturnType>} Limiter - Adapted from `p-limit` package.
68
+ */
59
69
  /**
60
70
  * Renders a mermaid diagram or mermaid markdown file.
61
71
  *
@@ -70,13 +80,18 @@ export type MarkdownImageProps = {
70
80
  * @param {"svg" | "png" | "pdf"} [opts.outputFormat] - Mermaid output format.
71
81
  * @param {string} [opts.artefacts] - Path to the artefacts directory.
72
82
  * Defaults to `output` extension. Overrides `output` extension if set.
83
+ * @param {import("puppeteer").Browser} [opts.browser] - If set, reuses the given puppeteer browser instance instead of creating a new one.
84
+ * This may leak cookies/cache between runs.
85
+ * @param {Limiter} [opts.limiter] - If set, limiter function to avoid rendering too many diagrams in parallel.
73
86
  * @param {ParseMDDOptions} [opts.parseMMDOptions] - Options to pass to {@link parseMMDOptions}.
74
87
  */
75
- export function run(input: `${string}.${"md" | "markdown"}` | string | undefined, output: `${string}.${"md" | "markdown" | "svg" | "png" | "pdf"}` | "/dev/stdout", { puppeteerConfig, quiet, outputFormat, parseMMDOptions, artefacts }?: {
88
+ export function run(input: `${string}.${"md" | "markdown"}` | string | undefined, output: `${string}.${"md" | "markdown" | "svg" | "png" | "pdf"}` | "/dev/stdout", { browser: userPassedBrowser, puppeteerConfig, quiet, outputFormat, parseMMDOptions, limiter, artefacts }?: {
76
89
  puppeteerConfig?: puppeteer.LaunchOptions | undefined;
77
90
  quiet?: boolean | undefined;
78
91
  outputFormat?: "svg" | "png" | "pdf" | undefined;
79
92
  artefacts?: string | undefined;
93
+ browser?: puppeteer.Browser | undefined;
94
+ limiter?: Limiter | undefined;
80
95
  parseMMDOptions?: ParseMDDOptions | undefined;
81
96
  }): Promise<void>;
82
97
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SAsZc,MAAM;;;;SACN,MAAM;;;;;;AAqBpB;;;;;;;;;;;;;;;GAeG;AACH,2BAbW,GAAG,MAAM,IAAI,IAAI,GAAG,UAAU,EAAE,GAAG,MAAM,GAAG,SAAS,UAIrD,GAAG,MAAM,IAAI,IAAI,GAAG,UAAU,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,EAAE,GAAG,aAAa,yEAEhF;IAAiD,eAAe;IACzC,KAAK;IACS,YAAY;IAC3B,SAAS;IAEA,eAAe;CAChD,iBAoHA;AA7UD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,uCAPW,OAAO,WAAW,EAAE,OAAO,GAAG,OAAO,WAAW,EAAE,cAAc,cAChE,MAAM,gBACN,KAAK,GAAG,KAAK,GAAG,KAAK,yGACrB,eAAe,GACb,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAC,CAAC,CA6JlF;AAzSD,qCAwHC;AA5MD;;;;;GAKG;AACH,+BAHW,MAAM,GACJ,KAAK,CAKjB;sBAxBqB,WAAW"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SAodc,MAAM;;;;SACN,MAAM;;;;;;;;;sBAsBP,CAAC,SAAS,SAAS,OAAO,EAAE,EAAE,UAAU,EACjD,SAAS,EAAE,CAAC,GAAG,UAAU,EAAE,SAAS,KAAK,OAAO,CAAC,UAAU,CAAC,EAC/D,GAAQ,UAAU,EAAE,SAAS,KACtB,OAAO,CAAC,UAAU,CAAC;AAJ3B;;;;;GAKG;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,2BAhBW,GAAG,MAAM,IAAI,IAAI,GAAG,UAAU,EAAE,GAAG,MAAM,GAAG,SAAS,UAIrD,GAAG,MAAM,IAAI,IAAI,GAAG,UAAU,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,EAAE,GAAG,aAAa,8GAEhF;IAAiD,eAAe;IACzC,KAAK;IACS,YAAY;IAC3B,SAAS;IAEY,OAAO;IAE3B,OAAO;IACC,eAAe;CAChD,iBAyHA;AA3WD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,uCAPW,OAAO,WAAW,EAAE,OAAO,GAAG,OAAO,WAAW,EAAE,cAAc,cAChE,MAAM,gBACN,KAAK,GAAG,KAAK,GAAG,KAAK,yGACrB,eAAe,GACb,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAC,CAAC,CA4KlF;AA5TD,qCA4HC;AAhOD;;;;;GAKG;AACH,+BAHW,MAAM,GACJ,KAAK,CAKjB;sBAjDqB,WAAW"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Puppeteer doesn't allow importing ESM modules from `file://` URLs.
3
+ * We don't want to create a dummy http server to serve ESM modules
4
+ * (since that would cause issues with ports/firewalls), so this module
5
+ * instead intercepts dummy `https://mermaid-cli-intercept.invalid` requests.
6
+ */
7
+ export class Interceptor {
8
+ /**
9
+ * @param {URL | `file://${string}`} fileUrl - File URL
10
+ * @param {Object} [options] - Optional options.
11
+ * @param {number} [options.allowParentDirectoryLevel] - Number of parent directory levels to allow access to.
12
+ */
13
+ fileUrlToInterceptUrl(fileUrl: URL | `file://${string}`, { allowParentDirectoryLevel }?: {
14
+ allowParentDirectoryLevel?: number | undefined;
15
+ }): Promise<string>;
16
+ /**
17
+ *
18
+ * @param {URL | string} interceptUrl
19
+ * @throws {Error} If the URL is not a valid intercept URL
20
+ */
21
+ interceptUrlToFileUrl(interceptUrl: URL | string): Promise<URL>;
22
+ /**
23
+ * Intercepts requests to `https://mermaid-cli-intercept.invalid`
24
+ * and serves the corresponding file content.
25
+ *
26
+ * @return {puppeteer.Handler<puppeteer.HTTPRequest>}
27
+ */
28
+ get interceptRequestHandler(): puppeteer.Handler<puppeteer.HTTPRequest>;
29
+ #private;
30
+ }
31
+ import type puppeteer from 'puppeteer';
32
+ //# sourceMappingURL=puppeteerIntercept.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"puppeteerIntercept.d.ts","sourceRoot":"","sources":["../../src/puppeteerIntercept.js"],"names":[],"mappings":"AA+BA;;;;;GAKG;AACH;IAcE;;;;OAIG;IACH,+BAJW,GAAG,GAAG,UAAU,MAAM,EAAE,kCAEhC;QAAyB,yBAAyB;KACpD,mBAcA;IAED;;;;SAIK;IACL,oCAHa,GAAG,GAAG,MAAM,gBAcxB;IAyBD;;;;;SAKK;IACL,+BAFc,kBAAkB,qBAAqB,CAAC,CAIrD;;CACF;2BAxHyB,WAAW"}
@@ -1,2 +1,2 @@
1
- export const version: "11.12.0";
1
+ export const version: "11.15.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mermaid-js/mermaid-cli",
3
- "version": "11.12.0",
3
+ "version": "11.15.0",
4
4
  "description": "Command-line interface for mermaid",
5
5
  "license": "MIT",
6
6
  "repository": "git@github.com:mermaid-js/mermaid-cli.git",
@@ -14,43 +14,47 @@
14
14
  },
15
15
  "exports": {
16
16
  ".": {
17
- "import": {
18
- "types": "./dist-types/src/index.d.ts",
19
- "default": "./src/index.js"
20
- }
17
+ "types": "./dist-types/src/index.d.ts",
18
+ "default": "./src/index.js"
21
19
  }
22
20
  },
23
21
  "types": "./dist-types/src/index.d.ts",
24
22
  "scripts": {
25
23
  "prepare": "tsc && vite build",
26
24
  "prepack": "tsc && vite build",
27
- "test": "NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" npx jest",
25
+ "test": "cross-env-shell NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules\" npx jest",
28
26
  "test:cli": "bash run-tests.sh test-positive",
29
27
  "version": "node scripts/version.js",
30
28
  "lint": "standard",
31
29
  "lint-fix": "standard --fix"
32
30
  },
33
31
  "dependencies": {
32
+ "@fortawesome/fontawesome-free": "^6.0.0 || ^7.0.1",
33
+ "@mermaid-js/layout-elk": "^0.1.5 || ^0.2.0",
34
34
  "@mermaid-js/mermaid-zenuml": "^0.2.0",
35
35
  "chalk": "^5.0.1",
36
- "commander": "^14.0.0",
36
+ "commander": "^13.1.0",
37
37
  "import-meta-resolve": "^4.1.0",
38
- "mermaid": "^11.0.2"
38
+ "katex": "^0.16.25",
39
+ "mermaid": "^11.14.0",
40
+ "p-limit": "^6.2.0"
41
+ },
42
+ "optionalDependencies": {
43
+ "@mermaid-js/layout-tidy-tree": "^0.2.1"
39
44
  },
40
45
  "peerDependencies": {
41
- "puppeteer": "^23"
46
+ "puppeteer": "^23 || ^24"
42
47
  },
43
48
  "devDependencies": {
44
- "@fortawesome/fontawesome-free": "^6.5.2",
45
- "@mermaid-js/layout-elk": "^0.1.2",
46
49
  "@tsconfig/node18": "^18.2.4",
47
50
  "@types/node": "~18.19.31",
51
+ "cross-env": "^7.0.3",
48
52
  "jest": "^30.0.5",
49
- "puppeteer": "^23.1.1",
53
+ "puppeteer": "^24.0.0",
50
54
  "standard": "^17.0.0",
51
- "typescript": "^5.0.1-rc",
55
+ "typescript": "^6.0.3",
52
56
  "vite": "^6.0.2",
53
- "yarn-upgrade-all": "^0.7.0"
57
+ "yarn-upgrade-all": "^0.8.1"
54
58
  },
55
59
  "files": [
56
60
  "src/",
package/src/index.js CHANGED
@@ -2,21 +2,48 @@ import { Command, Option, InvalidArgumentError } from 'commander'
2
2
  import chalk from 'chalk'
3
3
  import fs from 'fs'
4
4
  import { resolve } from 'import-meta-resolve'
5
+ import os from 'node:os'
5
6
  import path from 'path'
7
+ import pLimit from 'p-limit'
6
8
  import puppeteer from 'puppeteer'
7
9
  import url from 'url'
10
+ import { promisify } from 'node:util'
8
11
  import { version } from './version.js'
12
+ import { Interceptor } from './puppeteerIntercept.js'
9
13
 
10
14
  // __dirname is not available in ESM modules by default
11
15
  const __dirname = url.fileURLToPath(new url.URL('.', import.meta.url))
12
16
 
13
17
  /**
14
- * Mermaid.js IFFE path.
15
- *
16
- * Importing this in a browser adds a global `mermaid` object.
18
+ * CSS paths to embed in the page.
19
+ */
20
+ const cssImports = /** @type {const} */ ({
21
+ '@fortawesome/fontawesome-free/css/brands.css': { level: 2 },
22
+ '@fortawesome/fontawesome-free/css/regular.css': { level: 2 },
23
+ '@fortawesome/fontawesome-free/css/solid.css': { level: 2 },
24
+ '@fortawesome/fontawesome-free/css/fontawesome.css': { level: 2 },
25
+ 'katex/dist/katex.css': { level: 2 }
26
+ })
27
+
28
+ /**
29
+ * ESM bundles. Our interceptor doesn't support loading ESM modules that load
30
+ * other modules using relative paths, so these have to no `dependencies`.
17
31
  */
18
- const mermaidIIFEPath = path.resolve(path.dirname(url.fileURLToPath(resolve('mermaid', import.meta.url))), 'mermaid.js')
19
- const zenumlIIFEPath = path.resolve(path.dirname(url.fileURLToPath(resolve('@mermaid-js/mermaid-zenuml', import.meta.url))), 'mermaid-zenuml.js')
32
+ const mermaidESMPath = path.resolve(path.dirname(url.fileURLToPath(resolve('mermaid', import.meta.url))), 'mermaid.esm.mjs')
33
+ const elkESMPath = path.resolve(path.dirname(url.fileURLToPath(resolve('@mermaid-js/layout-elk', import.meta.url))), 'mermaid-layout-elk.esm.mjs')
34
+ const zenumlESMPath = path.resolve(path.dirname(url.fileURLToPath(resolve('@mermaid-js/mermaid-zenuml', import.meta.url))), 'mermaid-zenuml.esm.mjs')
35
+
36
+ /** @type {string | undefined} Path to `@mermaid-js/layout-tidy-tree`, if it is installed */
37
+ let tidyTreeESMPath
38
+ try {
39
+ tidyTreeESMPath = path.resolve(path.dirname(url.fileURLToPath(resolve('@mermaid-js/layout-tidy-tree', import.meta.url))), 'mermaid-layout-tidy-tree.esm.mjs')
40
+ } catch (error) {
41
+ if (error instanceof Error && 'code' in error && error.code === 'ERR_MODULE_NOT_FOUND') {
42
+ // optional dependency, this is normal
43
+ } else {
44
+ throw error
45
+ }
46
+ }
20
47
 
21
48
  /**
22
49
  * Prints an error to stderr, then closes with exit code 1
@@ -102,6 +129,22 @@ function parseCommanderInt (value, _unused) {
102
129
  return parsedValue
103
130
  }
104
131
 
132
+ /**
133
+ * Commander parser that converts a string to a float.
134
+ *
135
+ * @param {string} value - The value from commander.
136
+ * @param {*} _unused - Unused.
137
+ * @returns {number} The value parsed as a number.
138
+ * @see https://github.com/tj/commander.js/wiki/Class:-Option#argparserfn
139
+ */
140
+ function parseCommanderFloat (value, _unused) {
141
+ const parsedValue = parseFloat(value)
142
+ if (isNaN(parsedValue) || parsedValue <= 0) {
143
+ throw new InvalidArgumentError('Not a positive number.')
144
+ }
145
+ return parsedValue
146
+ }
147
+
105
148
  async function cli () {
106
149
  const commander = new Command()
107
150
  commander
@@ -112,12 +155,15 @@ async function cli () {
112
155
  .option('-i, --input <input>', 'Input mermaid file. Files ending in .md will be treated as Markdown and all charts (e.g. ```mermaid (...)``` or :::mermaid (...):::) will be extracted and generated. Use `-` to read from stdin.')
113
156
  .option('-o, --output [output]', 'Output file. It should be either md, svg, png, pdf or use `-` to output to stdout. Optional. Default: input + ".svg"')
114
157
  .option('-a, --artefacts [artefacts]', 'Output artefacts path. Only used with Markdown input file. Optional. Default: output directory')
158
+ .addOption(new Option('-j, --jobs <jobs>', 'Number of parallel jobs to run when rendering multiple diagrams. Defaults to half the available CPUs.').argParser(parseCommanderInt).default(
159
+ Math.floor(os.availableParallelism() / 2) || 1
160
+ ))
115
161
  .addOption(new Option('-e, --outputFormat [format]', 'Output format for the generated image.').choices(['svg', 'png', 'pdf']).default(null, 'Loaded from the output file extension'))
116
162
  .addOption(new Option('-b, --backgroundColor [backgroundColor]', 'Background color for pngs/svgs (not pdfs). Example: transparent, red, \'#F0F0F0\'.').default('white'))
117
163
  .option('-c, --configFile [configFile]', 'JSON configuration file for mermaid.')
118
164
  .option('-C, --cssFile [cssFile]', 'CSS file for the page.')
119
165
  .option('-I, --svgId [svgId]', 'The id attribute for the SVG element to be rendered.')
120
- .addOption(new Option('-s, --scale [scale]', 'Puppeteer scale factor').argParser(parseCommanderInt).default(1))
166
+ .addOption(new Option('-s, --scale [scale]', 'Puppeteer scale factor').argParser(parseCommanderFloat).default(1))
121
167
  .option('-f, --pdfFit', 'Scale PDF to fit chart')
122
168
  .option('-q, --quiet', 'Suppress log output')
123
169
  .option('-p --puppeteerConfigFile [puppeteerConfigFile]', 'JSON configuration file for puppeteer.')
@@ -127,7 +173,7 @@ async function cli () {
127
173
 
128
174
  const options = commander.opts()
129
175
 
130
- let { theme, width, height, input, output, outputFormat, backgroundColor, configFile, cssFile, svgId, puppeteerConfigFile, scale, pdfFit, quiet, iconPacks, iconPacksNamesAndUrls, artefacts } = options
176
+ let { theme, width, height, input, output, outputFormat, backgroundColor, configFile, cssFile, svgId, puppeteerConfigFile, scale, pdfFit, quiet, iconPacks, iconPacksNamesAndUrls, artefacts, jobs } = options
131
177
 
132
178
  // check input file
133
179
  if (!input) {
@@ -189,7 +235,7 @@ async function cli () {
189
235
  mermaidConfig = Object.assign(mermaidConfig, JSON.parse(fs.readFileSync(configFile, 'utf-8')))
190
236
  }
191
237
 
192
- let puppeteerConfig = /** @type {import('puppeteer').PuppeteerLaunchOptions} */ ({
238
+ let puppeteerConfig = /** @type {import('puppeteer').LaunchOptions} */ ({
193
239
  /*
194
240
  * `headless: 'shell'` is not officially supported in Puppeteer v19, v20, v21,
195
241
  * but still works. In Puppeteer v22, it uses the `chrome-headless-shell` package,
@@ -216,6 +262,7 @@ async function cli () {
216
262
  puppeteerConfig,
217
263
  quiet,
218
264
  outputFormat,
265
+ limiter: pLimit(jobs),
219
266
  parseMMDOptions: {
220
267
  mermaidConfig, backgroundColor, myCSS, pdfFit, viewport: { width, height, deviceScaleFactor: scale }, svgId, iconPacks, iconPacksNamesAndUrls
221
268
  },
@@ -259,25 +306,41 @@ async function renderMermaid (browser, definition, outputFormat, { viewport, bac
259
306
  await page.$eval('body', (body, backgroundColor) => {
260
307
  body.style.background = backgroundColor
261
308
  }, backgroundColor)
262
- await Promise.all([
263
- page.addScriptTag({ path: mermaidIIFEPath }),
264
- page.addScriptTag({ path: zenumlIIFEPath })
265
- ])
266
- const metadata = await page.$eval('#container', async (container, definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks, iconPacksNamesAndUrls) => {
267
- await Promise.all(Array.from(document.fonts, (font) => font.load()))
268
309
 
269
- /**
270
- * @typedef {Object} GlobalThisWithMermaid
271
- * We've already imported these modules in our `index.html` file (or by running `page.addScriptTag`),
272
- * so that they get correctly bundled.
273
- * @property {import("mermaid")["default"]} mermaid Already imported mermaid instance
274
- * @property {import("@mermaid-js/mermaid-zenuml")["default"]} mermaid-zenuml Already imported mermaid-zenuml instance
275
- * @property {import("@mermaid-js/layout-elk")["default"]} elkLayouts Already imported mermaid-elkLayouts instance
276
- */
277
- const { mermaid, 'mermaid-zenuml': zenuml, elkLayouts } = /** @type {GlobalThisWithMermaid & typeof globalThis} */ (globalThis)
310
+ const interceptor = new Interceptor()
311
+ const mermaidUrl = await interceptor.fileUrlToInterceptUrl(url.pathToFileURL(mermaidESMPath))
312
+ const elkUrl = await interceptor.fileUrlToInterceptUrl(url.pathToFileURL(elkESMPath))
313
+ const zenumlUrl = await interceptor.fileUrlToInterceptUrl(url.pathToFileURL(zenumlESMPath))
314
+ const tidyTreeESMUrl = tidyTreeESMPath ? await interceptor.fileUrlToInterceptUrl(url.pathToFileURL(tidyTreeESMPath)) : undefined
315
+
316
+ page.on('request', interceptor.interceptRequestHandler)
317
+ await page.setRequestInterception(true)
318
+
319
+ await Promise.all(Object.entries(cssImports).map(async ([cssImport, { level }]) => {
320
+ const interceptUrl = await interceptor.fileUrlToInterceptUrl(new URL(resolve(cssImport, import.meta.url)), {
321
+ allowParentDirectoryLevel: level
322
+ })
323
+ await page.addStyleTag({
324
+ url: interceptUrl
325
+ })
326
+ }))
327
+
328
+ const metadata = await page.$eval('#container', async (container, { definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks, iconPacksNamesAndUrls, elkUrl, mermaidUrl, zenumlUrl, tidyTreeESMUrl }) => {
329
+ const { default: mermaid } = await import(mermaidUrl)
330
+ /** @type {typeof import('@mermaid-js/layout-elk')} */
331
+ const { default: elkLayouts } = await import(elkUrl)
332
+ /** @type {typeof import('@mermaid-js/mermaid-zenuml')} */
333
+ const { default: zenuml } = await import(zenumlUrl)
334
+ // @ts-ignore -- @mermaid-js/layout-tidy-tree is an optionalDependency and might not be installed
335
+ /** @type {typeof import('@mermaid-js/layout-tidy-tree') | {default: undefined}} */
336
+ const { default: tidyTree } = tidyTreeESMUrl ? await import(tidyTreeESMUrl) : { default: undefined }
337
+ await Promise.all(Array.from(document.fonts, (font) => font.load()))
278
338
 
279
339
  await mermaid.registerExternalDiagrams([zenuml])
280
- mermaid.registerLayoutLoaders(elkLayouts)
340
+ mermaid.registerLayoutLoaders([
341
+ ...elkLayouts,
342
+ ...(tidyTree ?? [])
343
+ ])
281
344
  // lazy load icon packs
282
345
 
283
346
  mermaid.registerIconPacks(
@@ -291,22 +354,21 @@ async function renderMermaid (browser, definition, outputFormat, { viewport, bac
291
354
  )
292
355
 
293
356
  mermaid.registerIconPacks(
294
- iconPacksNamesAndUrls.map((iconPackInfo) =>
295
- {
296
- var packName = iconPackInfo.split('#')[0];
297
- var packUrl = iconPackInfo.split('#')[1];
298
-
299
- return ({
300
- name: packName,
301
- loader: () =>
302
- fetch(packUrl)
303
- .then((res) => res.json())
304
- .catch(() => {
305
- error(`Failed to fetch icon: ${iconPackInfo}`);
306
- })
307
- }
308
- )
357
+ iconPacksNamesAndUrls.map((iconPackInfo) => {
358
+ const packName = iconPackInfo.split('#')[0]
359
+ const packUrl = iconPackInfo.split('#')[1]
360
+
361
+ return ({
362
+ name: packName,
363
+ loader: () =>
364
+ fetch(packUrl)
365
+ .then((res) => res.json())
366
+ .catch(() => {
367
+ error(`Failed to fetch icon: ${iconPackInfo}`)
368
+ })
309
369
  }
370
+ )
371
+ }
310
372
  )
311
373
  )
312
374
  mermaid.initialize({ startOnLoad: false, ...mermaidConfig })
@@ -347,7 +409,7 @@ async function renderMermaid (browser, definition, outputFormat, { viewport, bac
347
409
  return {
348
410
  title, desc
349
411
  }
350
- }, definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks, iconPacksNamesAndUrls)
412
+ }, { definition, mermaidConfig, myCSS, backgroundColor, svgId, iconPacks, iconPacksNamesAndUrls, elkUrl, mermaidUrl, zenumlUrl, tidyTreeESMUrl })
351
413
 
352
414
  if (outputFormat === 'svg') {
353
415
  const svgXML = await page.$eval('svg', (svg) => {
@@ -426,6 +488,13 @@ function markdownImage ({ url, title, alt }) {
426
488
  }
427
489
  }
428
490
 
491
+ /**
492
+ * @typedef {<Arguments extends unknown[], ReturnType>(
493
+ * function_: (...arguments_: Arguments) => Promise<ReturnType>,
494
+ * ...arguments_: Arguments
495
+ * ) => Promise<ReturnType>} Limiter - Adapted from `p-limit` package.
496
+ */
497
+
429
498
  /**
430
499
  * Renders a mermaid diagram or mermaid markdown file.
431
500
  *
@@ -440,9 +509,12 @@ function markdownImage ({ url, title, alt }) {
440
509
  * @param {"svg" | "png" | "pdf"} [opts.outputFormat] - Mermaid output format.
441
510
  * @param {string} [opts.artefacts] - Path to the artefacts directory.
442
511
  * Defaults to `output` extension. Overrides `output` extension if set.
512
+ * @param {import("puppeteer").Browser} [opts.browser] - If set, reuses the given puppeteer browser instance instead of creating a new one.
513
+ * This may leak cookies/cache between runs.
514
+ * @param {Limiter} [opts.limiter] - If set, limiter function to avoid rendering too many diagrams in parallel.
443
515
  * @param {ParseMDDOptions} [opts.parseMMDOptions] - Options to pass to {@link parseMMDOptions}.
444
516
  */
445
- async function run (input, output, { puppeteerConfig = {}, quiet = false, outputFormat, parseMMDOptions, artefacts } = {}) {
517
+ async function run (input, output, { browser: userPassedBrowser, puppeteerConfig = {}, quiet = false, outputFormat, parseMMDOptions, limiter = (x, ...args) => x(...args), artefacts } = {}) {
446
518
  /**
447
519
  * Logs the given message to stdout, unless `quiet` is set to `true`.
448
520
  *
@@ -461,7 +533,7 @@ async function run (input, output, { puppeteerConfig = {}, quiet = false, output
461
533
  * @type {puppeteer.Browser | undefined}
462
534
  * Lazy-loaded browser instance, only created when needed.
463
535
  */
464
- let browser
536
+ let browser = userPassedBrowser
465
537
  try {
466
538
  if (!outputFormat) {
467
539
  const outputFormatFromFilename =
@@ -510,7 +582,7 @@ async function run (input, output, { puppeteerConfig = {}, quiet = false, output
510
582
 
511
583
  const outputFileRelative = `./${path.relative(path.dirname(path.resolve(output)), path.resolve(outputFile))}`
512
584
 
513
- const imagePromise = (async () => {
585
+ const imagePromise = limiter(async (browser, outputFormat) => {
514
586
  const { title, desc, data } = await renderMermaid(browser, mermaidDefinition, outputFormat, parseMMDOptions)
515
587
  await fs.promises.writeFile(outputFile, data)
516
588
  info(` ✅ ${outputFileRelative}`)
@@ -520,7 +592,7 @@ async function run (input, output, { puppeteerConfig = {}, quiet = false, output
520
592
  title,
521
593
  alt: desc
522
594
  }
523
- })()
595
+ }, browser, outputFormat)
524
596
  imagePromises.push(imagePromise)
525
597
  }
526
598
 
@@ -548,14 +620,19 @@ async function run (input, output, { puppeteerConfig = {}, quiet = false, output
548
620
  }
549
621
  } else {
550
622
  info('Generating single mermaid chart')
551
- browser = await puppeteer.launch(puppeteerConfig)
623
+ browser ??= await puppeteer.launch(puppeteerConfig)
552
624
  const { data } = await renderMermaid(browser, definition, outputFormat, parseMMDOptions)
553
- await output !== '/dev/stdout'
554
- ? fs.promises.writeFile(output, data)
555
- : process.stdout.write(data)
625
+ if (output === '/dev/stdout') {
626
+ await promisify(process.stdout.write).call(process.stdout, data)
627
+ } else {
628
+ await fs.promises.writeFile(output, data)
629
+ }
556
630
  }
557
631
  } finally {
558
- await browser?.close?.()
632
+ // Don't close the browser if it was passed in by the user
633
+ if (browser !== userPassedBrowser) {
634
+ await browser?.close?.()
635
+ }
559
636
  }
560
637
  }
561
638
 
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @import puppeteer from 'puppeteer';
3
+ */
4
+
5
+ import { readFile, realpath } from 'node:fs/promises'
6
+ import path from 'node:path'
7
+ import url from 'node:url'
8
+
9
+ /**
10
+ * Guesses the MIME-type of a file based on its extension.
11
+ *
12
+ * I've hardcoded the bare minimum number of MIME-types to support for security reasons.
13
+ *
14
+ * @param {string} filePath - The file path to guess the MIME-type for.
15
+ */
16
+ function getContentTypeFromFileExtension (filePath) {
17
+ const ext = path.extname(filePath).toLowerCase()
18
+ switch (ext) {
19
+ case '.css':
20
+ // Make sure to set UTF-8, since sometimes Puppeteer can parse it as Latin-1.
21
+ return 'text/css;charset=UTF-8'
22
+ case '.js':
23
+ case '.mjs':
24
+ return 'application/javascript'
25
+ case '.woff2':
26
+ return 'font/woff2'
27
+ default:
28
+ throw new Error(`Unsupported file extension for intercept: ${ext}`)
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Puppeteer doesn't allow importing ESM modules from `file://` URLs.
34
+ * We don't want to create a dummy http server to serve ESM modules
35
+ * (since that would cause issues with ports/firewalls), so this module
36
+ * instead intercepts dummy `https://mermaid-cli-intercept.invalid` requests.
37
+ */
38
+ export class Interceptor {
39
+ #INTERCEPT_ORIGIN = 'https://mermaid-cli-intercept.invalid'
40
+
41
+ /**
42
+ * Set of allowed file directories that can be intercepted.
43
+ *
44
+ * This is used to prevent arbitrary file access through the intercept mechanism.
45
+ *
46
+ * Make sure to use `realpath` to resolve any symlinks.
47
+ *
48
+ * @type {Set<string>}
49
+ */
50
+ #allowedDirs = new Set()
51
+
52
+ /**
53
+ * @param {URL | `file://${string}`} fileUrl - File URL
54
+ * @param {Object} [options] - Optional options.
55
+ * @param {number} [options.allowParentDirectoryLevel] - Number of parent directory levels to allow access to.
56
+ */
57
+ async fileUrlToInterceptUrl (fileUrl, {
58
+ allowParentDirectoryLevel = 1
59
+ } = {}) {
60
+ fileUrl = new URL(fileUrl)
61
+ if (fileUrl.protocol !== 'file:') {
62
+ throw new Error(`Invalid file URL: ${fileUrl}`)
63
+ }
64
+ let parentDirectory = await realpath(url.fileURLToPath(fileUrl))
65
+ while (allowParentDirectoryLevel-- >= 0) {
66
+ parentDirectory = path.dirname(parentDirectory)
67
+ }
68
+ this.#allowedDirs.add(parentDirectory)
69
+ return `${this.#INTERCEPT_ORIGIN}${fileUrl.pathname}`
70
+ }
71
+
72
+ /**
73
+ *
74
+ * @param {URL | string} interceptUrl
75
+ * @throws {Error} If the URL is not a valid intercept URL
76
+ */
77
+ async interceptUrlToFileUrl (interceptUrl) {
78
+ interceptUrl = new URL(interceptUrl)
79
+ if (interceptUrl.origin !== this.#INTERCEPT_ORIGIN) {
80
+ throw new Error(`Invalid intercept URL: ${interceptUrl}`)
81
+ }
82
+ const fileUrl = new URL(interceptUrl.href.slice(this.#INTERCEPT_ORIGIN.length), 'file://')
83
+ const filePath = await realpath(url.fileURLToPath(fileUrl))
84
+ if (![...this.#allowedDirs].some(dir => path.relative(filePath, dir).startsWith('..'))) {
85
+ throw new Error(`Intercept URL is not in an allowed directory: ${interceptUrl}`)
86
+ }
87
+ return fileUrl
88
+ }
89
+
90
+ /**
91
+ * @param {puppeteer.HTTPRequest} request - The intercepted request
92
+ */
93
+ async #interceptRequestHandler (request) {
94
+ try {
95
+ if (request.url().startsWith(this.#INTERCEPT_ORIGIN)) {
96
+ const fileUrl = await this.interceptUrlToFileUrl(request.url())
97
+ return request.respond({
98
+ status: 200,
99
+ headers: {
100
+ 'Access-Control-Allow-Origin': '*'
101
+ },
102
+ contentType: getContentTypeFromFileExtension(url.fileURLToPath(fileUrl)),
103
+ body: await readFile(fileUrl)
104
+ })
105
+ }
106
+ } catch (error) {
107
+ console.error(`Error handling intercept request for ${request.url()}:`, error)
108
+ request.abort()
109
+ }
110
+ request.continue()
111
+ }
112
+
113
+ /**
114
+ * Intercepts requests to `https://mermaid-cli-intercept.invalid`
115
+ * and serves the corresponding file content.
116
+ *
117
+ * @return {puppeteer.Handler<puppeteer.HTTPRequest>}
118
+ */
119
+ get interceptRequestHandler () {
120
+ return this.#interceptRequestHandler.bind(this)
121
+ }
122
+ }
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export const version = '11.12.0'
1
+ export const version = '11.15.0'