Skip to content

Commit 5867312

Browse files
committed
fix: vendor mermaid
Mermaid diagrams render as raw text on docs.aws.amazon.com because the site's Content Security Policy blocks Zensical's default load of mermaid from unpkg.com. The same pages work on localhost only because localhost has no CSP. Switch to self-hosting the @mermaid-js/tiny UMD build, which ships as a single file with no lazy-loaded chunks and supports all diagram types currently in use (flowchart, sequence, state, class, entity-relationship). - Add scripts/vendor_mermaid.py, which reads version and expected sha256 from scripts/vendor_mermaid.toml, downloads the pinned tiny build from cdn.jsdelivr.net, and verifies the sha256. The script supports --check (CI-safe) and --latest modes. - Vendor @mermaid-js/tiny@11.14.0 into docs/assets/javascripts/mermaid.tiny.js. - Add docs/javascripts/mermaid-init.js, which initializes the vendored build with startOnLoad: false and securityLevel: strict. - Register both files under extra_javascript in zensical.toml. - Add .gitattributes marking docs/assets/javascripts/** as binary so PR diffs for vendored files stay readable. - Add a "Verify vendored Mermaid" step to the docs workflow so drift between the committed file and the pinned sha256 fails CI. - Document the vendoring convention and upgrade procedure in CONTRIBUTING.md under "Vendored dependencies". Affected pages: getting-started/development-environment, sdk-reference/operations/{invoke,wait-for-condition,callback}, testing/runner. Closes #152
1 parent 7d7b226 commit 5867312

8 files changed

Lines changed: 2839 additions & 1 deletion

File tree

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Vendored third-party JavaScript under docs/assets/javascripts/ is treated
2+
# as binary so PR diffs stay readable. See CONTRIBUTING.md for the convention.
3+
docs/assets/javascripts/** binary

.github/workflows/docs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ jobs:
2828
- name: Install dependencies
2929
run: pip install zensical
3030

31+
- name: Verify vendored Mermaid
32+
run: python3 scripts/vendor_mermaid.py --check
33+
3134
- name: Build documentation
3235
run: zensical build --clean
3336

CONTRIBUTING.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,53 @@ zensical serve
2222
zensical build --clean
2323
```
2424

25+
## Vendored dependencies
26+
27+
The file `docs/assets/javascripts/mermaid.tiny.js` is a pinned copy of the
28+
`@mermaid-js/tiny` UMD build. We self-host it because the Content Security
29+
Policy on `docs.aws.amazon.com` blocks Zensical's default CDN load of Mermaid
30+
from `unpkg.com`.
31+
32+
Mermaid's tiny build supports flowcharts and sequence, state, class, and
33+
entity-relationship diagrams. It does not support mindmap or architecture
34+
diagrams, or KaTeX math rendering.
35+
36+
Upgrade procedure:
37+
38+
```bash
39+
# 1. Show the pinned version and the latest on npm
40+
python3 scripts/vendor_mermaid.py --latest
41+
42+
# 2. Edit scripts/vendor_mermaid.toml. Bump `version`. Run the script.
43+
# It will fail and print the new SHA-256. Paste that value into `sha256`
44+
# in the TOML, then run the script again.
45+
python3 scripts/vendor_mermaid.py
46+
47+
# 3. Preview locally and spot-check pages with diagrams.
48+
zensical serve
49+
50+
# 4. Commit scripts/vendor_mermaid.toml and docs/assets/javascripts/mermaid.tiny.js
51+
# together in the same commit.
52+
```
53+
54+
CI verifies the committed file matches the pinned SHA-256. If you hand-edit the
55+
vendored file or forget to update the SHA on upgrade, the build fails.
56+
57+
### Directory convention
58+
59+
JavaScript under `docs/` is split by ownership:
60+
61+
- `docs/assets/javascripts/` holds vendored third-party bundles. Treat these
62+
as read-only outputs of the vendoring scripts under `scripts/`. Do not
63+
hand-edit them. Upgrade by bumping the version pin in the corresponding
64+
script and re-running it. Files in this directory are marked `binary` in
65+
`.gitattributes` so PR diffs do not try to render minified code.
66+
- `docs/javascripts/` holds first-party scripts we author and maintain, such
67+
as `mermaid-init.js`, which initializes the vendored Mermaid build.
68+
69+
Both directories are served from the same origin and register through
70+
`extra_javascript` in `zensical.toml`.
71+
2572
## Formatting
2673

2774
Run `mdformat` to auto-format Markdown files before committing:

docs/assets/javascripts/mermaid.tiny.js

Lines changed: 2572 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/javascripts/mermaid-init.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Initialize the self-hosted Mermaid tiny build.
2+
//
3+
// The companion file docs/assets/javascripts/mermaid.tiny.js is a UMD bundle
4+
// that sets window.mermaid when it loads. Zensical's theme integration picks
5+
// up window.mermaid automatically and uses it instead of loading Mermaid from
6+
// unpkg (which is blocked by the Content Security Policy on
7+
// docs.aws.amazon.com).
8+
//
9+
// See CONTRIBUTING.md ("Vendored dependencies") for the upgrade procedure.
10+
11+
if (window.mermaid && typeof window.mermaid.initialize === "function") {
12+
window.mermaid.initialize({
13+
startOnLoad: false,
14+
securityLevel: "strict",
15+
});
16+
}

scripts/vendor_mermaid.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env python3
2+
"""Vendor a pinned build of @mermaid-js/tiny into docs/assets/javascripts/.
3+
4+
Why this exists:
5+
docs.aws.amazon.com applies a Content Security Policy that disallows
6+
third-party script origins. Zensical's default Mermaid integration loads
7+
mermaid from unpkg.com, which the CSP blocks. We therefore self-host
8+
Mermaid from docs/assets/ (served same-origin).
9+
10+
We use the "tiny" build (@mermaid-js/tiny), which ships as a single UMD
11+
file with no lazy-loaded chunks. All currently-used diagram types
12+
(flowchart, sequence, state, class, ER) are supported. Mindmap,
13+
architecture, and KaTeX math are not.
14+
15+
Configuration:
16+
Version and expected SHA-256 are read from scripts/vendor_mermaid.toml.
17+
18+
Usage:
19+
python3 scripts/vendor_mermaid.py # download and verify
20+
python3 scripts/vendor_mermaid.py --check # verify only, do not download
21+
python3 scripts/vendor_mermaid.py --latest # print pinned + latest on npm
22+
23+
Upgrading:
24+
1. Bump `version` in scripts/vendor_mermaid.toml.
25+
2. Run the script. It will fail with the new SHA-256 printed. Paste that
26+
value into `sha256` in the TOML.
27+
3. Run the script again. It should succeed.
28+
4. Preview with `zensical serve`, then commit the TOML and the vendored
29+
file together.
30+
"""
31+
32+
from __future__ import annotations
33+
34+
import argparse
35+
import hashlib
36+
import json
37+
import sys
38+
import tomllib
39+
import urllib.request
40+
from pathlib import Path
41+
42+
REPO_ROOT = Path(__file__).resolve().parent.parent
43+
PIN_FILE = Path(__file__).resolve().parent / "vendor_mermaid.toml"
44+
DEST_FILE = REPO_ROOT / "docs" / "assets" / "javascripts" / "mermaid.tiny.js"
45+
SOURCE_URL_TEMPLATE = (
46+
"https://cdn.jsdelivr.net/npm/@mermaid-js/tiny@{version}/dist/mermaid.tiny.js"
47+
)
48+
NPM_REGISTRY_LATEST = "https://registry.npmjs.org/@mermaid-js/tiny/latest"
49+
50+
51+
def load_pin() -> tuple[str, str]:
52+
"""Read the pinned version and expected SHA-256 from the TOML file."""
53+
if not PIN_FILE.exists():
54+
print(f"ERROR: pin file missing: {PIN_FILE}", file=sys.stderr)
55+
sys.exit(1)
56+
57+
with PIN_FILE.open("rb") as handle:
58+
data = tomllib.load(handle)
59+
60+
missing = [key for key in ("version", "sha256") if key not in data]
61+
if missing:
62+
print(
63+
f"ERROR: {PIN_FILE.name} is missing required keys: "
64+
f"{', '.join(missing)}",
65+
file=sys.stderr,
66+
)
67+
sys.exit(1)
68+
return data["version"], data["sha256"]
69+
70+
71+
def sha256_of(path: Path) -> str:
72+
hasher = hashlib.sha256()
73+
with path.open("rb") as handle:
74+
for chunk in iter(lambda: handle.read(65536), b""):
75+
hasher.update(chunk)
76+
return hasher.hexdigest()
77+
78+
79+
def check_only(version: str, expected_sha256: str) -> int:
80+
if not DEST_FILE.exists():
81+
print(
82+
f"ERROR: {DEST_FILE.relative_to(REPO_ROOT)} is missing. "
83+
f"Run without --check to download.",
84+
file=sys.stderr,
85+
)
86+
return 1
87+
88+
actual = sha256_of(DEST_FILE)
89+
if actual != expected_sha256:
90+
print(
91+
f"ERROR: SHA-256 mismatch for {DEST_FILE.relative_to(REPO_ROOT)}",
92+
file=sys.stderr,
93+
)
94+
print(f" expected (from {PIN_FILE.name}): {expected_sha256}", file=sys.stderr)
95+
print(f" actual: {actual}", file=sys.stderr)
96+
return 1
97+
98+
print(
99+
f"OK: {DEST_FILE.relative_to(REPO_ROOT)} matches "
100+
f"@mermaid-js/tiny@{version} (sha256={expected_sha256})"
101+
)
102+
return 0
103+
104+
105+
def print_latest(version: str) -> int:
106+
with urllib.request.urlopen(NPM_REGISTRY_LATEST, timeout=10) as response:
107+
payload = json.load(response)
108+
print(f"Pinned: {version}")
109+
print(f"Latest: {payload['version']}")
110+
return 0
111+
112+
113+
def download_and_verify(version: str, expected_sha256: str) -> int:
114+
# Idempotent: skip download if the committed file already matches.
115+
if DEST_FILE.exists() and sha256_of(DEST_FILE) == expected_sha256:
116+
print(f"Already up to date: @mermaid-js/tiny@{version}")
117+
return 0
118+
119+
source_url = SOURCE_URL_TEMPLATE.format(version=version)
120+
print(f"Downloading @mermaid-js/tiny@{version} from {source_url}")
121+
122+
DEST_FILE.parent.mkdir(parents=True, exist_ok=True)
123+
tmp_file = DEST_FILE.with_suffix(DEST_FILE.suffix + ".tmp")
124+
125+
try:
126+
with urllib.request.urlopen(source_url, timeout=30) as response:
127+
tmp_file.write_bytes(response.read())
128+
129+
actual = sha256_of(tmp_file)
130+
if actual != expected_sha256:
131+
print("ERROR: SHA-256 mismatch after download", file=sys.stderr)
132+
print(
133+
f" expected (from {PIN_FILE.name}): {expected_sha256}",
134+
file=sys.stderr,
135+
)
136+
print(f" actual: {actual}", file=sys.stderr)
137+
print(
138+
f"\nIf you intentionally bumped `version` in {PIN_FILE.name}, "
139+
f"update `sha256` to the 'actual' value above and re-run.",
140+
file=sys.stderr,
141+
)
142+
tmp_file.unlink(missing_ok=True)
143+
return 1
144+
145+
tmp_file.replace(DEST_FILE)
146+
except Exception:
147+
tmp_file.unlink(missing_ok=True)
148+
raise
149+
150+
print(f"Vendored @mermaid-js/tiny@{version}")
151+
print(f" Path: {DEST_FILE.relative_to(REPO_ROOT)}")
152+
print(f" SHA-256: {expected_sha256}")
153+
return 0
154+
155+
156+
def main() -> int:
157+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
158+
group = parser.add_mutually_exclusive_group()
159+
group.add_argument(
160+
"--check",
161+
action="store_true",
162+
help="Verify the committed vendored file matches the pinned SHA-256 "
163+
"without downloading.",
164+
)
165+
group.add_argument(
166+
"--latest",
167+
action="store_true",
168+
help="Print the currently-pinned version and the latest version on npm, "
169+
"then exit.",
170+
)
171+
args = parser.parse_args()
172+
173+
version, expected_sha256 = load_pin()
174+
175+
if args.check:
176+
return check_only(version, expected_sha256)
177+
if args.latest:
178+
return print_latest(version)
179+
return download_and_verify(version, expected_sha256)
180+
181+
182+
if __name__ == "__main__":
183+
sys.exit(main())

scripts/vendor_mermaid.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Pin file for scripts/vendor_mermaid.py.
2+
#
3+
# Bump `version`, run `python3 scripts/vendor_mermaid.py`. The script will
4+
# fail with the new SHA-256 printed; paste that value into `sha256` below,
5+
# then re-run. See CONTRIBUTING.md ("Vendored dependencies") for details.
6+
7+
version = "11.14.0"
8+
sha256 = "aa1f0a2b8e3ce63bc187079fc3481f02758bf71498b3d6d2dddcd069b8233f61"

zensical.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,13 @@ extra_css = ["stylesheets/extra.css"]
133133
# The path provided should be relative to the "docs_dir".
134134
#
135135
# Read more: https://zensical.org/docs/customization/#additional-javascript
136-
#extra_javascript = ["javascripts/extra.js"]
136+
#
137+
# mermaid.tiny.js: self-hosted Mermaid diagram renderer.
138+
# See CONTRIBUTING.md for the upgrade procedure.
139+
extra_javascript = [
140+
"assets/javascripts/mermaid.tiny.js",
141+
"javascripts/mermaid-init.js",
142+
]
137143

138144
# ----------------------------------------------------------------------------
139145
# Section for configuring theme options

0 commit comments

Comments
 (0)