@marver-design/marver 0.16.1 → 0.18.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +3 -2
  3. package/dist/bake-jr38C_pX.mjs +747 -0
  4. package/dist/{build-B4yPgFNF.mjs → build-ER_7T6Cw.mjs} +179 -10
  5. package/dist/cli.mjs +21 -13
  6. package/dist/{comments-oYcZ3cE-.mjs → comments-ClVgfQib.mjs} +1 -1
  7. package/dist/{daemon-Bbh_jmui.mjs → daemon-CKhg0zuT.mjs} +1 -1
  8. package/dist/{dev-DH2W7Ffw.mjs → dev-BvLbY98O.mjs} +208 -9
  9. package/dist/{init-B7YhcN2o.mjs → init-BWbHqVng.mjs} +2 -2
  10. package/dist/{manifest-CaslQIAO.mjs → manifest-DAnEL8_a.mjs} +1 -1
  11. package/dist/{marver-id-gate-D6By7XHj.mjs → marver-id-gate-B_idGdHm.mjs} +1 -1
  12. package/dist/{plugin-BeBGu3gH.mjs → plugin-Cai5C1WV.mjs} +244 -47
  13. package/dist/{poster-CoyobbGW.mjs → poster-CIuz_PwH.mjs} +13 -5
  14. package/dist/publish-bakes-D0LhbQQ3.mjs +216 -0
  15. package/dist/{serve-Bcwfpvhl.mjs → serve-z5qtj_wJ.mjs} +3 -3
  16. package/dist/{share-Gqo_Ygqw.mjs → share--bdSc4G5.mjs} +1 -1
  17. package/dist/shot-BFEuYbaz.mjs +86 -0
  18. package/dist/shot-iicees2e.mjs +933 -0
  19. package/dist/{work-lzC-lPY0.mjs → work-0YopuMt9.mjs} +1 -1
  20. package/docs/live-jam.md +20 -8
  21. package/docs/publish.md +27 -5
  22. package/package.json +1 -1
  23. package/src/client/frame-host/bridge.js +4 -9
  24. package/src/client/frame-host/main.tsx +20 -6
  25. package/src/client/shell/App.tsx +7 -0
  26. package/src/client/shell/Comments.tsx +2 -2
  27. package/src/client/shell/canvas/Canvas.tsx +2 -2
  28. package/src/client/shell/canvas/FrameNode.tsx +82 -89
  29. package/src/client/shell/canvas/admission.ts +70 -0
  30. package/src/client/shell/canvas/sleep.ts +194 -0
  31. package/src/client/shell/store.ts +14 -0
  32. package/src/client/shell/styles.css +9 -25
  33. package/src/shared/sleep-rule.ts +41 -0
  34. package/templates/AGENTS-embedded.md +4 -1
  35. package/templates/AGENTS-studio.md +4 -1
  36. package/templates/instructions/craft.md +9 -0
  37. package/templates/instructions/jam.md +15 -3
  38. package/templates/instructions/publish.md +62 -9
  39. package/templates/instructions/shape.md +2 -1
  40. package/dist/shot-By1AItpD.mjs +0 -30
  41. package/dist/shot-z-d-zMzf.mjs +0 -528
  42. package/src/client/frame-host/serialize.ts +0 -195
  43. package/src/client/shell/canvas/snapshots.ts +0 -233
@@ -1,6 +1,6 @@
1
1
  import { i as avatarSourceFor, n as attachAvatar, p as provisionFromMarverId } from "./auth-DdUKeVKt.mjs";
2
2
  import { TransactionStore, browserBinding, verifyAssertion } from "./marver-id-CVCoaMGI.mjs";
3
- import { t as poweredByUrl } from "./serve-Bcwfpvhl.mjs";
3
+ import { t as poweredByUrl } from "./serve-z5qtj_wJ.mjs";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { isIP } from "node:net";
6
6
  import { lookup } from "node:dns/promises";
@@ -1,8 +1,8 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { i as ROUTE, n as NAME, r as PKG } from "./cli.mjs";
2
+ import { a as ROUTE, i as PKG, r as NAME } from "./cli.mjs";
3
3
  import { a as listBoardFiles, c as BOARD_NAME, g as hash, h as validateWire, i as isRegularFile, l as FOLDERS_FILE, m as readTitle, n as checkBoardsDir, o as nodeExists, p as readDescription, s as readRegistry, t as boardFields } from "./boards-BmxcT3Lc.mjs";
4
4
  import { n as localProfile, t as isConnected } from "./profile-BjAPAJSb.mjs";
5
- import { a as writeManifest, n as scanFrames, r as setSceneTitle, s as loadConfig, t as affectedFrameIds } from "./manifest-CaslQIAO.mjs";
5
+ import { a as writeManifest, n as scanFrames, r as setSceneTitle, s as loadConfig, t as affectedFrameIds } from "./manifest-DAnEL8_a.mjs";
6
6
  import { copyFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
7
7
  import { basename, dirname, join, resolve, sep } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
@@ -422,7 +422,7 @@ function apiMiddleware(root, opts = {}) {
422
422
  };
423
423
  const asPng = url.searchParams.get("format") === "png";
424
424
  const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
425
- const { shootFrame } = await import("./shot-z-d-zMzf.mjs");
425
+ const { shootFrame } = await import("./shot-iicees2e.mjs");
426
426
  const r = await shootFrame({
427
427
  root,
428
428
  viewports: opts.viewports ?? {},
@@ -441,6 +441,7 @@ function apiMiddleware(root, opts = {}) {
441
441
  height: r.height,
442
442
  scale: r.scale,
443
443
  ...r.truncated ? { truncated: true } : {},
444
+ ...r.unsettled ? { unsettled: true } : {},
444
445
  ...r.note ? { note: r.note } : {}
445
446
  };
446
447
  if (!asPng) return json(res, 200, summary);
@@ -453,11 +454,98 @@ function apiMiddleware(root, opts = {}) {
453
454
  });
454
455
  return res.end(png);
455
456
  }
457
+ if (path === "shots" && req.method === "POST") {
458
+ const { readDevInfo } = await import("./work-CLrmY-vQ.mjs");
459
+ const token = readDevInfo(root)?.token;
460
+ if (!ownerGated(req) && !(token && req.headers["x-mv-work"] === token)) return json(res, 403, { error: "forbidden" });
461
+ const raw = await readBody(req);
462
+ if (raw == null) return json(res, 400, { error: "body too large or unreadable" });
463
+ let parsed;
464
+ try {
465
+ parsed = JSON.parse(raw);
466
+ } catch {
467
+ return json(res, 400, { error: "malformed JSON" });
468
+ }
469
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return json(res, 400, { error: "expected an object" });
470
+ const body = parsed;
471
+ const theme = body.theme === void 0 ? "light" : body.theme;
472
+ if (typeof theme !== "string" || !/^[a-z0-9-]+$/i.test(theme)) return json(res, 400, { error: "invalid theme" });
473
+ const scale = body.scale === void 0 ? void 0 : body.scale;
474
+ if (scale !== void 0 && !(Number.isInteger(scale) && scale >= 1 && scale <= 4)) return json(res, 400, { error: "invalid scale" });
475
+ const { resolveFrames, shootBatch } = await import("./shot-iicees2e.mjs");
476
+ const sel = resolveFrames(root, body);
477
+ if (!sel.ok) return json(res, sel.status, { error: sel.error });
478
+ const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
479
+ try {
480
+ return json(res, 200, { results: await shootBatch({
481
+ root,
482
+ viewports: opts.viewports ?? {},
483
+ frames: sel.frames,
484
+ theme,
485
+ origin,
486
+ scale
487
+ }) });
488
+ } catch (err) {
489
+ return json(res, 503, { error: err.message });
490
+ }
491
+ }
492
+ if (path === "bakes" && req.method === "POST") {
493
+ if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
494
+ const raw = await readBody(req);
495
+ if (raw == null) return json(res, 400, { error: "body too large or unreadable" });
496
+ let body;
497
+ try {
498
+ body = JSON.parse(raw);
499
+ } catch {
500
+ return json(res, 400, { error: "malformed JSON" });
501
+ }
502
+ if (!body || typeof body !== "object" || !Array.isArray(body.asks) || !body.asks.length || body.asks.length > 200) return json(res, 400, { error: "asks must list 1-200 frames" });
503
+ const { bakeBatch, ASK_MAX } = await import("./bake-jr38C_pX.mjs");
504
+ let manifest = {};
505
+ try {
506
+ manifest = JSON.parse(readFileSync(join(root, "design", "manifest.json"), "utf8"));
507
+ } catch {}
508
+ const frames = new Map((manifest.frames ?? []).map((f) => [f.id, f]));
509
+ const asks = [];
510
+ for (const a of body.asks) {
511
+ const frame = typeof a?.frame === "string" ? a.frame : "";
512
+ const theme = typeof a?.theme === "string" && /^[a-z0-9-]{1,32}$/i.test(a.theme) ? a.theme : "";
513
+ const w = Math.round(Number(a?.w)), h = Math.round(Number(a?.h));
514
+ if (!frames.has(frame) || !theme || !(w >= 120 && h >= 80 && w <= ASK_MAX.side && h <= ASK_MAX.side && w * h <= ASK_MAX.area)) return json(res, 400, { error: `invalid ask for "${frame}"` });
515
+ asks.push({
516
+ frame,
517
+ theme,
518
+ w,
519
+ h
520
+ });
521
+ }
522
+ const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
523
+ const { ROUTE } = await import("./cli.mjs").then((n) => n.t);
524
+ const gen = opts.bakeGen?.() ?? 0;
525
+ return json(res, 200, {
526
+ gen,
527
+ answers: await bakeBatch({
528
+ root,
529
+ gen,
530
+ asks,
531
+ live: () => opts.bakeGen?.() ?? 0,
532
+ log: (a) => {
533
+ if (a.ok) console.log(` bake: ${a.frame} ${a.theme} ${a.w}x${a.h} - ${a.targets.length} effects, ${a.rejected} stay live, ${a.ms} ms`);
534
+ else console.log(` bake: ${a.frame} ${a.theme} ${a.w}x${a.h} - stays live: ${a.error}`);
535
+ },
536
+ urlFor: (ask) => {
537
+ const f = frames.get(ask.frame);
538
+ return f.kind === "html" ? `${origin}/${f.file}?theme=${encodeURIComponent(ask.theme)}` : `${origin}${ROUTE}/frame/?id=${encodeURIComponent(ask.frame)}&theme=${encodeURIComponent(ask.theme)}`;
539
+ },
540
+ urlBase: `${ROUTE}/bakes`
541
+ })
542
+ });
543
+ }
456
544
  if (path === "poster" && req.method === "GET") {
457
545
  if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
458
546
  const src = url.searchParams.get("src") ?? "";
459
- const { ensurePoster, isLocalClip } = await import("./poster-CoyobbGW.mjs");
460
- const { isLocalAssetRef } = await import("./build-B4yPgFNF.mjs");
547
+ const { ensurePoster, isLocalClip } = await import("./poster-CIuz_PwH.mjs");
548
+ const { isLocalAssetRef } = await import("./build-ER_7T6Cw.mjs");
461
549
  if (!isLocalAssetRef(src) || !isLocalClip(src)) return json(res, 400, { error: "src must be a clip under design/assets/" });
462
550
  const r = await ensurePoster(join(root, "design", "assets"), src);
463
551
  if (!r.ok) return json(res, r.error.includes("does not exist") ? 404 : 503, { error: r.error });
@@ -645,6 +733,24 @@ function routesMiddleware(server, clientDir) {
645
733
  } catch {
646
734
  return next();
647
735
  }
736
+ } else if (path.startsWith(`/__mv/bakes/`)) {
737
+ const m = /^\/(\d+)\/([a-f0-9]{16})\/(\d+)\.png$/.exec(path.slice(`${ROUTE}/bakes`.length));
738
+ if (!m) {
739
+ res.statusCode = 404;
740
+ return res.end();
741
+ }
742
+ try {
743
+ const base = realpathSync(join(server.config.root, "design", ".local", "bakes"));
744
+ const file = realpathSync(join(base, m[1], m[2], `${m[3]}.png`));
745
+ if (!file.startsWith(base + sep)) throw new Error("outside the cache");
746
+ const png = readFileSync(file);
747
+ res.setHeader("content-type", "image/png");
748
+ res.setHeader("cache-control", "public, max-age=31536000, immutable");
749
+ return res.end(png);
750
+ } catch {
751
+ res.statusCode = 404;
752
+ return res.end();
753
+ }
648
754
  } else if (path === `/__mv/bridge.js`) {
649
755
  res.setHeader("content-type", "text/javascript");
650
756
  return res.end(readFileSync(join(clientDir, "frame-host", "bridge.js"), "utf8"));
@@ -810,27 +916,19 @@ function marverPlugin(ctx) {
810
916
  const bridge = "/@fs/" + join(clientDir, "frame-host", "bridge.js").split("\\").join("/") + "?html=1";
811
917
  return {
812
918
  html,
813
- tags: [
814
- {
815
- tag: "script",
816
- children: `(function(){var a=Element.prototype.attachShadow;if(a)Element.prototype.attachShadow=function(i){if(i&&i.mode==='closed')window.__mvClosedShadow=1;return a.call(this,i)};})();`,
817
- injectTo: "head-prepend"
919
+ tags: [{
920
+ tag: "script",
921
+ attrs: { type: "module" },
922
+ children: `import '${VIRTUAL_THEME}'`,
923
+ injectTo: "head-prepend"
924
+ }, {
925
+ tag: "script",
926
+ attrs: {
927
+ type: "module",
928
+ src: bridge
818
929
  },
819
- {
820
- tag: "script",
821
- attrs: { type: "module" },
822
- children: `import '${VIRTUAL_THEME}'`,
823
- injectTo: "head-prepend"
824
- },
825
- {
826
- tag: "script",
827
- attrs: {
828
- type: "module",
829
- src: bridge
830
- },
831
- injectTo: "head-prepend"
832
- }
833
- ]
930
+ injectTo: "head-prepend"
931
+ }]
834
932
  };
835
933
  }
836
934
  },
@@ -852,6 +950,7 @@ function marverPlugin(ctx) {
852
950
  return [];
853
951
  },
854
952
  configureServer(server) {
953
+ let bakeGen = Date.now();
855
954
  server.middlewares.use((req, res, next) => {
856
955
  if (req.url && req.url.includes("/frame-host/registry.ts")) {
857
956
  const orig = res.setHeader.bind(res);
@@ -881,7 +980,8 @@ function marverPlugin(ctx) {
881
980
  };
882
981
  server.middlewares.use(apiMiddleware(root, {
883
982
  viewports: config.viewports,
884
- origin: listeningOrigin
983
+ origin: listeningOrigin,
984
+ bakeGen: () => bakeGen
885
985
  }));
886
986
  server.middlewares.use(routesMiddleware(server, clientDir));
887
987
  devServer = server;
@@ -919,6 +1019,21 @@ function marverPlugin(ctx) {
919
1019
  for (const mod of themeModules()) server.moduleGraph.invalidateModule(mod);
920
1020
  pushTheme();
921
1021
  };
1022
+ const localDir = join(root, "design", ".local"), boardsDirGen = join(root, "design", "boards"), manifestPath = join(root, "design", "manifest.json");
1023
+ const bumpGen = (f) => {
1024
+ if (f.startsWith(localDir) || f.startsWith(boardsDirGen) || f === manifestPath || f.includes("/node_modules/")) return;
1025
+ bakeGen++;
1026
+ prune();
1027
+ };
1028
+ const prune = () => setTimeout(async () => {
1029
+ try {
1030
+ (await import("./bake-jr38C_pX.mjs")).pruneBakes(root, bakeGen);
1031
+ } catch {}
1032
+ }, 500);
1033
+ prune();
1034
+ server.watcher.on("add", bumpGen);
1035
+ server.watcher.on("unlink", bumpGen);
1036
+ server.watcher.on("change", bumpGen);
922
1037
  server.watcher.on("add", (f) => {
923
1038
  if (inScope(f)) {
924
1039
  regen();
@@ -982,23 +1097,68 @@ function marverPlugin(ctx) {
982
1097
  if (!existsSync(reqPath)) return;
983
1098
  inFlight.add(file);
984
1099
  setTimeout(async () => {
1100
+ const base = file.replace(/\.request\.json$/, "");
1101
+ const resultPath = join(inbox, `${base}.result.json`);
1102
+ const claimed = join(inbox, `.${base}.${process.pid}.${Date.now().toString(36)}.claimed`);
985
1103
  try {
1104
+ renameSync(reqPath, claimed);
1105
+ } catch {
1106
+ inFlight.delete(file);
1107
+ return;
1108
+ }
1109
+ try {
1110
+ const write = (r) => {
1111
+ const tmp = join(inbox, `.${base}.${process.pid}.tmp`);
1112
+ try {
1113
+ writeFileSync(tmp, JSON.stringify(r, null, 2));
1114
+ renameSync(tmp, resultPath);
1115
+ } catch {
1116
+ try {
1117
+ rmSync(tmp, { force: true });
1118
+ } catch {}
1119
+ }
1120
+ };
986
1121
  let spec;
987
1122
  try {
988
- spec = JSON.parse(readFileSync(reqPath, "utf8"));
989
- } catch {
1123
+ spec = JSON.parse(readFileSync(claimed, "utf8"));
1124
+ if (!spec || typeof spec !== "object" || Array.isArray(spec)) throw new Error("expected an object");
1125
+ } catch (err) {
1126
+ write({
1127
+ ok: false,
1128
+ error: `malformed request - ${err.message}`
1129
+ });
1130
+ return;
1131
+ }
1132
+ const theme = spec.theme === void 0 ? "light" : spec.theme;
1133
+ if (typeof theme !== "string" || !/^[a-z0-9-]+$/i.test(theme)) {
1134
+ write({
1135
+ ok: false,
1136
+ error: "invalid theme"
1137
+ });
1138
+ return;
1139
+ }
1140
+ const scale = spec.scale === void 0 ? void 0 : spec.scale;
1141
+ if (scale !== void 0 && !(Number.isInteger(scale) && scale >= 1 && scale <= 4)) {
1142
+ write({
1143
+ ok: false,
1144
+ error: "invalid scale"
1145
+ });
1146
+ return;
1147
+ }
1148
+ const ways = [
1149
+ "frame",
1150
+ "frames",
1151
+ "scene",
1152
+ "all"
1153
+ ].filter((k) => spec[k] !== void 0);
1154
+ if (ways.length !== 1) {
1155
+ write({
1156
+ ok: false,
1157
+ error: "name the frames one way: frame (an id), frames (an array of ids), scene (a name) or all (true)"
1158
+ });
990
1159
  return;
991
1160
  }
992
- const base = file.replace(/\.request\.json$/, "");
993
- const resultPath = join(inbox, `${base}.result.json`);
994
- const frameId = typeof spec.frame === "string" ? spec.frame : "";
995
- const theme = typeof spec.theme === "string" ? spec.theme : "light";
996
1161
  const o = origin();
997
- const write = (r) => {
998
- try {
999
- writeFileSync(resultPath, JSON.stringify(r, null, 2));
1000
- } catch {}
1001
- };
1002
1162
  if (!o) {
1003
1163
  write({
1004
1164
  ok: false,
@@ -1006,18 +1166,55 @@ function marverPlugin(ctx) {
1006
1166
  });
1007
1167
  return;
1008
1168
  }
1009
- const { shootFrame } = await import("./shot-z-d-zMzf.mjs");
1010
- write(await shootFrame({
1011
- root,
1012
- viewports: config.viewports,
1013
- frameId,
1014
- theme,
1015
- origin: o
1016
- }));
1169
+ const { shootFrame, shootBatch, resolveFrames } = await import("./shot-iicees2e.mjs");
1170
+ if (ways[0] === "frame") {
1171
+ if (typeof spec.frame !== "string") {
1172
+ write({
1173
+ ok: false,
1174
+ error: "frame must be a frame id"
1175
+ });
1176
+ return;
1177
+ }
1178
+ write(await shootFrame({
1179
+ root,
1180
+ viewports: config.viewports,
1181
+ frameId: spec.frame,
1182
+ theme,
1183
+ origin: o,
1184
+ scale
1185
+ }));
1186
+ return;
1187
+ }
1188
+ const sel = resolveFrames(root, spec);
1189
+ if (!sel.ok) {
1190
+ write({
1191
+ ok: false,
1192
+ error: sel.error
1193
+ });
1194
+ return;
1195
+ }
1017
1196
  try {
1018
- rmSync(reqPath, { force: true });
1019
- } catch {}
1197
+ write({
1198
+ ok: true,
1199
+ results: await shootBatch({
1200
+ root,
1201
+ viewports: config.viewports,
1202
+ frames: sel.frames,
1203
+ theme,
1204
+ origin: o,
1205
+ scale
1206
+ })
1207
+ });
1208
+ } catch (err) {
1209
+ write({
1210
+ ok: false,
1211
+ error: err.message
1212
+ });
1213
+ }
1020
1214
  } finally {
1215
+ try {
1216
+ rmSync(claimed, { force: true });
1217
+ } catch {}
1021
1218
  inFlight.delete(file);
1022
1219
  }
1023
1220
  }, 60);
@@ -1,4 +1,4 @@
1
- import { capture, findChrome } from "./shot-z-d-zMzf.mjs";
1
+ import { capture, t as findChrome } from "./shot-iicees2e.mjs";
2
2
  import { constants, copyFileSync, existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { basename, dirname, join, sep } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
@@ -52,15 +52,23 @@ html,body{margin:0;background:#000}#root video{display:block}
52
52
  setTimeout(function(){ if (!done && v.readyState >= 2) show() }, 4000);
53
53
  })();
54
54
  <\/script></body></html>`;
55
- /** Render `<clip>.poster.png` for a clip under design/assets/. Idempotent: an existing
56
- * poster (authored or generated) is left alone. */
57
- async function ensurePoster(assetsDir, src) {
55
+ /** Single-flight by output path: N frames of one batch referencing the same clip ask at the
56
+ * same moment, and each would otherwise pass the "exists" check and render the same poster. */
57
+ const rendering = /* @__PURE__ */ new Map();
58
+ function ensurePoster(assetsDir, src) {
59
+ const out = join(assetsDir, posterNameFor(src));
60
+ const live = rendering.get(out);
61
+ if (live) return live;
62
+ const run = ensurePosterNow(assetsDir, src, out).finally(() => rendering.delete(out));
63
+ rendering.set(out, run);
64
+ return run;
65
+ }
66
+ async function ensurePosterNow(assetsDir, src, out) {
58
67
  if (!isLocalClip(src)) return {
59
68
  ok: false,
60
69
  error: `not a local clip: ${src}`
61
70
  };
62
71
  const clip = join(assetsDir, src);
63
- const out = join(assetsDir, posterNameFor(src));
64
72
  if (existsSync(out)) return {
65
73
  ok: true,
66
74
  path: out,
@@ -0,0 +1,216 @@
1
+ import { planShot, t as findChrome } from "./shot-iicees2e.mjs";
2
+ import { ASK_MAX, bakeBatch } from "./bake-jr38C_pX.mjs";
3
+ import { MIME } from "./serve-z5qtj_wJ.mjs";
4
+ import { copyFileSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import { extname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { createServer } from "node:http";
7
+ //#region src/server/publish-bakes.ts
8
+ /**
9
+ * Textures at publish time (spec 16, published canvases).
10
+ *
11
+ * A published canvas is a static site with no compiler, so its glass stayed live and a shared hi-fi
12
+ * board flashed on pan and zoom as before 0.18.0. Everything the compiler needs is known when the
13
+ * site is built: the published boards, every node's frame and size on them, and the themes a
14
+ * visitor can flip to. So the build serves design/.dist to itself on a loopback port, compiles every
15
+ * (frame, theme, size) against the PUBLISHED document - the one visitors get, not the dev one - with
16
+ * the same certification as the dev server (bake.ts), ships the certified textures under
17
+ * design/.dist/__mv/bakes/<build>/ and writes one static index the shell reads instead of asking a
18
+ * server. Anything the compiler refuses is simply absent from the index: that frame sleeps with the
19
+ * pause alone, glass live, as today. No Chrome on the build machine: the note is printed and the
20
+ * site ships without textures.
21
+ */
22
+ /** The shell's plain key for an ask (sleep.ts keyOf): frame|theme|w|h. */
23
+ const indexKey = (a) => `${a.frame}|${a.theme}|${Math.round(a.w)}|${Math.round(a.h)}`;
24
+ /** Every (frame, theme, size) a visitor can rest on: each published board's nodes, sized the way
25
+ * the shell sizes them (the node's own size, else the frame's default), each theme; the published
26
+ * `all-scenes` board shows every frame at its default size. Sizes are rounded first, then held to
27
+ * the compiler's limits: an oversize node is skipped, never clamped to a document of another size. */
28
+ function publishedAsks(boards, themes, frames, viewports, allScenes = false) {
29
+ const byId = new Map(frames.map((f) => [f.id, f]));
30
+ const seen = /* @__PURE__ */ new Map();
31
+ const add = (frame, w, h) => {
32
+ w = Math.round(w);
33
+ h = Math.round(h);
34
+ if (!(w >= 120) || !(h >= 80) || w > ASK_MAX.side || h > ASK_MAX.side || w * h > ASK_MAX.area) return;
35
+ for (const theme of themes.length ? themes : ["light"]) {
36
+ const ask = {
37
+ frame,
38
+ theme,
39
+ w,
40
+ h
41
+ };
42
+ seen.set(indexKey(ask), ask);
43
+ }
44
+ };
45
+ const size = (f, n) => {
46
+ const nw = n && typeof n.w === "number" && n.w > 0 ? n.w : void 0, nh = n && typeof n.h === "number" && n.h > 0 ? n.h : void 0;
47
+ const p = planShot(f, viewports, {});
48
+ if (p.fullHeight && !nh) return null;
49
+ return {
50
+ w: nw ?? p.width,
51
+ h: nh ?? p.initialHeight
52
+ };
53
+ };
54
+ for (const b of Object.values(boards)) for (const n of b?.nodes ?? []) {
55
+ const f = typeof n?.frame === "string" ? byId.get(n.frame) : void 0;
56
+ const s = f && size(f, n);
57
+ if (s) add(f.id, s.w, s.h);
58
+ }
59
+ if (allScenes) for (const f of frames) {
60
+ const s = size(f);
61
+ if (s) add(f.id, s.w, s.h);
62
+ }
63
+ return [...seen.values()];
64
+ }
65
+ /** The index: only answers that certified at least one texture, and only their certified targets
66
+ * (a refused target's selector ships nothing); everything else is absent. */
67
+ function publishedIndex(gen, answers) {
68
+ const out = {
69
+ gen,
70
+ answers: {}
71
+ };
72
+ for (const a of answers) {
73
+ if (!a.ok) continue;
74
+ const targets = a.targets.filter((t) => t.verified && t.texture);
75
+ if (targets.length) out.answers[indexKey(a)] = {
76
+ ok: true,
77
+ targets
78
+ };
79
+ }
80
+ return out;
81
+ }
82
+ /** Serve `dir` on a loopback port the way `marver serve` does (extensionless = index.html). */
83
+ function serveDir(dir) {
84
+ const real = realpathSync(dir);
85
+ const server = createServer((req, res) => {
86
+ let path = "";
87
+ try {
88
+ path = decodeURIComponent(new URL(req.url ?? "/", "http://x").pathname);
89
+ } catch {
90
+ res.statusCode = 400;
91
+ return res.end();
92
+ }
93
+ if (path.endsWith("/")) path += "index.html";
94
+ let file = resolve(dir, path.slice(1));
95
+ try {
96
+ const r = realpathSync(file);
97
+ if (relative(real, r).startsWith("..") || isAbsolute(relative(real, r))) throw 0;
98
+ file = r;
99
+ } catch {
100
+ file = join(dir, "index.html");
101
+ }
102
+ if (!extname(file)) file = join(dir, "index.html");
103
+ try {
104
+ const c = readFileSync(file);
105
+ res.setHeader("content-type", MIME[extname(file)] ?? "application/octet-stream");
106
+ res.end(c);
107
+ } catch {
108
+ res.statusCode = 404;
109
+ res.end();
110
+ }
111
+ });
112
+ return new Promise((ok) => server.listen(0, "127.0.0.1", () => {
113
+ ok({
114
+ origin: `http://127.0.0.1:${server.address().port}`,
115
+ close: () => server.close()
116
+ });
117
+ }));
118
+ }
119
+ /** Compile the published boards' frames against the built site and ship the textures with it.
120
+ * Returns null when there is no Chrome to compile with. */
121
+ async function bakePublished(opts) {
122
+ const { root, outDir, gen, boards, themes, frames, viewports, allScenes, urlFor, log } = opts;
123
+ if (!findChrome()) return null;
124
+ const t0 = Date.now();
125
+ const asks = publishedAsks(boards, themes, frames, viewports, allScenes).filter((a) => urlFor(a.frame, a.theme));
126
+ const stats = {
127
+ asked: asks.length,
128
+ asleep: 0,
129
+ live: 0,
130
+ plain: 0,
131
+ bytes: 0,
132
+ ms: 0
133
+ };
134
+ if (!asks.length) return stats;
135
+ const cache = join(root, "design", ".local", "bakes", String(gen));
136
+ const site = await serveDir(outDir);
137
+ let answers;
138
+ try {
139
+ answers = await bakeBatch({
140
+ root,
141
+ gen,
142
+ asks,
143
+ urlBase: "/__mv/bakes",
144
+ urlFor: (a) => site.origin + urlFor(a.frame, a.theme)
145
+ });
146
+ const index = publishedIndex(gen, answers);
147
+ const to = join(outDir, "__mv", "bakes");
148
+ rmSync(to, {
149
+ recursive: true,
150
+ force: true
151
+ });
152
+ const grammar = new RegExp(`^/__mv/bakes/${gen}/[0-9a-f]{16}/\\d+\\.png$`);
153
+ const realCache = (() => {
154
+ try {
155
+ return realpathSync(cache);
156
+ } catch {
157
+ return null;
158
+ }
159
+ })();
160
+ for (const [key, a] of Object.entries(index.answers)) {
161
+ const files = [];
162
+ if (!(realCache !== null && a.targets.every((t) => {
163
+ if (!grammar.test(t.texture)) return false;
164
+ const from = join(root, "design", ".local", "bakes", t.texture.replace(/^\/__mv\/bakes\//, ""));
165
+ try {
166
+ if (!lstatSync(from).isFile()) return false;
167
+ if (!realpathSync(from).startsWith(realCache + "/")) return false;
168
+ } catch {
169
+ return false;
170
+ }
171
+ files.push([from, join(outDir, t.texture.slice(1))]);
172
+ return true;
173
+ }))) {
174
+ delete index.answers[key];
175
+ continue;
176
+ }
177
+ for (const [from, dest] of files) {
178
+ mkdirSync(join(dest, ".."), { recursive: true });
179
+ copyFileSync(from, dest);
180
+ stats.bytes += statSync(dest).size;
181
+ }
182
+ }
183
+ mkdirSync(join(to, String(gen)), { recursive: true });
184
+ writeFileSync(join(to, String(gen), "index.json"), JSON.stringify(index));
185
+ for (const a of answers) {
186
+ if (!a.ok) {
187
+ stats.live++;
188
+ log?.(` bake: ${a.frame} ${a.theme} ${a.w}x${a.h} - stays live: ${a.error}`);
189
+ continue;
190
+ }
191
+ if (!(a.effects ?? a.targets.length)) {
192
+ stats.plain++;
193
+ continue;
194
+ }
195
+ if (!a.targets.length) {
196
+ stats.live++;
197
+ log?.(` bake: ${a.frame} ${a.theme} ${a.w}x${a.h} - ${a.effects} effects, all nested, stay live`);
198
+ continue;
199
+ }
200
+ const shipped = index.answers[indexKey(a)]?.targets.length ?? 0;
201
+ if (shipped) stats.asleep++;
202
+ else stats.live++;
203
+ log?.(` bake: ${a.frame} ${a.theme} ${a.w}x${a.h} - ${a.targets.length} effects, ${a.targets.length - shipped} stay live, ${a.ms} ms`);
204
+ }
205
+ } finally {
206
+ site.close();
207
+ rmSync(cache, {
208
+ recursive: true,
209
+ force: true
210
+ });
211
+ }
212
+ stats.ms = Date.now() - t0;
213
+ return stats;
214
+ }
215
+ //#endregion
216
+ export { bakePublished };
@@ -1,4 +1,4 @@
1
- import { n as NAME } from "./cli.mjs";
1
+ import { r as NAME } from "./cli.mjs";
2
2
  import { t as secureSuffix } from "./secure-cookie-_K1Hsx8H.mjs";
3
3
  import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
4
4
  import { extname, isAbsolute, join, relative, resolve } from "node:path";
@@ -147,7 +147,7 @@ async function serve(root, portFlag) {
147
147
  console.error(`[${NAME}] MARVER_ID_ISSUER is set but MARVER_PUBLIC_ORIGIN is not.\n Every assertion is bound to this canvas's exact origin, and it cannot be\n inferred from request headers - a proxy can make any request look local.\n Set it to the origin people actually reach this canvas on:\n MARVER_PUBLIC_ORIGIN=https://canvas.example.com\n MARVER_PUBLIC_ORIGIN=http://localhost:${portFlag ?? (Number(process.env.PORT) || 4199)} (development)`);
148
148
  process.exit(1);
149
149
  }
150
- const { marverIdHandler } = await import("./marver-id-gate-D6By7XHj.mjs");
150
+ const { marverIdHandler } = await import("./marver-id-gate-B_idGdHm.mjs");
151
151
  const { dataDir } = await import("./comments-DZyobpxG.mjs").then((n) => n.n);
152
152
  const canvasName = humanName(meta.name);
153
153
  const { ceilingsFromRights } = await import("./auth-DdUKeVKt.mjs").then((n) => n.R);
@@ -659,4 +659,4 @@ ${collabOn ? `
659
659
  }
660
660
  const esc = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
661
661
  //#endregion
662
- export { serve, poweredByUrl as t };
662
+ export { MIME, serve, poweredByUrl as t };
@@ -1,4 +1,4 @@
1
- import { n as NAME } from "./cli.mjs";
1
+ import { r as NAME } from "./cli.mjs";
2
2
  import { M as resolveAccess } from "./auth-DdUKeVKt.mjs";
3
3
  import { i as loadCollab } from "./sync-BZaCWqK-.mjs";
4
4
  //#region src/cli/share.ts