diff --git a/web/package.json b/web/package.json index 07f45876..195761ec 100644 --- a/web/package.json +++ b/web/package.json @@ -93,6 +93,7 @@ "cookie": "0.7.2", "dompurify": "3.4.0", "lodash-es": "4.18.1", + "mermaid": "11.15.0", "postcss": "8.5.10", "uuid": "14.0.0" } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 9de7caa3..a9f937b9 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -8,6 +8,7 @@ overrides: cookie: 0.7.2 dompurify: 3.4.0 lodash-es: 4.18.1 + mermaid: 11.15.0 postcss: 8.5.10 uuid: 14.0.0 @@ -930,10 +931,10 @@ packages: peerDependencies: svelte: '>=3 <5' - '@mermaid-js/parser@1.0.1': + '@mermaid-js/parser@1.1.1': resolution: { - integrity: sha512-opmV19kN1JsK0T6HhhokHpcVkqKpF+x2pPDKKM2ThHtZAB5F4PROopk0amuVYK5qMrIA4erzpNm8gmPNJgMDxQ==, + integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==, } '@napi-rs/wasm-runtime@0.2.12': @@ -3056,6 +3057,12 @@ packages: } engines: { node: '>= 0.4' } + es-toolkit@1.46.1: + resolution: + { + integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==, + } + es-module-lexer@2.0.0: resolution: { @@ -4344,10 +4351,10 @@ packages: integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==, } - mermaid@11.13.0: + mermaid@11.15.0: resolution: { - integrity: sha512-fEnci+Immw6lKMFI8sqzjlATTyjLkRa6axrEgLV2yHTfv8r+h1wjFbV6xeRtd4rUV1cS4EpR9rwp3Rci7TRWDw==, + integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==, } micromark-core-commonmark@2.0.3: @@ -6624,9 +6631,9 @@ snapshots: nanoid: 5.1.7 svelte: 5.54.0 - '@mermaid-js/parser@1.0.1': + '@mermaid-js/parser@1.1.1': dependencies: - langium: 4.2.1 + '@chevrotain/types': 11.1.2 '@napi-rs/wasm-runtime@0.2.12': dependencies: @@ -6818,7 +6825,7 @@ snapshots: dependencies: '@shikijs/langs': 3.23.0 '@shikijs/themes': 3.23.0 - mermaid: 11.13.0 + mermaid: 11.15.0 rehype-katex: 7.0.1 remark-cjk-friendly: 2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5) remark-cjk-friendly-gfm-strikethrough: 2.0.1(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5) @@ -7959,6 +7966,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.46.1: {} + esbuild@0.27.7: optionalDependencies: '@esbuild/aix-ppc64': 0.27.7 @@ -8869,11 +8878,11 @@ snapshots: mdn-data@2.27.1: {} - mermaid@11.13.0: + mermaid@11.15.0: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 1.0.1 + '@mermaid-js/parser': 1.1.1 '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 cytoscape: 3.33.1 @@ -8884,9 +8893,9 @@ snapshots: dagre-d3-es: 7.0.14 dayjs: 1.11.20 dompurify: 3.4.0 + es-toolkit: 1.46.1 katex: 0.16.44 khroma: 2.1.0 - lodash-es: 4.18.1 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.3.6 @@ -9700,7 +9709,7 @@ snapshots: esm-env: 1.2.2 katex: 0.16.44 marked: 16.4.2 - mermaid: 11.13.0 + mermaid: 11.15.0 rehype-harden: 1.1.8 rehype-raw: 7.0.0 rehype-sanitize: 6.0.0 diff --git a/web/scripts/check-security-overrides.mjs b/web/scripts/check-security-overrides.mjs index 216ec1e9..bacd38bd 100644 --- a/web/scripts/check-security-overrides.mjs +++ b/web/scripts/check-security-overrides.mjs @@ -14,6 +14,10 @@ const requiredOverrides = { requiredVersion: '8.5.10', staleVersions: ['8.5.8', '8.5.9'], }, + mermaid: { + requiredVersion: '11.15.0', + staleVersionPatterns: ['11\\.(?:[0-9]|1[0-4])(?:\\.\\d+)?(?:-[^\\s:]+)?'], + }, uuid: { requiredVersion: '14.0.0', staleVersions: ['11.1.0'], @@ -43,13 +47,24 @@ for (const [packageName, config] of Object.entries(requiredOverrides)) { problems.push(`pnpm-lock.yaml must record the ${packageName} override at ${requiredVersion}`) } - for (const staleVersion of staleVersions) { - const staleVersionPattern = new RegExp( - `${escapeRegExp(packageName)}(?::|@)\\s*${escapeRegExp(staleVersion)}\\b|${escapeRegExp(packageName)}@${escapeRegExp(staleVersion)}:`, + const staleVersionPatterns = + config.staleVersionPatterns ?? staleVersions.map((staleVersion) => escapeRegExp(staleVersion)) + for (const staleVersionPattern of staleVersionPatterns) { + if (!staleVersionPattern) { + continue + } + const staleEntryPattern = new RegExp( + `${escapeRegExp(packageName)}(?::|@)\\s*(?:${staleVersionPattern})\\b|${escapeRegExp(packageName)}@(?:${staleVersionPattern}):`, 'm', ) - if (staleVersionPattern.test(lockfile)) { - problems.push(`pnpm-lock.yaml still references stale ${packageName} ${staleVersion} entries`) + if (staleEntryPattern.test(lockfile)) { + const staleVersionLabel = + staleVersions.find((staleVersion) => escapeRegExp(staleVersion) === staleVersionPattern) ?? + `/${staleVersionPattern.replaceAll('\\\\', '\\')}/` + problems.push( + `pnpm-lock.yaml still references stale ${packageName} ${staleVersionLabel} entries`, + ) + break } } diff --git a/web/scripts/verify-mermaid-gantt-no-hang.mjs b/web/scripts/verify-mermaid-gantt-no-hang.mjs new file mode 100644 index 00000000..4fe85d5f --- /dev/null +++ b/web/scripts/verify-mermaid-gantt-no-hang.mjs @@ -0,0 +1,109 @@ +import { readdir } from 'node:fs/promises' +import path from 'node:path' +import { pathToFileURL } from 'node:url' +import { JSDOM } from 'jsdom' + +const timeoutMs = Number(process.env.MERMAID_RENDER_TIMEOUT_MS ?? '1500') +const mermaidPath = await resolveMermaidEntrypoint() +const { default: mermaid } = await import(pathToFileURL(mermaidPath).href) + +const dom = new JSDOM('
', { pretendToBeVisual: true }) +const { window } = dom + +installDomGlobals(window) + +mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'base' }) + +const hostileGanttChart = [ + 'gantt', + 'dateFormat YYYY-MM-DD', + 'excludes weekends monday,tuesday,wednesday,thursday,friday', + 'section Demo', + 'Task :2026-05-12, 1d', +].join('\n') + +const timeout = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(`mermaid.render timed out after ${timeoutMs}ms`)) + }, timeoutMs) +}) + +const outcome = await Promise.race([ + mermaid + .render('test-graph', hostileGanttChart) + .then((result) => { + if (!result?.svg || !result.svg.includes('