@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.
- package/dist/index.html +0 -2
- package/dist-types/src/index.d.ts +16 -1
- package/dist-types/src/index.d.ts.map +1 -1
- package/dist-types/src/puppeteerIntercept.d.ts +32 -0
- package/dist-types/src/puppeteerIntercept.d.ts.map +1 -0
- package/dist-types/src/version.d.ts +1 -1
- package/package.json +18 -14
- package/src/index.js +126 -49
- package/src/puppeteerIntercept.js +122 -0
- package/src/version.js +1 -1
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/fa-brands-400-Dur5g48u.ttf +0 -0
- package/dist/assets/fa-brands-400-O7nZalfM.woff2 +0 -0
- package/dist/assets/fa-regular-400-Bf3rG5Nx.ttf +0 -0
- package/dist/assets/fa-regular-400-DgEfZSYE.woff2 +0 -0
- package/dist/assets/fa-solid-900-BV3CbEM2.ttf +0 -0
- package/dist/assets/fa-solid-900-DOQJEhcS.woff2 +0 -0
- package/dist/assets/index-Bv5aVo5X.js +0 -38
package/dist/index.html
CHANGED
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
18
|
-
|
|
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": "^
|
|
36
|
+
"commander": "^13.1.0",
|
|
37
37
|
"import-meta-resolve": "^4.1.0",
|
|
38
|
-
"
|
|
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": "^
|
|
53
|
+
"puppeteer": "^24.0.0",
|
|
50
54
|
"standard": "^17.0.0",
|
|
51
|
-
"typescript": "^
|
|
55
|
+
"typescript": "^6.0.3",
|
|
52
56
|
"vite": "^6.0.2",
|
|
53
|
-
"yarn-upgrade-all": "^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
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
19
|
-
const
|
|
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(
|
|
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').
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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(
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
623
|
+
browser ??= await puppeteer.launch(puppeteerConfig)
|
|
552
624
|
const { data } = await renderMermaid(browser, definition, outputFormat, parseMMDOptions)
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
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.
|
|
1
|
+
export const version = '11.15.0'
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|