jamdesk 1.1.95 → 1.1.96
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.96",
|
|
4
4
|
"description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jamdesk",
|
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import dynamic from 'next/dynamic';
|
|
4
|
+
import { useMemo, useState } from 'react';
|
|
5
|
+
import { readCachedHeight } from './mermaidCache';
|
|
6
|
+
import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
|
|
4
7
|
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
>
|
|
12
|
-
<div className="animate-pulse bg-gray-200 dark:bg-gray-700 rounded-lg w-full max-w-xl h-48" />
|
|
13
|
-
</div>
|
|
14
|
-
);
|
|
15
|
-
}
|
|
8
|
+
// Neutral floor reserved in the SSR markup itself (present on the browser's
|
|
9
|
+
// first paint, before any JS) so a cache miss / pre-hydration window shows
|
|
10
|
+
// correctly-sized empty space instead of a collapse or a grey pulse. Roughly
|
|
11
|
+
// the prior skeleton footprint; refined to the exact diagram height on a
|
|
12
|
+
// cache hit by the layout effect below.
|
|
13
|
+
const DEFAULT_MERMAID_RESERVE_PX = 192;
|
|
16
14
|
|
|
17
|
-
// Dynamic import - ssr: false because mermaid requires browser APIs
|
|
18
15
|
const MermaidInner = dynamic(
|
|
19
16
|
() => import('./MermaidInner').then(mod => ({ default: mod.MermaidInner })),
|
|
20
|
-
{
|
|
21
|
-
ssr: false,
|
|
22
|
-
loading: () => <MermaidSkeleton />
|
|
23
|
-
}
|
|
17
|
+
{ ssr: false, loading: () => null }
|
|
24
18
|
);
|
|
25
19
|
|
|
26
20
|
interface MermaidProps {
|
|
@@ -31,5 +25,46 @@ interface MermaidProps {
|
|
|
31
25
|
}
|
|
32
26
|
|
|
33
27
|
export function Mermaid({ children, className, minWidth }: MermaidProps) {
|
|
34
|
-
|
|
28
|
+
// Server + first (hydration) render emit the constant default — identical
|
|
29
|
+
// markup, so no hydration mismatch. The sessionStorage read happens only
|
|
30
|
+
// in the post-hydration layout effect, never in this initializer.
|
|
31
|
+
const [reservedPx, setReservedPx] = useState(DEFAULT_MERMAID_RESERVE_PX);
|
|
32
|
+
|
|
33
|
+
// Fires once, after the hydration commit, synchronously before that
|
|
34
|
+
// commit's paint — and BEFORE the MermaidInner chunk has resolved or
|
|
35
|
+
// injected anything. On a cache hit, refine the reserved space to the
|
|
36
|
+
// exact diagram height so the SVG paints into already-exact space.
|
|
37
|
+
// Distinct from the reverted spike: one pre-paint settle before injection,
|
|
38
|
+
// never a re-render around an already-injected node. minWidth still flows
|
|
39
|
+
// to MermaidInner unchanged; the wrapper owns height only.
|
|
40
|
+
useIsomorphicLayoutEffect(() => {
|
|
41
|
+
const h = readCachedHeight(children);
|
|
42
|
+
if (h > 0) setReservedPx(h);
|
|
43
|
+
}, [children]);
|
|
44
|
+
|
|
45
|
+
// Stable element identity across reservedPx changes. The pre-paint
|
|
46
|
+
// setReservedPx above re-renders this wrapper; without this memo that
|
|
47
|
+
// re-render also re-renders the dynamic MermaidInner, making React
|
|
48
|
+
// re-commit its raw-HTML injection and discard the DOM that MermaidInner
|
|
49
|
+
// imperatively themed post-commit. That theming is reapplied only on a
|
|
50
|
+
// genuine content change, not on this re-commit, so on soft-navigation the
|
|
51
|
+
// diagram paints raw mermaid "neutral" colors instead of the active theme.
|
|
52
|
+
// Memoizing the element makes the wrapper own min-height ONLY.
|
|
53
|
+
// Soundness depends on MermaidInner being a stable reference: the dynamic()
|
|
54
|
+
// call MUST stay module-scoped (above) — moving it into this component
|
|
55
|
+
// would memoize an element closed over a stale component identity.
|
|
56
|
+
const inner = useMemo(
|
|
57
|
+
() => (
|
|
58
|
+
<MermaidInner className={className} minWidth={minWidth}>
|
|
59
|
+
{children}
|
|
60
|
+
</MermaidInner>
|
|
61
|
+
),
|
|
62
|
+
[children, className, minWidth]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div data-testid="mermaid-wrapper" style={{ minHeight: reservedPx }}>
|
|
67
|
+
{inner}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
35
70
|
}
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import mermaid from 'mermaid';
|
|
5
|
+
import {
|
|
6
|
+
CACHE_KEY_PREFIX,
|
|
7
|
+
hashDiagram,
|
|
8
|
+
readCache,
|
|
9
|
+
writeCache,
|
|
10
|
+
readSvgHeight,
|
|
11
|
+
type CachedDiagram,
|
|
12
|
+
} from './mermaidCache';
|
|
5
13
|
|
|
6
14
|
mermaid.initialize({
|
|
7
15
|
startOnLoad: false,
|
|
@@ -12,73 +20,15 @@ mermaid.initialize({
|
|
|
12
20
|
},
|
|
13
21
|
});
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface CachedDiagram {
|
|
27
|
-
svg: string;
|
|
28
|
-
height: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Coarse tamper check, NOT a sanitizer. mermaid's securityLevel:'strict' is
|
|
32
|
-
// the real defense (it sanitizes at render). This only guards the cache-read
|
|
33
|
-
// path, which re-injects stored bytes via React's raw inner-HTML prop without
|
|
34
|
-
// re-sanitizing — so a tampered same-origin sessionStorage entry is rejected
|
|
35
|
-
// here, forcing a fresh re-sanitized render. Catches the obvious signatures
|
|
36
|
-
// only (non-<svg> root, <script>, inline on*= handlers).
|
|
37
|
-
function looksTampered(svg: string): boolean {
|
|
38
|
-
return (
|
|
39
|
-
!/^\s*<svg/.test(svg) ||
|
|
40
|
-
/<script\b/i.test(svg) ||
|
|
41
|
-
/<[^>]+\son[a-z]+\s*=/i.test(svg)
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function readCache(source: string): CachedDiagram | null {
|
|
46
|
-
try {
|
|
47
|
-
const raw = sessionStorage.getItem(CACHE_KEY_PREFIX + hashDiagram(source));
|
|
48
|
-
if (!raw) return null;
|
|
49
|
-
const parsed = JSON.parse(raw) as Partial<CachedDiagram>;
|
|
50
|
-
if (typeof parsed?.svg !== 'string') return null;
|
|
51
|
-
if (looksTampered(parsed.svg)) return null;
|
|
52
|
-
// Normalize the shape so the return honestly matches CachedDiagram:
|
|
53
|
-
// legacy v1 entries predate height tracking, and a tampered/foreign
|
|
54
|
-
// entry could carry a non-numeric height. Coerce both to a number.
|
|
55
|
-
return {
|
|
56
|
-
svg: parsed.svg,
|
|
57
|
-
height: typeof parsed.height === 'number' ? parsed.height : 0,
|
|
58
|
-
};
|
|
59
|
-
} catch {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function writeCache(source: string, entry: CachedDiagram): void {
|
|
65
|
-
try {
|
|
66
|
-
sessionStorage.setItem(CACHE_KEY_PREFIX + hashDiagram(source), JSON.stringify(entry));
|
|
67
|
-
} catch {}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Real mermaid output has no `height` attribute (it sizes via viewBox +
|
|
71
|
-
// max-width), so fall back to the viewBox height; an explicit `height` still
|
|
72
|
-
// wins when present (other/older renderers). Markup-parsing instead of
|
|
73
|
-
// getBoundingClientRect keeps this jsdom-safe and reflow-free on the hot path.
|
|
74
|
-
export function readSvgHeight(svgMarkup: string): number {
|
|
75
|
-
const attr = svgMarkup.match(/<svg\b[^>]*\bheight=["']([\d.]+)(?:px)?["']/i);
|
|
76
|
-
if (attr) return parseFloat(attr[1]);
|
|
77
|
-
const viewBox = svgMarkup.match(
|
|
78
|
-
/<svg\b[^>]*\bviewBox=["']\s*[\d.+-]+\s+[\d.+-]+\s+[\d.+-]+\s+([\d.]+)/i
|
|
79
|
-
);
|
|
80
|
-
return viewBox ? parseFloat(viewBox[1]) : 0;
|
|
81
|
-
}
|
|
23
|
+
// readCachedHeight intentionally not re-exported; Mermaid.tsx imports it directly from mermaidCache
|
|
24
|
+
export {
|
|
25
|
+
CACHE_KEY_PREFIX,
|
|
26
|
+
hashDiagram,
|
|
27
|
+
readCache,
|
|
28
|
+
writeCache,
|
|
29
|
+
readSvgHeight,
|
|
30
|
+
};
|
|
31
|
+
export type { CachedDiagram };
|
|
82
32
|
|
|
83
33
|
interface MermaidInnerProps {
|
|
84
34
|
children: string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const CACHE_KEY_PREFIX = 'mermaid:v1:';
|
|
2
|
+
|
|
3
|
+
// djb2; collisions are theoretically possible but the input space is tiny for a docs site.
|
|
4
|
+
export function hashDiagram(source: string): string {
|
|
5
|
+
let h = 5381;
|
|
6
|
+
for (let i = 0; i < source.length; i++) {
|
|
7
|
+
h = ((h << 5) + h) ^ source.charCodeAt(i);
|
|
8
|
+
}
|
|
9
|
+
return (h >>> 0).toString(36);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CachedDiagram {
|
|
13
|
+
svg: string;
|
|
14
|
+
height: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Coarse tamper check, NOT a sanitizer. mermaid's securityLevel:'strict' is
|
|
18
|
+
// the real defense (it sanitizes at render). This only guards the cache-read
|
|
19
|
+
// path, which re-injects stored bytes via React's raw inner-HTML prop without
|
|
20
|
+
// re-sanitizing — so a tampered same-origin sessionStorage entry is rejected
|
|
21
|
+
// here, forcing a fresh re-sanitized render. Catches the obvious signatures
|
|
22
|
+
// only (non-<svg> root, <script>, inline on*= handlers).
|
|
23
|
+
function looksTampered(svg: string): boolean {
|
|
24
|
+
return (
|
|
25
|
+
!/^\s*<svg/.test(svg) ||
|
|
26
|
+
/<script\b/i.test(svg) ||
|
|
27
|
+
/<[^>]+\son[a-z]+\s*=/i.test(svg)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readCache(source: string): CachedDiagram | null {
|
|
32
|
+
try {
|
|
33
|
+
const raw = sessionStorage.getItem(CACHE_KEY_PREFIX + hashDiagram(source));
|
|
34
|
+
if (!raw) return null;
|
|
35
|
+
const parsed = JSON.parse(raw) as Partial<CachedDiagram>;
|
|
36
|
+
if (typeof parsed?.svg !== 'string') return null;
|
|
37
|
+
if (looksTampered(parsed.svg)) return null;
|
|
38
|
+
// Normalize the shape so the return honestly matches CachedDiagram:
|
|
39
|
+
// legacy v1 entries predate height tracking, and a tampered/foreign
|
|
40
|
+
// entry could carry a non-numeric height. Coerce both to a number.
|
|
41
|
+
return {
|
|
42
|
+
svg: parsed.svg,
|
|
43
|
+
height: typeof parsed.height === 'number' ? parsed.height : 0,
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function writeCache(source: string, entry: CachedDiagram): void {
|
|
51
|
+
try {
|
|
52
|
+
sessionStorage.setItem(CACHE_KEY_PREFIX + hashDiagram(source), JSON.stringify(entry));
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Real mermaid output has no `height` attribute (it sizes via viewBox +
|
|
57
|
+
// max-width), so fall back to the viewBox height; an explicit `height` still
|
|
58
|
+
// wins when present (other/older renderers). Markup-parsing instead of
|
|
59
|
+
// getBoundingClientRect keeps this jsdom-safe and reflow-free on the hot path.
|
|
60
|
+
export function readSvgHeight(svgMarkup: string): number {
|
|
61
|
+
const attr = svgMarkup.match(/<svg\b[^>]*\bheight=["']([\d.]+)(?:px)?["']/i);
|
|
62
|
+
if (attr) return parseFloat(attr[1]);
|
|
63
|
+
const viewBox = svgMarkup.match(
|
|
64
|
+
/<svg\b[^>]*\bviewBox=["']\s*[\d.+-]+\s+[\d.+-]+\s+[\d.+-]+\s+([\d.]+)/i
|
|
65
|
+
);
|
|
66
|
+
return viewBox ? parseFloat(viewBox[1]) : 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Cached pixel height for a diagram, or 0 when not cached. Lets the
|
|
70
|
+
// always-loaded Mermaid.tsx shell refine its reserved space to the exact
|
|
71
|
+
// diagram height before the mermaid chunk loads, without reaching into the
|
|
72
|
+
// cache entry shape itself.
|
|
73
|
+
export function readCachedHeight(source: string): number {
|
|
74
|
+
return readCache(source)?.height ?? 0;
|
|
75
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// useLayoutEffect on the client (runs after commit, synchronously before
|
|
4
|
+
// paint), useEffect on the server (no-op) — avoids React's SSR warning and
|
|
5
|
+
// keeps the rules-of-hooks lint happy (single named hook, no conditional
|
|
6
|
+
// hook-through-a-variable at the call site).
|
|
7
|
+
export const useIsomorphicLayoutEffect =
|
|
8
|
+
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|