@malloydata/malloyyo 0.2.35 → 0.2.37

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.
@@ -10,6 +10,7 @@
10
10
  //
11
11
  // Injected frame globals (read lazily — script order differs between hosts):
12
12
  // window.__DASHBOARD__ { name, query, title, description? } (the # artifact tag)
13
+ // window.__DASHBOARDS__ [{ name, title, description?, href }] (the siblings, nav order)
13
14
  // window.__GIVENS__ given specs introspected from the model's given: decls
14
15
  // window.__INITIAL_GIVENS__ URL-seeded given values ($-prefixed) for shareable links
15
16
  // window.__INITIAL_URLSTATE__ URL-seeded useUrlState view-state (~-prefixed)
@@ -33,6 +34,13 @@ import { combineTiles } from "./combine";
33
34
  export { filters };
34
35
 
35
36
  export const dashboardInfo = () => window.__DASHBOARD__ || {};
37
+
38
+ /** The dashboard's SIBLINGS: `[{ name, title, description?, href }]`, in nav
39
+ order, with this one excluded. Every host injects it (dev server, hosted
40
+ frame, static bundle), so a component that links to other dashboards — the
41
+ About page's card list, a "see also" — is written once and works on all
42
+ three. Empty when a host supplies nothing, so reading it is always safe. */
43
+ export const dashboardList = () => window.__DASHBOARDS__ || [];
36
44
  export const givenSpecs = () => window.__GIVENS__ || [];
37
45
 
38
46
  // ── host bridge ─────────────────────────────────────────────────────
@@ -936,6 +944,7 @@ function Root({ Dashboard, extraProps }) {
936
944
  <UrlStateCtx.Provider value={urlCtx}>
937
945
  <Dashboard
938
946
  dashboard={dashboardInfo()}
947
+ dashboards={dashboardList()}
939
948
  givenSpecs={givenSpecs()}
940
949
  givens={committed}
941
950
  setGiven={setGiven}
@@ -966,6 +975,20 @@ function Root({ Dashboard, extraProps }) {
966
975
  // stays legible; override --dash-panel-bg if your renderer output is dark-safe.
967
976
  const THEME_CSS = `
968
977
  :root {
978
+ /* The static site's variable names, aliased onto the theme.
979
+ A written page (the About page, most obviously) is authored against the
980
+ bundle's site.css, which names them --bg/--fg/--card/--line/--muted/--accent.
981
+ The same component also runs here — under the dev server and in the hosted
982
+ frame — where only --dash-* exists, so without these it renders with
983
+ transparent cards and invisible borders: technically fine, visibly broken.
984
+ Defined first and with plain names so a page may still override them. */
985
+ --bg: var(--dash-bg, #ffffff);
986
+ --fg: var(--dash-fg, #111418);
987
+ --card: var(--dash-panel-bg, #ffffff);
988
+ --line: var(--dash-border, #e5e7eb);
989
+ --muted: var(--dash-muted, #6b7280);
990
+ --accent: var(--dash-accent, #1573a1);
991
+
969
992
  --dash-font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
970
993
  --dash-bg: #ffffff;
971
994
  --dash-fg: #171717;
@@ -1029,8 +1052,61 @@ function injectTheme(bodyReset) {
1029
1052
  `rootEl` (defaults to #root, the sandboxed iframe's mount node). The tag-only
1030
1053
  in-page host passes its own container so the dashboard mounts directly in the
1031
1054
  trusted page — no iframe. */
1055
+ /**
1056
+ * Route a click on a sibling link through the host's navigate bridge.
1057
+ *
1058
+ * A written page (the About page) links to its siblings as ordinary anchors —
1059
+ * that is what makes the same component work as a plain static page in a
1060
+ * `dashboard bundle`. Inside a framed host it is NOT a plain page: the document
1061
+ * is a sandboxed iframe on its own origin, so a relative href navigates the
1062
+ * FRAME to a URL that does not exist there (the dev server answers "not found"
1063
+ * on the frame port), and a top-level href would need allow-top-navigation,
1064
+ * which the sandbox deliberately withholds.
1065
+ *
1066
+ * The bridge already solves this for drill: only the trusted parent knows the
1067
+ * environment's dashboard shape (hosted /datasets/:id/dashboard/:slug vs local
1068
+ * /?d=slug). So intercept the click and hand the parent a NAME, exactly as drill
1069
+ * does. The anchor keeps its href, so the static build is untouched and the link
1070
+ * still shows a real target on hover.
1071
+ *
1072
+ * Deliberately capture-phase and non-destructive: modified clicks (new tab,
1073
+ * download) and anchors that aren't siblings fall through untouched.
1074
+ */
1075
+ /** Installed once per document. `mount()` runs again on every client-side
1076
+ navigation between tag-only dashboards (TagOnlyDashboard unmounts the React
1077
+ root and re-mounts), and a listener added per mount is never removed — so
1078
+ without this each visit would add another handler and one click would fire
1079
+ navigate once per handler. injectTheme guards itself the same way. */
1080
+ let siblingLinksInstalled = false;
1081
+
1082
+ function interceptSiblingLinks() {
1083
+ if (typeof document === "undefined" || siblingLinksInstalled) return;
1084
+ siblingLinksInstalled = true;
1085
+ document.addEventListener(
1086
+ "click",
1087
+ (e: any) => {
1088
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
1089
+ const a = e.target && e.target.closest ? e.target.closest("a[href]") : null;
1090
+ if (!a || a.target === "_blank" || a.hasAttribute("download")) return;
1091
+ // dashboardList() is read HERE, per click, not captured at mount: the
1092
+ // header's rule is that injected globals are read lazily because script
1093
+ // order differs between hosts, and a host that sets __DASHBOARDS__ after
1094
+ // the bundle runs would otherwise get no interception at all — silently,
1095
+ // which is the very failure this exists to prevent.
1096
+ // Matched on the authored attribute, which is what the host injected.
1097
+ const href = a.getAttribute("href");
1098
+ const hit = dashboardList().find((d: any) => d && d.href === href);
1099
+ if (!hit) return;
1100
+ e.preventDefault();
1101
+ host.navigate(hit.name, {});
1102
+ },
1103
+ true,
1104
+ );
1105
+ }
1106
+
1032
1107
  export function mount(Dashboard, extraProps, rootEl: any = null, opts: any = {}) {
1033
1108
  injectTheme(opts.bodyReset !== false);
1109
+ interceptSiblingLinks();
1034
1110
  window.addEventListener("error", (e) => {
1035
1111
  if (isBenign(e && e.message)) return;
1036
1112
  showFatal((e.error && e.error.stack) || e.message);
package/dist/index.js CHANGED
@@ -558,6 +558,9 @@ function stripScalarArrayValue(parent, groups) {
558
558
  if (!isScalarArray(parent)) return groups;
559
559
  return { ...groups, dimensions: groups.dimensions.filter((f) => f.name !== "value") };
560
560
  }
561
+ function rawDef(structDefFields, name) {
562
+ return structDefFields.find((x) => (x.as ?? x.name) === name);
563
+ }
561
564
  function fieldKind(af, structDefFields) {
562
565
  const raw = structDefFields.find((x) => x.name === af.name);
563
566
  const et = raw?.expressionType;
@@ -617,6 +620,7 @@ function walkFields(fields, structDefFields, depth, ctx, anon) {
617
620
  const mLoc = f.location;
618
621
  const local = isLocal(mLoc, ctx.rootUri);
619
622
  const loc = local ? toLoc(mLoc) : void 0;
623
+ const access = rawDef(structDefFields, f.name)?.accessModifier;
620
624
  if (f.isExploreField()) {
621
625
  const ef = f;
622
626
  const cls = classifyJoinTarget(ef, ctx.knownSources);
@@ -630,6 +634,7 @@ function walkFields(fields, structDefFields, depth, ctx, anon) {
630
634
  if (needsQuote(f.name)) join5.must_quote = true;
631
635
  if (annotations.length > 0) join5.annotations = annotations;
632
636
  if (loc) join5.location = loc;
637
+ if (access) join5.access = access;
633
638
  const synthetic = isScalarArray(ef) || isRepeatedRecord(ef) || isAnonymousRecord(ef);
634
639
  if (isScalarArray(ef)) join5.column_shape = "scalar_array";
635
640
  else if (isRepeatedRecord(ef)) join5.column_shape = "record_array";
@@ -653,6 +658,7 @@ function walkFields(fields, structDefFields, depth, ctx, anon) {
653
658
  if (needsQuote(f.name)) view.must_quote = true;
654
659
  if (annotations.length > 0) view.annotations = annotations;
655
660
  if (loc) view.location = loc;
661
+ if (access) view.access = access;
656
662
  if (mLoc) {
657
663
  const body = sliceSource(ctx.readSource(mLoc.url), mLoc);
658
664
  if (body) view.body = body;
@@ -669,6 +675,7 @@ function walkFields(fields, structDefFields, depth, ctx, anon) {
669
675
  if (expr && expr !== f.name) info.expression = expr;
670
676
  if (annotations.length > 0) info.annotations = annotations;
671
677
  if (loc) info.location = loc;
678
+ if (access) info.access = access;
672
679
  if (fieldKind(af, structDefFields) === "measure") groups.measures.push(info);
673
680
  else groups.dimensions.push(info);
674
681
  }
@@ -840,6 +847,31 @@ async function compile(runtime, entry, opts = {}) {
840
847
  return { ok: false, problems: [errorProblem(e, entry.href)] };
841
848
  }
842
849
  }
850
+ var isPublic = (m) => m.access === void 0;
851
+ function publicGroups(g) {
852
+ return {
853
+ dimensions: g.dimensions.filter(isPublic),
854
+ measures: g.measures.filter(isPublic),
855
+ views: g.views.filter(isPublic),
856
+ joins: g.joins.filter(isPublic).map(
857
+ (j) => j.fields ? { ...j, fields: publicGroups(j.fields) } : j
858
+ )
859
+ };
860
+ }
861
+ function publicSource(s) {
862
+ const groups = publicGroups(s);
863
+ const out = { ...s, ...groups };
864
+ if (out.primary_key && !groups.dimensions.some((d) => d.name === out.primary_key)) {
865
+ out.primary_key = null;
866
+ }
867
+ if (s.anon_srcs) out.anon_srcs = s.anon_srcs.map(publicSource);
868
+ return out;
869
+ }
870
+ function publicOnlyModel(m) {
871
+ const sources = {};
872
+ for (const [name, s] of Object.entries(m.sources)) sources[name] = publicSource(s);
873
+ return { ...m, sources };
874
+ }
843
875
  var EMPTY_GROUPS = { dimensions: [], measures: [], views: [], joins: [] };
844
876
  var seg = (m) => m.must_quote ? `\`${m.name}\`` : m.name;
845
877
  var barePath = (prefix, j) => prefix ? `${prefix}.${j.name}` : j.name;
@@ -986,7 +1018,8 @@ function emitSourceJoin(j, bare, quoted, anonScope, fans, namedOnPath, anonOnPat
986
1018
  );
987
1019
  }
988
1020
  }
989
- function buildSourceDescribe(model, name) {
1021
+ function buildSourceDescribe(compiled, name) {
1022
+ const model = publicOnlyModel(compiled);
990
1023
  const root = model.sources[name];
991
1024
  if (!root) return void 0;
992
1025
  const ctx = { model, joins: /* @__PURE__ */ Object.create(null), map: /* @__PURE__ */ Object.create(null) };
@@ -2443,6 +2476,97 @@ async function makeRunner(root) {
2443
2476
  };
2444
2477
  }
2445
2478
 
2479
+ // src/discover.ts
2480
+ import fs3 from "node:fs";
2481
+ import path4 from "node:path";
2482
+ import { fileURLToPath } from "node:url";
2483
+ import { createRequire as createRequire2 } from "node:module";
2484
+ var require2 = createRequire2(import.meta.url);
2485
+ var HOST_LIBS = [
2486
+ "react",
2487
+ "react-dom",
2488
+ "react-dom/client",
2489
+ "react/jsx-runtime",
2490
+ "react/jsx-dev-runtime",
2491
+ "@malloydata/render",
2492
+ "@malloydata/malloy-filter"
2493
+ ];
2494
+ var HOST_ALIAS = {};
2495
+ for (const spec of HOST_LIBS) {
2496
+ try {
2497
+ HOST_ALIAS[spec] = require2.resolve(spec);
2498
+ } catch {
2499
+ }
2500
+ }
2501
+ function resolveRuntimeDir() {
2502
+ const candidates = [
2503
+ new URL("./frame-runtime/", import.meta.url),
2504
+ // src/dashboard.ts (tsx) OR dist/index.js (published)
2505
+ new URL("../src/frame-runtime/", import.meta.url)
2506
+ // built dist/ next to sibling src/ (checkout)
2507
+ ].map((u) => fileURLToPath(u));
2508
+ const found = candidates.find((c) => fs3.existsSync(c));
2509
+ if (!found) {
2510
+ throw new Error(
2511
+ "frame-runtime/ not found next to the CLI (looked in ./frame-runtime and ../src/frame-runtime). A published install should ship it in dist/; reinstall the CLI, or rebuild with `npm run build`."
2512
+ );
2513
+ }
2514
+ return found;
2515
+ }
2516
+ var hostAliasPlugin = {
2517
+ name: "host-alias",
2518
+ setup(b) {
2519
+ b.onResolve(
2520
+ { filter: /^(react($|\/)|react-dom($|\/)|@malloydata\/(render|malloy-filter)$)/ },
2521
+ (args) => HOST_ALIAS[args.path] ? { path: HOST_ALIAS[args.path] } : void 0
2522
+ );
2523
+ }
2524
+ };
2525
+ async function discoverDashboards(root, runner) {
2526
+ const dir = path4.join(root, "dashboards");
2527
+ if (!fs3.existsSync(dir)) return [];
2528
+ const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".malloy")).sort();
2529
+ const dashboards = [];
2530
+ for (const file of files) {
2531
+ const base = file.slice(0, -".malloy".length);
2532
+ const entryFile = path4.join("dashboards", file);
2533
+ const res = await runner.artifactForFile(entryFile, base);
2534
+ if (!res.ok) throw new Error(`dashboard ${file}: ${res.error}`);
2535
+ if (!res.artifact) continue;
2536
+ const component = ["jsx", "tsx"].map((ext) => path4.join(dir, `${base}.${ext}`)).find((p) => fs3.existsSync(p));
2537
+ dashboards.push({ ...res.artifact, name: res.artifact.name || base, entryFile, tsxPath: component });
2538
+ }
2539
+ const about = aboutPage(root);
2540
+ if (about && !dashboards.some((d) => d.name === about.name)) dashboards.unshift(about);
2541
+ return dashboards;
2542
+ }
2543
+ function browserBuildBase() {
2544
+ const shims = path4.join(resolveRuntimeDir(), "..", "shims");
2545
+ return {
2546
+ platform: "browser",
2547
+ jsx: "automatic",
2548
+ loader: { ".css": "empty" },
2549
+ define: { "process.env.NODE_ENV": '"production"' },
2550
+ alias: {
2551
+ assert: path4.join(shims, "assert.cjs"),
2552
+ util: path4.join(shims, "util.cjs")
2553
+ },
2554
+ banner: {
2555
+ js: "globalThis.process||={env:{},platform:'browser',versions:{},argv:[],cwd:()=>'/'};"
2556
+ }
2557
+ };
2558
+ }
2559
+ var ABOUT_NAME = "index";
2560
+ var ABOUT_TITLE = "About";
2561
+ function aboutPage(root) {
2562
+ const dir = path4.join(root, "dashboards");
2563
+ if (fs3.existsSync(path4.join(dir, "index.malloy"))) return null;
2564
+ const component = ["jsx", "tsx"].map((ext) => path4.join(dir, `index.${ext}`)).find((p) => fs3.existsSync(p));
2565
+ if (!component) return null;
2566
+ return { name: ABOUT_NAME, query: "", title: ABOUT_TITLE, tsxPath: component };
2567
+ }
2568
+ var rendersNoData = (d) => !d.query && (!d.tiles || d.tiles.length === 0);
2569
+
2446
2570
  // src/gather.ts
2447
2571
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
2448
2572
  function gatherDirectory(dir) {
@@ -2494,6 +2618,14 @@ async function gatherDashboards(dir) {
2494
2618
  source: component ? readFileSync2(component, "utf8") : ""
2495
2619
  });
2496
2620
  }
2621
+ const about = aboutPage(dir);
2622
+ if (about?.tsxPath && !payloads.some((p) => p.name === about.name)) {
2623
+ payloads.unshift({
2624
+ name: about.name,
2625
+ manifest: { title: about.title },
2626
+ source: readFileSync2(about.tsxPath, "utf8")
2627
+ });
2628
+ }
2497
2629
  return payloads;
2498
2630
  } finally {
2499
2631
  await runner.dispose();
@@ -2538,6 +2670,16 @@ function componentQueryLiterals(source) {
2538
2670
  while ((m = re.exec(source)) !== null) out.add(m[2].trim());
2539
2671
  return [...out];
2540
2672
  }
2673
+ function landingPageErrors(dir, file) {
2674
+ const ext = file.endsWith(".tsx") ? "tsx" : "jsx";
2675
+ try {
2676
+ esbuild.transformSync(readFileSync3(join3(dir, file), "utf8"), { loader: ext, jsx: "automatic" });
2677
+ return [];
2678
+ } catch (e) {
2679
+ const msg = e.errors?.map((x) => x.text).join("; ") ?? String(e);
2680
+ return [`${file}: ${msg}`];
2681
+ }
2682
+ }
2541
2683
  async function lintDashboards(root) {
2542
2684
  const abs = resolve(root);
2543
2685
  const runner = await makeRunner(abs);
@@ -2560,13 +2702,16 @@ async function runLint(abs, runner) {
2560
2702
  const malloyBases = new Set(malloyFiles.map((f) => f.slice(0, -".malloy".length)));
2561
2703
  for (const c of entries.filter((f) => /\.(jsx|tsx)$/.test(f)).sort()) {
2562
2704
  const cbase = c.replace(/\.(jsx|tsx)$/, "");
2563
- if (!malloyBases.has(cbase)) {
2564
- dashboards.push({
2565
- name: c,
2566
- errors: [`component "${c}" has no matching "${cbase}.malloy" dashboard`],
2567
- warnings: []
2568
- });
2705
+ if (malloyBases.has(cbase)) continue;
2706
+ if (cbase === "index") {
2707
+ dashboards.push({ name: c, errors: landingPageErrors(dir, c), warnings: [] });
2708
+ continue;
2569
2709
  }
2710
+ dashboards.push({
2711
+ name: c,
2712
+ errors: [`component "${c}" has no matching "${cbase}.malloy" dashboard`],
2713
+ warnings: []
2714
+ });
2570
2715
  }
2571
2716
  const seenNames = /* @__PURE__ */ new Map();
2572
2717
  for (const file of malloyFiles) {
@@ -2735,7 +2880,7 @@ function clearCreds(url6) {
2735
2880
  }
2736
2881
 
2737
2882
  // package.json
2738
- var version = "0.2.35";
2883
+ var version = "0.2.37";
2739
2884
 
2740
2885
  // src/http.ts
2741
2886
  var USER_AGENT = `malloyyo/${version}`;
@@ -2917,8 +3062,8 @@ Run: malloyyo login ${target.name}`);
2917
3062
  }
2918
3063
 
2919
3064
  // src/mcp.ts
2920
- import fs3 from "node:fs";
2921
- import path4 from "node:path";
3065
+ import fs4 from "node:fs";
3066
+ import path5 from "node:path";
2922
3067
  import url4 from "node:url";
2923
3068
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2924
3069
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -3007,7 +3152,7 @@ function defaultConfig(rootUrl) {
3007
3152
  });
3008
3153
  }
3009
3154
  async function loadConfig2(root, reader) {
3010
- const rootUrl = url4.pathToFileURL(root + path4.sep);
3155
+ const rootUrl = url4.pathToFileURL(root + path5.sep);
3011
3156
  let discovered;
3012
3157
  try {
3013
3158
  discovered = await discoverConfig2(rootUrl, rootUrl, reader);
@@ -3029,13 +3174,13 @@ function fsReader2() {
3029
3174
  if (u.protocol !== "file:") {
3030
3175
  throw new Error(`unsupported URL scheme for import: ${u.href}`);
3031
3176
  }
3032
- return fs3.promises.readFile(u, "utf8");
3177
+ return fs4.promises.readFile(u, "utf8");
3033
3178
  }
3034
3179
  };
3035
3180
  }
3036
3181
  function resolveUnderRoot(root, p) {
3037
- const abs = p.includes("://") ? path4.resolve(decodeURIComponent(new URL(p).pathname)) : path4.resolve(root, p);
3038
- if (abs !== root && !abs.startsWith(root + path4.sep)) {
3182
+ const abs = p.includes("://") ? path5.resolve(decodeURIComponent(new URL(p).pathname)) : path5.resolve(root, p);
3183
+ if (abs !== root && !abs.startsWith(root + path5.sep)) {
3039
3184
  throw new Error(`path is outside the project root: ${p}`);
3040
3185
  }
3041
3186
  return abs;
@@ -3044,7 +3189,7 @@ function makeConfigSource(root) {
3044
3189
  let cached;
3045
3190
  const signature = () => ["malloy-config.json", "malloy-config-local.json"].map((name) => {
3046
3191
  try {
3047
- const st = fs3.statSync(path4.join(root, name));
3192
+ const st = fs4.statSync(path5.join(root, name));
3048
3193
  return `${name}:${st.mtimeMs}:${st.size}`;
3049
3194
  } catch {
3050
3195
  return `${name}:absent`;
@@ -3064,7 +3209,7 @@ function makeWithRuntime(root, currentConfig) {
3064
3209
  return gateConfigProblems(problems, async () => {
3065
3210
  const resolved = "url" in input ? { url: resolveUnderRoot(root, input.url) } : {
3066
3211
  source: input.source,
3067
- baseUrl: input.baseUrl ? resolveUnderRoot(root, input.baseUrl) : root + path4.sep
3212
+ baseUrl: input.baseUrl ? resolveUnderRoot(root, input.baseUrl) : root + path5.sep
3068
3213
  };
3069
3214
  const { reader, entry, readSource } = prepareSource(fsReader2(), resolved);
3070
3215
  const runtime = new Runtime2({ config, urlReader: reader });
@@ -3078,7 +3223,7 @@ function makeWithRuntime(root, currentConfig) {
3078
3223
  }
3079
3224
  function makeExploreHost(root, currentConfig) {
3080
3225
  const withRuntime = makeWithRuntime(root, currentConfig);
3081
- const published = (ref) => ref === ENTRY2 && fs3.existsSync(path4.join(root, ENTRY2));
3226
+ const published = (ref) => ref === ENTRY2 && fs4.existsSync(path5.join(root, ENTRY2));
3082
3227
  return {
3083
3228
  withModel: (ref, fn) => {
3084
3229
  if (!published(ref)) throw new Error(`no published model '${ref}'`);
@@ -3099,7 +3244,7 @@ function makeDevelopHost(root, currentConfig) {
3099
3244
  }
3100
3245
  async function serveMcp(opts) {
3101
3246
  await initConnections();
3102
- const root = path4.resolve(opts.root ?? process.cwd());
3247
+ const root = path5.resolve(opts.root ?? process.cwd());
3103
3248
  const mode = opts.mode ?? "explore";
3104
3249
  const currentConfig = makeConfigSource(root);
3105
3250
  const surface = mode === "develop" ? developSurface(makeDevelopHost(root, currentConfig)) : exploreSurface(makeExploreHost(root, currentConfig));
@@ -3172,84 +3317,13 @@ function navHtml(active, all, href, homeHref = "./") {
3172
3317
  ).join("");
3173
3318
  return `<nav class="dash-nav">${brand}<span class="sep"></span>${links}</nav>`;
3174
3319
  }
3175
-
3176
- // src/discover.ts
3177
- import fs4 from "node:fs";
3178
- import path5 from "node:path";
3179
- import { fileURLToPath } from "node:url";
3180
- import { createRequire as createRequire2 } from "node:module";
3181
- var require2 = createRequire2(import.meta.url);
3182
- var HOST_LIBS = [
3183
- "react",
3184
- "react-dom",
3185
- "react-dom/client",
3186
- "react/jsx-runtime",
3187
- "react/jsx-dev-runtime",
3188
- "@malloydata/render",
3189
- "@malloydata/malloy-filter"
3190
- ];
3191
- var HOST_ALIAS = {};
3192
- for (const spec of HOST_LIBS) {
3193
- try {
3194
- HOST_ALIAS[spec] = require2.resolve(spec);
3195
- } catch {
3196
- }
3197
- }
3198
- function resolveRuntimeDir() {
3199
- const candidates = [
3200
- new URL("./frame-runtime/", import.meta.url),
3201
- // src/dashboard.ts (tsx) OR dist/index.js (published)
3202
- new URL("../src/frame-runtime/", import.meta.url)
3203
- // built dist/ next to sibling src/ (checkout)
3204
- ].map((u) => fileURLToPath(u));
3205
- const found = candidates.find((c) => fs4.existsSync(c));
3206
- if (!found) {
3207
- throw new Error(
3208
- "frame-runtime/ not found next to the CLI (looked in ./frame-runtime and ../src/frame-runtime). A published install should ship it in dist/; reinstall the CLI, or rebuild with `npm run build`."
3209
- );
3210
- }
3211
- return found;
3212
- }
3213
- var hostAliasPlugin = {
3214
- name: "host-alias",
3215
- setup(b) {
3216
- b.onResolve(
3217
- { filter: /^(react($|\/)|react-dom($|\/)|@malloydata\/(render|malloy-filter)$)/ },
3218
- (args) => HOST_ALIAS[args.path] ? { path: HOST_ALIAS[args.path] } : void 0
3219
- );
3220
- }
3221
- };
3222
- async function discoverDashboards(root, runner) {
3223
- const dir = path5.join(root, "dashboards");
3224
- if (!fs4.existsSync(dir)) return [];
3225
- const files = fs4.readdirSync(dir).filter((f) => f.endsWith(".malloy")).sort();
3226
- const dashboards = [];
3227
- for (const file of files) {
3228
- const base = file.slice(0, -".malloy".length);
3229
- const entryFile = path5.join("dashboards", file);
3230
- const res = await runner.artifactForFile(entryFile, base);
3231
- if (!res.ok) throw new Error(`dashboard ${file}: ${res.error}`);
3232
- if (!res.artifact) continue;
3233
- const component = ["jsx", "tsx"].map((ext) => path5.join(dir, `${base}.${ext}`)).find((p) => fs4.existsSync(p));
3234
- dashboards.push({ ...res.artifact, name: res.artifact.name || base, entryFile, tsxPath: component });
3235
- }
3236
- return dashboards;
3237
- }
3238
- function browserBuildBase() {
3239
- const shims = path5.join(resolveRuntimeDir(), "..", "shims");
3240
- return {
3241
- platform: "browser",
3242
- jsx: "automatic",
3243
- loader: { ".css": "empty" },
3244
- define: { "process.env.NODE_ENV": '"production"' },
3245
- alias: {
3246
- assert: path5.join(shims, "assert.cjs"),
3247
- util: path5.join(shims, "util.cjs")
3248
- },
3249
- banner: {
3250
- js: "globalThis.process||={env:{},platform:'browser',versions:{},argv:[],cwd:()=>'/'};"
3251
- }
3252
- };
3320
+ function siblingList(current, all, href) {
3321
+ return all.filter((d) => d.name !== current).map((d) => ({
3322
+ name: d.name,
3323
+ title: d.title || d.name,
3324
+ ...d.description ? { description: d.description } : {},
3325
+ href: href(d.name)
3326
+ }));
3253
3327
  }
3254
3328
 
3255
3329
  // src/dashboard.ts
@@ -3330,6 +3404,7 @@ function makeInPageBundler() {
3330
3404
  };
3331
3405
  }
3332
3406
  var html = (body, title) => `<!doctype html><html><head><meta charset="utf-8"><title>${esc2(title)}</title><meta name="viewport" content="width=device-width,initial-scale=1"><style>${NAV_CSS}</style></head><body style="margin:0">${body}</body></html>`;
3407
+ var devSiblings = (dash, all) => siblingList(dash.name, all, (n) => `/?d=${encodeURIComponent(n)}`);
3333
3408
  function navHtml2(dash, all) {
3334
3409
  return navHtml(dash.name, all, (n) => `/?d=${encodeURIComponent(n)}`);
3335
3410
  }
@@ -3346,7 +3421,7 @@ function inPageShell(dash, all, givenSpecs, initialGivens, initialUrlState, tile
3346
3421
  autorun: dash.autorun
3347
3422
  };
3348
3423
  return html(
3349
- navHtml2(dash, all) + `<div id="root"></div><script>window.__DASHBOARD__=${safeJson(info)};window.__GIVENS__=${safeJson(givenSpecs)};window.__INITIAL_GIVENS__=${safeJson(initialGivens)};window.__INITIAL_URLSTATE__=${safeJson(initialUrlState)}</script><script>try{new EventSource('/events').onmessage=()=>location.reload();}catch(e){}</script><script src="/inpage.js?d=${encodeURIComponent(dash.name)}"></script>`,
3424
+ navHtml2(dash, all) + `<div id="root"></div><script>window.__DASHBOARD__=${safeJson(info)};window.__DASHBOARDS__=${safeJson(devSiblings(dash, all))};window.__GIVENS__=${safeJson(givenSpecs)};window.__INITIAL_GIVENS__=${safeJson(initialGivens)};window.__INITIAL_URLSTATE__=${safeJson(initialUrlState)}</script><script>try{new EventSource('/events').onmessage=()=>location.reload();}catch(e){}</script><script src="/inpage.js?d=${encodeURIComponent(dash.name)}"></script>`,
3350
3425
  dash.title
3351
3426
  );
3352
3427
  }
@@ -3413,7 +3488,7 @@ function givensFromUrl(url6) {
3413
3488
  function urlStateFromUrl(url6) {
3414
3489
  return urlStateFromSearch(url6.search);
3415
3490
  }
3416
- function frameDoc(dash, givenSpecs, initialGivens, initialUrlState, tileSpecs) {
3491
+ function frameDoc(dash, all, givenSpecs, initialGivens, initialUrlState, tileSpecs) {
3417
3492
  const info = {
3418
3493
  name: dash.name,
3419
3494
  query: dash.query,
@@ -3428,7 +3503,7 @@ function frameDoc(dash, givenSpecs, initialGivens, initialUrlState, tileSpecs) {
3428
3503
  autorun: dash.autorun
3429
3504
  };
3430
3505
  return html(
3431
- `<div id="root"></div><script>window.__DASHBOARD__=${safeJson(info)};window.__GIVENS__=${safeJson(givenSpecs)};window.__INITIAL_GIVENS__=${safeJson(initialGivens)};window.__INITIAL_URLSTATE__=${safeJson(initialUrlState)}</script><script src="/bundle.js?d=${encodeURIComponent(dash.name)}"></script>`,
3506
+ `<div id="root"></div><script>window.__DASHBOARD__=${safeJson(info)};window.__DASHBOARDS__=${safeJson(devSiblings(dash, all))};window.__GIVENS__=${safeJson(givenSpecs)};window.__INITIAL_GIVENS__=${safeJson(initialGivens)};window.__INITIAL_URLSTATE__=${safeJson(initialUrlState)}</script><script src="/bundle.js?d=${encodeURIComponent(dash.name)}"></script>`,
3432
3507
  dash.title
3433
3508
  );
3434
3509
  }
@@ -3458,6 +3533,7 @@ async function serveDashboard(opts) {
3458
3533
  const inPageBundle = makeInPageBundler();
3459
3534
  const pick = (url6) => byName.get(url6.searchParams.get("d") ?? dashboards[0].name) ?? dashboards[0];
3460
3535
  async function resolveGivens(dash) {
3536
+ if (rendersNoData(dash)) return { ok: true, union: [] };
3461
3537
  if (dash.tiles && dash.entryFile) {
3462
3538
  const t = await runner.dashboardTiles(dash.entryFile, dash.tiles);
3463
3539
  return { ok: true, union: t.union, tiles: t.tiles };
@@ -3510,7 +3586,7 @@ async function serveDashboard(opts) {
3510
3586
  return send(
3511
3587
  200,
3512
3588
  "text/html; charset=utf-8",
3513
- frameDoc(dash, g.union, givensFromUrl(url6), urlStateFromUrl(url6), g.tiles)
3589
+ frameDoc(dash, dashboards, g.union, givensFromUrl(url6), urlStateFromUrl(url6), g.tiles)
3514
3590
  );
3515
3591
  }
3516
3592
  if (url6.pathname === "/bundle.js") {
@@ -3795,11 +3871,12 @@ window.__GIVENS__ = ${safeJson(givenSpecs)};
3795
3871
  }
3796
3872
  function indexPage(dashboards, title, custom, cleanUrls, analytics) {
3797
3873
  const link = (n) => cleanUrls ? `./${encodeURIComponent(n)}` : `./${encodeURIComponent(n)}.html`;
3874
+ const listed = dashboards.filter((d) => !rendersNoData(d));
3798
3875
  const body = custom ? `<div id="root"></div>
3799
3876
  <script>window.__DASHBOARDS__ = ${safeJson(
3800
- dashboards.map((d) => ({ name: d.name, title: d.title, description: d.description, href: link(d.name) }))
3877
+ listed.map((d) => ({ name: d.name, title: d.title, description: d.description, href: link(d.name) }))
3801
3878
  )};</script>
3802
- <script type="module" src="./assets/index.js"></script>` : `<main class="index"><h1>${esc3(title)}</h1><ul>` + dashboards.map(
3879
+ <script type="module" src="./assets/index.js"></script>` : `<main class="index"><h1>${esc3(title)}</h1><ul>` + listed.map(
3803
3880
  (d) => `<li><a href="${link(d.name)}"><strong>${esc3(d.title || d.name)}</strong>` + (d.description ? `<span>${esc3(d.description)}</span>` : "") + `</a></li>`
3804
3881
  ).join("") + `</ul></main>`;
3805
3882
  return `<!doctype html>
@@ -3960,6 +4037,7 @@ boot(Dashboard);
3960
4037
  });
3961
4038
  fs7.writeFileSync(path8.join(outDir, "assets", "site.css"), SITE_CSS);
3962
4039
  for (const d of dashboards) {
4040
+ if (rendersNoData(d)) continue;
3963
4041
  let specs = [];
3964
4042
  let tileSpecs;
3965
4043
  if (d.tiles && d.entryFile) {
@@ -70,3 +70,25 @@ export function navHtml(
70
70
  .join("");
71
71
  return `<nav class="dash-nav">${brand}<span class="sep"></span>${links}</nav>`;
72
72
  }
73
+
74
+ /** The sibling list a host injects as `window.__DASHBOARDS__`, in nav order and
75
+ excluding the current page.
76
+ *
77
+ * Same shape and same link-shape callback as `navHtml`, because it answers the
78
+ * same question in component form: an About page (or any dashboard) that wants
79
+ * to link to the others gets `props.dashboards` and never has to know whether it
80
+ * is running under the dev server, the hosted frame, or a static bundle. */
81
+ export function siblingList<T extends NavDashboard & { description?: string }>(
82
+ current: string,
83
+ all: readonly T[],
84
+ href: (name: string) => string,
85
+ ): Array<{ name: string; title: string; description?: string; href: string }> {
86
+ return all
87
+ .filter((d) => d.name !== current)
88
+ .map((d) => ({
89
+ name: d.name,
90
+ title: d.title || d.name,
91
+ ...(d.description ? { description: d.description } : {}),
92
+ href: href(d.name),
93
+ }));
94
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/malloyyo",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
4
4
  "description": "Publish Malloy models to a Malloyyo instance",
5
5
  "license": "MIT",
6
6
  "repository": {