Skip to content

Commit 2ac6943

Browse files
aksOpsclaude
andcommitted
feat(docs): live-render chart pages via separate rcs-charts bundle
The chart docs pages were the last surface still falling back to a static placeholder. They now render live in the browser, same as the core component pages, completing the "React-only docs" goal of this PR (no hand-written HTML left). Approach — separate IIFE bundle: - New scripts/bundle-entry-charts.ts re-exports `src/charts/*` plus React + ReactDOM's createRoot. esbuild rolls it into rcs-charts.iife.js as `window.RCSCharts`. - build-site.mjs runs a second esbuild pass for this entry. Heavy WebGL peer deps (@deck.gl/core, @deck.gl/layers, d3-force) are marked --external; the chart components already have canvas2d / SVG fallback paths when the WebGL handoff fails to import. That keeps the chart bundle at ~794 KB instead of ~2.3 MB. - The chart bundle ships its own React. Because chart demos render standalone in their own createRoot — never nested inside the main bundle's React tree — the "two React instances" footgun does not apply, and they read theme via CSS custom properties (not Context). - esbuild also emits rcs-charts.iife.css (~1.6 KB) from the dynamic `await import("uplot/dist/uPlot.min.css")` in Chart.tsx. The chart-page <head> includes it. build-docs.mjs: - runner.js now exposes both `runExample` (uses window.RCS) and `runChartExample` (uses window.RCSCharts), refactored to share a `transformExample` helper. Each runner verifies its bundle is loaded and surfaces the failure inline. - renderComponentPage picks the right runner via a `runFn` variable and conditionally injects `<script src="../bundle/rcs-charts.iife.js">` + the chart CSS into chart pages only. - Static chart placeholder removed: chart pages now use the same `<div class="demo-render">` and a hidden code panel as core pages. - Dead .demo-render--static / .demo-static-note CSS rules dropped. Verified: pnpm typecheck clean, Sparkline unit tests still pass, pnpm build:site emits both bundles (rcs.iife.js 236 KB + rcs-charts.iife.js 794 KB) and 58 component pages. Spot-checked generated HTML: /docs/Chart/ pulls both bundles + the chart CSS and calls runChartExample; /docs/Button/ pulls only the main bundle and calls runExample. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b4740e3 commit 2ac6943

3 files changed

Lines changed: 103 additions & 49 deletions

File tree

scripts/build-docs.mjs

Lines changed: 54 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -576,24 +576,7 @@ function renderDocsCss() {
576576
color: var(--fg-1); line-height: 1.55; white-space: pre;
577577
}
578578
579-
/* Charts: static placeholder where live preview would be, plus peer-dep list. */
580-
.demo-render--static {
581-
display: flex; align-items: center; justify-content: center;
582-
min-height: 120px; padding: 24px;
583-
background: var(--bg-2);
584-
background-image:
585-
repeating-linear-gradient(
586-
45deg, transparent, transparent 8px,
587-
var(--bg-1) 8px, var(--bg-1) 9px
588-
);
589-
}
590-
.demo-static-note {
591-
font-family: var(--font-mono); font-size: 11px; line-height: 1.55;
592-
color: var(--fg-3); text-align: center; max-width: 48ch;
593-
background: var(--bg-1); border: 1px solid var(--border-1); border-radius: 4px;
594-
padding: 10px 14px;
595-
}
596-
.demo-static-note a { color: var(--accent); }
579+
/* Charts: peer-dep list (live preview now renders via window.RCSCharts). */
597580
ul.peer-deps {
598581
list-style: none; margin: 0; padding: 0;
599582
display: flex; flex-direction: column; gap: 6px;
@@ -647,35 +630,37 @@ html { scroll-behavior: smooth; }
647630
}
648631

649632
function renderRunner() {
650-
return `// Shared runtime for component live previews and the playground.
651-
// Loaded after window.RCS (from /docs/bundle/rcs.iife.js) and Babel-standalone.
633+
return `// Shared runtime for component live previews, the playground, and the
634+
// charts subpath. Loaded after the IIFE bundle(s) and Babel-standalone.
635+
//
636+
// runExample(code, targetId) — core components, uses window.RCS
637+
// runChartExample(code, targetId) — chart components, uses window.RCSCharts
652638
(function (global) {
653-
function listAllNames() {
654-
return Object.keys(global.RCS || {}).filter(function (n) { return n !== "default"; });
655-
}
656639
function escapeHtml(s) {
657640
return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
658641
}
659642
function showError(target, msg) {
660643
target.innerHTML = '<div class="err">' + escapeHtml(msg) + '</div>';
661644
}
645+
function transformExample(R, code) {
646+
var names = Object.keys(R || {}).filter(function (n) { return n !== "default"; });
647+
return (
648+
"(function() { var R = arguments[0]; " +
649+
"var { " + names.join(", ") + " } = R; " +
650+
"return (" + code + "); " +
651+
"})"
652+
);
653+
}
662654
function runExample(code, targetId) {
663655
var target = document.getElementById(targetId || "preview");
664656
if (!target) return;
665657
if (!global.RCS) return showError(target, "window.RCS not loaded");
666658
if (!global.Babel) return showError(target, "Babel standalone not loaded");
667-
var names = listAllNames();
668-
var wrapped =
669-
"(function() { var RCS = window.RCS; " +
670-
"var { " + names.join(", ") + " } = RCS; " +
671-
"return (" + code + "); " +
672-
"})()";
673-
var transformed;
674-
try {
675-
transformed = global.Babel.transform(wrapped, { presets: ["env", "react"] }).code;
676-
} catch (e) { return showError(target, "Compile error: " + (e && e.message || e)); }
677-
var result;
678-
try { result = (0, eval)(transformed); }
659+
var wrapped = transformExample(global.RCS, code);
660+
var transformed, factory, result;
661+
try { transformed = global.Babel.transform(wrapped, { presets: ["env", "react"] }).code; }
662+
catch (e) { return showError(target, "Compile error: " + (e && e.message || e)); }
663+
try { factory = (0, eval)(transformed); result = factory(global.RCS); }
679664
catch (e) { return showError(target, "Runtime error: " + (e && e.message || e)); }
680665
try {
681666
var R = global.RCS;
@@ -686,7 +671,28 @@ function renderRunner() {
686671
root.render(R.React.createElement(R.ThemeProvider, { mode: "dark" }, result));
687672
} catch (e) { showError(target, "Render error: " + (e && e.message || e)); }
688673
}
674+
function runChartExample(code, targetId) {
675+
var target = document.getElementById(targetId || "preview");
676+
if (!target) return;
677+
if (!global.RCSCharts) return showError(target, "window.RCSCharts not loaded");
678+
if (!global.Babel) return showError(target, "Babel standalone not loaded");
679+
var wrapped = transformExample(global.RCSCharts, code);
680+
var transformed, factory, result;
681+
try { transformed = global.Babel.transform(wrapped, { presets: ["env", "react"] }).code; }
682+
catch (e) { return showError(target, "Compile error: " + (e && e.message || e)); }
683+
try { factory = (0, eval)(transformed); result = factory(global.RCSCharts); }
684+
catch (e) { return showError(target, "Runtime error: " + (e && e.message || e)); }
685+
try {
686+
var R = global.RCSCharts;
687+
target.innerHTML = "";
688+
var mount = document.createElement("div");
689+
target.appendChild(mount);
690+
var root = R.createRoot(mount);
691+
root.render(result);
692+
} catch (e) { showError(target, "Render error: " + (e && e.message || e)); }
693+
}
689694
global.runExample = runExample;
695+
global.runChartExample = runChartExample;
690696
})(typeof window !== "undefined" ? window : globalThis);
691697
`;
692698
}
@@ -990,11 +996,7 @@ function renderComponentPage(name) {
990996

991997
const demosHtml = demos.map((d, i) => `
992998
<article class="demo" id="demo-${i}">
993-
${chart
994-
? `<div class="demo-render demo-render--static" id="demo-render-${i}">
995-
<div class="demo-static-note">Live render coming in the chart bundle pass. Copy the code below to try it locally.</div>
996-
</div>`
997-
: `<div class="demo-render" id="demo-render-${i}"></div>`}
999+
<div class="demo-render" id="demo-render-${i}"></div>
9981000
<div class="demo-meta">
9991001
<div class="demo-meta-text">
10001002
<h3>${escapeHtml(d.title)}</h3>
@@ -1005,10 +1007,10 @@ function renderComponentPage(name) {
10051007
${chart
10061008
? ""
10071009
: `<a class="demo-action" href="../playground/?code=${encodeForUrl(d.code)}" target="_blank" rel="noopener" title="Open in playground">↗ Playground</a>`}
1008-
<button type="button" class="demo-action demo-toggle" data-target="${i}" aria-expanded="${chart ? "true" : "false"}">‹/› ${chart ? "Hide" : "Show"} code</button>
1010+
<button type="button" class="demo-action demo-toggle" data-target="${i}" aria-expanded="false">‹/› Show code</button>
10091011
</div>
10101012
</div>
1011-
<div class="demo-code" id="demo-code-${i}"${chart ? "" : " hidden"}>
1013+
<div class="demo-code" id="demo-code-${i}" hidden>
10121014
<pre><code class="lang-tsx">${escapeHtml(d.code)}</code></pre>
10131015
</div>
10141016
</article>
@@ -1063,24 +1065,27 @@ function renderComponentPage(name) {
10631065

10641066
const head = `
10651067
<link rel="stylesheet" href="../../dist/styles.css">
1066-
<link rel="stylesheet" href="../docs.css">`;
1068+
<link rel="stylesheet" href="../docs.css">${chart ? `
1069+
<link rel="stylesheet" href="../bundle/rcs-charts.iife.css">` : ""}`;
10671070

10681071
// Build a script that runs each demo and wires up copy / toggle / sidebar drawer.
1069-
// Charts skip the live runner — they're not in the IIFE bundle.
1070-
const runCalls = chart
1071-
? ""
1072-
: demos.map((d, i) => `runExample(${JSON.stringify(d.code)}, "demo-render-${i}");`).join("\n ");
1072+
// The runner functions themselves verify their bundle is loaded (window.RCS
1073+
// for runExample, window.RCSCharts for runChartExample) and surface the
1074+
// failure inline, so we only need to guard against runner.js itself failing.
1075+
const runFn = chart ? "runChartExample" : "runExample";
1076+
const runCalls = demos.map((d, i) => `${runFn}(${JSON.stringify(d.code)}, "demo-render-${i}");`).join("\n ");
10731077
const codeJson = JSON.stringify(demos.map((d) => d.code));
10741078

10751079
const tail = `
1076-
<script src="../bundle/rcs.iife.js"></script>
1080+
<script src="../bundle/rcs.iife.js"></script>${chart ? `
1081+
<script src="../bundle/rcs-charts.iife.js"></script>` : ""}
10771082
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"></script>
10781083
<script src="../runner.js"></script>
10791084
<script>
10801085
(function () {
10811086
var DEMO_CODES = ${codeJson};
10821087
function runAll() {
1083-
if (typeof runExample !== 'function') {
1088+
if (typeof ${runFn} !== 'function') {
10841089
document.querySelectorAll('.demo-render').forEach(function (el) {
10851090
el.innerHTML = '<div class="err">runner.js failed to load</div>';
10861091
});

scripts/build-site.mjs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ const r1 = spawnSync(esbuildBin, [
4949
], { stdio: "inherit" });
5050
if (r1.status !== 0) process.exit(r1.status ?? 1);
5151

52+
// Charts bundle — loaded only by chart docs pages. Heavy WebGL peer deps
53+
// stay external; chart components fall back to canvas2d / SVG paths.
54+
const r1b = spawnSync(esbuildBin, [
55+
"scripts/bundle-entry-charts.ts",
56+
"--bundle",
57+
"--format=iife",
58+
"--global-name=RCSCharts",
59+
`--outfile=${join(out, "docs/bundle/rcs-charts.iife.js")}`,
60+
"--target=es2020",
61+
"--jsx=automatic",
62+
"--minify",
63+
'--define:process.env.NODE_ENV="production"',
64+
"--external:@deck.gl/core",
65+
"--external:@deck.gl/layers",
66+
"--external:d3-force",
67+
], { stdio: "inherit" });
68+
if (r1b.status !== 0) process.exit(r1b.status ?? 1);
69+
5270
const r2 = spawnSync(process.execPath, ["scripts/build-docs.mjs", out], { stdio: "inherit" });
5371
if (r2.status !== 0) process.exit(r2.status ?? 1);
5472

scripts/bundle-entry-charts.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Bundle entry for the charts subpath live previews.
3+
*
4+
* Mirrors scripts/bundle-entry.ts but bundles only the chart components
5+
* plus the chart-only peer deps (uplot, d3-hierarchy, cytoscape,
6+
* cytoscape-cose-bilkent). React and ReactDOM's createRoot are bundled
7+
* inline so chart demos can render without sharing the main bundle's
8+
* React instance — this avoids the "two React instances in one tree"
9+
* footgun, since chart demos never nest inside core components.
10+
*
11+
* Heavy GPU peer deps (@deck.gl/core, @deck.gl/layers, d3-force) are
12+
* marked `--external` at bundle time. Chart components already have a
13+
* canvas2d / SVG fallback path when the WebGL handoff fails to import,
14+
* so the docs site renders charts via the canvas paths and skips
15+
* deck.gl entirely. That keeps the chart bundle ~700 KB instead of
16+
* pulling in another ~1.5 MB of WebGL machinery.
17+
*
18+
* Built by esbuild as IIFE → `window.RCSCharts = { Chart, Donut, …,
19+
* React, createRoot }`. Loaded only by chart docs pages, never by the
20+
* main docs site or playground.
21+
*
22+
* Used by:
23+
* - scripts/build-site.mjs (bundle pass)
24+
* - scripts/build-docs.mjs renderComponentPage (chart pages)
25+
*/
26+
27+
import * as React from "react";
28+
import { createRoot } from "react-dom/client";
29+
30+
export * from "../src/charts";
31+
export { React, createRoot };

0 commit comments

Comments
 (0)