executable-stories-react 0.15.0 → 0.17.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/ssr.d.cts CHANGED
@@ -91,6 +91,8 @@ interface RenderReportToHtmlOptions {
91
91
  share?: boolean;
92
92
  /** Command the share dialog hands over, e.g. `npx executable-stories share reports/`. */
93
93
  shareCommand?: string;
94
+ /** Draw the "Architecture, as it ran" span section (default false). `--html-architecture`. */
95
+ architecture?: boolean;
94
96
  }
95
97
  /**
96
98
  * Render a StoryReport (or a parse Result) to a complete, self-contained HTML
package/dist/ssr.d.ts CHANGED
@@ -91,6 +91,8 @@ interface RenderReportToHtmlOptions {
91
91
  share?: boolean;
92
92
  /** Command the share dialog hands over, e.g. `npx executable-stories share reports/`. */
93
93
  shareCommand?: string;
94
+ /** Draw the "Architecture, as it ran" span section (default false). `--html-architecture`. */
95
+ architecture?: boolean;
94
96
  }
95
97
  /**
96
98
  * Render a StoryReport (or a parse Result) to a complete, self-contained HTML
package/dist/ssr.js CHANGED
@@ -523,12 +523,29 @@ function DocVideo({ entry }) {
523
523
  }
524
524
 
525
525
  // src/components/doc/DocHtml.tsx
526
+ import { useEffect, useRef, useState } from "react";
526
527
  import { jsx as jsx14, jsxs as jsxs9 } from "react/jsx-runtime";
528
+ var MEASURE_SCRIPT = "<script>(function(){if(parent===window)return;var last=0,queued=false;var measure=function(){var kids=document.body.children,h=0;for(var i=0;i<kids.length;i++)h=Math.max(h,kids[i].getBoundingClientRect().bottom);return Math.ceil(h||document.body.getBoundingClientRect().bottom)};var post=function(){queued=false;var h=measure();if(h===last)return;last=h;parent.postMessage({__esHtmlHeight:h},'*')};var schedule=function(){if(queued)return;queued=true;requestAnimationFrame(post)};var start=function(){new ResizeObserver(schedule).observe(document.body);new MutationObserver(schedule).observe(document.body,{subtree:true,childList:true,attributes:true,characterData:true});addEventListener('load',schedule);post()};if(document.body)start();else addEventListener('DOMContentLoaded',start)})();</script>";
529
+ var MAX_MEASURED_HEIGHT = 5e3;
527
530
  function DocHtml({ entry }) {
528
- const height = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
531
+ const declaredHeight = typeof entry.height === "number" ? `${entry.height}px` : entry.height ?? "400px";
529
532
  const title = entry.title ?? "Embedded HTML";
530
533
  const safeHref = safeUrl(entry.url ?? entry.path);
531
- const frameProps = entry.content !== void 0 ? { srcDoc: entry.content } : { src: safeHref ?? "" };
534
+ const frameRef = useRef(null);
535
+ const [measuredHeight, setMeasuredHeight] = useState();
536
+ useEffect(() => {
537
+ if (entry.content === void 0) return;
538
+ const onMessage = (event) => {
539
+ if (event.source !== frameRef.current?.contentWindow) return;
540
+ const reported = event.data?.__esHtmlHeight;
541
+ if (typeof reported !== "number" || !Number.isFinite(reported) || reported <= 0) return;
542
+ setMeasuredHeight(Math.min(reported, MAX_MEASURED_HEIGHT));
543
+ };
544
+ window.addEventListener("message", onMessage);
545
+ return () => window.removeEventListener("message", onMessage);
546
+ }, [entry.content]);
547
+ const height = measuredHeight === void 0 ? declaredHeight : `${measuredHeight}px`;
548
+ const frameProps = entry.content !== void 0 ? { srcDoc: entry.content + MEASURE_SCRIPT } : { src: safeHref ?? "" };
532
549
  return /* @__PURE__ */ jsxs9("figure", { className: "my-3 overflow-hidden rounded-md border border-border", children: [
533
550
  /* @__PURE__ */ jsxs9("figcaption", { className: "flex items-center justify-between border-b border-border bg-muted/40 px-3 py-1.5 text-xs font-medium text-muted-foreground", children: [
534
551
  /* @__PURE__ */ jsx14("span", { children: entry.title ?? "HTML" }),
@@ -548,6 +565,7 @@ function DocHtml({ entry }) {
548
565
  /* @__PURE__ */ jsx14(
549
566
  "iframe",
550
567
  {
568
+ ref: frameRef,
551
569
  ...frameProps,
552
570
  sandbox: "allow-scripts",
553
571
  loading: "lazy",
@@ -1096,7 +1114,7 @@ function ScenarioRunHistory({ scenarioId }) {
1096
1114
  }
1097
1115
 
1098
1116
  // src/components/ScenarioStaleness.tsx
1099
- import { useEffect, useState } from "react";
1117
+ import { useEffect as useEffect2, useState as useState2 } from "react";
1100
1118
 
1101
1119
  // src/lib/provenance.ts
1102
1120
  function formatRelativeAge(thenMs, nowMs) {
@@ -1155,8 +1173,8 @@ import { jsxs as jsxs16 } from "react/jsx-runtime";
1155
1173
  function ScenarioStaleness({ scenario }) {
1156
1174
  const freshness = useScenarioFreshness();
1157
1175
  const fixedNow = freshness?.nowMs;
1158
- const [now, setNow] = useState(fixedNow);
1159
- useEffect(() => {
1176
+ const [now, setNow] = useState2(fixedNow);
1177
+ useEffect2(() => {
1160
1178
  if (fixedNow === void 0) setNow(Date.now());
1161
1179
  }, [fixedNow]);
1162
1180
  if (!freshness || now === void 0) return null;
@@ -1170,7 +1188,7 @@ function ScenarioStaleness({ scenario }) {
1170
1188
  }
1171
1189
 
1172
1190
  // src/interactive/collapse-context.ts
1173
- import { createContext as createContext4, useCallback, useContext as useContext5, useState as useState2 } from "react";
1191
+ import { createContext as createContext4, useCallback, useContext as useContext5, useState as useState3 } from "react";
1174
1192
  var CollapseContext = createContext4(null);
1175
1193
  var CollapseProvider = CollapseContext.Provider;
1176
1194
  function useCollapse() {
@@ -1744,23 +1762,85 @@ function ReportErrorShell({
1744
1762
  return /* @__PURE__ */ jsx36("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: /* @__PURE__ */ jsx36(ReportSchemaError, { error }) });
1745
1763
  }
1746
1764
 
1747
- // src/components/Report.tsx
1765
+ // src/components/ReportSpanGraph.tsx
1766
+ import { deriveSpanGraph, spanGraphNodeMark, spanGraphToMermaid } from "executable-stories-core/span-graph";
1767
+ import { useMemo as useMemo2 } from "react";
1748
1768
  import { jsx as jsx37, jsxs as jsxs25 } from "react/jsx-runtime";
1769
+ var LANE_LABEL = {
1770
+ edge: "Edge",
1771
+ service: "Service",
1772
+ queue: "Queue",
1773
+ datastore: "Data"
1774
+ };
1775
+ var MARK_LABEL = {
1776
+ failing: "Failing",
1777
+ added: "New",
1778
+ changed: "Changed"
1779
+ };
1780
+ var MARK_CLASS = {
1781
+ failing: "text-fail",
1782
+ added: "text-pass",
1783
+ changed: "text-pend"
1784
+ };
1785
+ function MarkBadge({ node }) {
1786
+ const mark = spanGraphNodeMark(node);
1787
+ if (!mark) return null;
1788
+ return /* @__PURE__ */ jsx37("span", { className: cn("ml-2 text-xs font-medium", MARK_CLASS[mark]), children: MARK_LABEL[mark] });
1789
+ }
1790
+ function ReportSpanGraph() {
1791
+ const report = useReport();
1792
+ const { code, graph, titleById } = useMemo2(() => {
1793
+ const scenarios = report.features.flatMap((feature) => feature.scenarios);
1794
+ const derived = deriveSpanGraph(scenarios);
1795
+ return {
1796
+ graph: derived,
1797
+ code: spanGraphToMermaid(derived),
1798
+ titleById: new Map(scenarios.map((scenario) => [scenario.id, scenario.title]))
1799
+ };
1800
+ }, [report]);
1801
+ if (code === "") return null;
1802
+ return /* @__PURE__ */ jsxs25("section", { className: "es-span-graph mt-6", "aria-labelledby": "es-span-graph-heading", children: [
1803
+ /* @__PURE__ */ jsx37("h2", { id: "es-span-graph-heading", className: "text-base font-semibold text-foreground", children: "Architecture, as it ran" }),
1804
+ /* @__PURE__ */ jsx37("p", { className: "mt-1 text-sm text-muted-foreground", children: "Derived from this run\u2019s traces. Every component was named by a span while a scenario ran, so nothing here is inferred from the code. Instrumented, exercised paths only: a component no scenario reaches does not appear." }),
1805
+ /* @__PURE__ */ jsx37(DocMermaid, { entry: { kind: "mermaid", phase: "static", code } }),
1806
+ /* @__PURE__ */ jsxs25("table", { className: "mt-2 w-full border-collapse text-left text-sm", children: [
1807
+ /* @__PURE__ */ jsx37("caption", { className: "sr-only", children: "Each component and the scenarios that exercised it" }),
1808
+ /* @__PURE__ */ jsx37("thead", { children: /* @__PURE__ */ jsxs25("tr", { className: "text-xs text-muted-foreground", children: [
1809
+ /* @__PURE__ */ jsx37("th", { scope: "col", className: "py-1 pr-4 font-medium", children: "Component" }),
1810
+ /* @__PURE__ */ jsx37("th", { scope: "col", className: "py-1 pr-4 font-medium", children: "Lane" }),
1811
+ /* @__PURE__ */ jsx37("th", { scope: "col", className: "py-1 font-medium", children: "Covered by" })
1812
+ ] }) }),
1813
+ /* @__PURE__ */ jsx37("tbody", { children: graph.nodes.map((node) => /* @__PURE__ */ jsxs25("tr", { className: "border-t border-border align-top", children: [
1814
+ /* @__PURE__ */ jsxs25("th", { scope: "row", className: "py-1.5 pr-4 font-mono text-xs font-normal", children: [
1815
+ node.id,
1816
+ /* @__PURE__ */ jsx37(MarkBadge, { node })
1817
+ ] }),
1818
+ /* @__PURE__ */ jsx37("td", { className: "py-1.5 pr-4 text-xs text-muted-foreground", children: LANE_LABEL[node.kind] }),
1819
+ /* @__PURE__ */ jsx37("td", { className: "py-1.5", children: /* @__PURE__ */ jsx37("ul", { className: "flex flex-col gap-0.5", children: node.scenarioIds.map((id) => /* @__PURE__ */ jsx37("li", { children: /* @__PURE__ */ jsx37("a", { href: `#${id}`, className: "text-xs text-link underline underline-offset-2 hover:text-foreground", children: titleById.get(id) ?? id }) }, id)) }) })
1820
+ ] }, node.id)) })
1821
+ ] })
1822
+ ] });
1823
+ }
1824
+
1825
+ // src/components/Report.tsx
1826
+ import { jsx as jsx38, jsxs as jsxs26 } from "react/jsx-runtime";
1749
1827
  function Report(props) {
1750
- const { report, customRenderers, renderers, className, title, dataTheme } = props;
1828
+ const { report, customRenderers, renderers, className, title, dataTheme, architecture = false } = props;
1751
1829
  const result = unwrapReport(report);
1752
1830
  if (!result.ok) {
1753
- return /* @__PURE__ */ jsx37(ReportErrorShell, { error: result.error, className, title, dataTheme });
1831
+ return /* @__PURE__ */ jsx38(ReportErrorShell, { error: result.error, className, title, dataTheme });
1754
1832
  }
1755
1833
  const data = result.data;
1756
1834
  const hasContent = data.features.length > 0;
1757
- return /* @__PURE__ */ jsx37(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ jsxs25("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1758
- /* @__PURE__ */ jsx37("header", { className: "es-report-header", children: /* @__PURE__ */ jsx37(ReportTitleBlock, { title }) }),
1759
- hasContent ? /* @__PURE__ */ jsx37(ReportFeatureList, {}) : /* @__PURE__ */ jsx37(ReportEmpty, {})
1835
+ return /* @__PURE__ */ jsx38(ReportRoot, { report: data, customRenderers, renderers, children: /* @__PURE__ */ jsxs26("main", { className: cn("es-report", className), "aria-label": title ?? "Test report", "data-theme": dataTheme, children: [
1836
+ /* @__PURE__ */ jsx38("header", { className: "es-report-header", children: /* @__PURE__ */ jsx38(ReportTitleBlock, { title }) }),
1837
+ architecture ? /* @__PURE__ */ jsx38(ReportSpanGraph, {}) : null,
1838
+ hasContent ? /* @__PURE__ */ jsx38(ReportFeatureList, {}) : /* @__PURE__ */ jsx38(ReportEmpty, {})
1760
1839
  ] }) });
1761
1840
  }
1762
1841
 
1763
1842
  // src/ssr-entry.ts
1843
+ import { REPORT_FAVICON_LINK } from "executable-stories-core/utils/report-favicon";
1764
1844
  var ROOT_ID = "es-report-root";
1765
1845
  var DATA_ID = "es-report-data";
1766
1846
  var HISTORY_ID = "es-report-history";
@@ -1770,7 +1850,7 @@ function escapeJsonForScript(json) {
1770
1850
  var HLJS_URL = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/es/highlight.min.js";
1771
1851
  var HLJS_CSS_LIGHT = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css";
1772
1852
  var HLJS_CSS_DARK = "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css";
1773
- var MERMAID_URL = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs";
1853
+ var MERMAID_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
1774
1854
  function cdnAssets(syntaxHighlighting, mermaid) {
1775
1855
  const headParts = [];
1776
1856
  const imports = [];
@@ -1816,10 +1896,11 @@ function renderReportToHtml(report, options = {}) {
1816
1896
  staleAfterDays = 7,
1817
1897
  scenarioHistory,
1818
1898
  share = false,
1819
- shareCommand = ""
1899
+ shareCommand = "",
1900
+ architecture = false
1820
1901
  } = options;
1821
1902
  const markup = renderToStaticMarkup(
1822
- createElement(Report, { report, title })
1903
+ createElement(Report, { report, title, architecture })
1823
1904
  );
1824
1905
  const cdn = cdnAssets(syntaxHighlighting, mermaid);
1825
1906
  const parsed = unwrapReport(report);
@@ -1829,8 +1910,9 @@ function renderReportToHtml(report, options = {}) {
1829
1910
  const historyScript = interactive && scenarioHistory && Object.keys(scenarioHistory).length > 0 ? `<script type="application/json" id="${HISTORY_ID}">${escapeJsonForScript(JSON.stringify(scenarioHistory))}</script>` : "";
1830
1911
  const islandTag = interactive ? `<script>${islandScript}</script>` : "";
1831
1912
  const cdnBody = interactive ? "" : cdn.body;
1913
+ const architectureAttr = architecture ? ` data-es-architecture="true"` : "";
1832
1914
  const shareAttrs = share ? ` data-es-share="true"${shareCommand ? ` data-es-share-cmd="${escapeHtml(shareCommand)}"` : ""}` : "";
1833
- const islandConfigAttrs = interactive ? ` data-es-syntax="${syntaxHighlighting ? "true" : "false"}" data-es-mermaid="${mermaid ? "true" : "false"}" data-es-stale-days="${Number.isFinite(staleAfterDays) && staleAfterDays >= 0 ? staleAfterDays : 7}"${shareAttrs}` : "";
1915
+ const islandConfigAttrs = interactive ? ` data-es-syntax="${syntaxHighlighting ? "true" : "false"}" data-es-mermaid="${mermaid ? "true" : "false"}" data-es-stale-days="${Number.isFinite(staleAfterDays) && staleAfterDays >= 0 ? staleAfterDays : 7}"${shareAttrs}${architectureAttr}` : "";
1834
1916
  const rootAttrs = interactive ? ` id="${ROOT_ID}" data-title="${escapeHtml(title)}"${islandConfigAttrs}` : "";
1835
1917
  return `<!DOCTYPE html>
1836
1918
  <html lang="${escapeHtml(lang)}" data-theme="${theme === "dark" ? "dark" : "light"}">
@@ -1838,6 +1920,7 @@ function renderReportToHtml(report, options = {}) {
1838
1920
  <meta charset="UTF-8">
1839
1921
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1840
1922
  <meta name="color-scheme" content="light dark">
1923
+ ${REPORT_FAVICON_LINK}
1841
1924
  <title>${escapeHtml(title)}</title>
1842
1925
  ${cdn.head}
1843
1926
  ${headExtra}