@marver-design/marver 0.17.0 → 0.19.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 (48) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +3 -1
  3. package/dist/bake-kaf5kGZ7.mjs +747 -0
  4. package/dist/{build-D_g53Bp2.mjs → build-7ed5H2vT.mjs} +215 -13
  5. package/dist/cli.mjs +19 -11
  6. package/dist/{comments-oYcZ3cE-.mjs → comments-ClVgfQib.mjs} +1 -1
  7. package/dist/{daemon-Bbh_jmui.mjs → daemon-DbHvLQUL.mjs} +1 -1
  8. package/dist/{dev-LnIISva5.mjs → dev-D3mP2x27.mjs} +205 -10
  9. package/dist/{init-B7YhcN2o.mjs → init-BQYCS3EU.mjs} +2 -2
  10. package/dist/{manifest-CaslQIAO.mjs → manifest-B01PSyDc.mjs} +34 -7
  11. package/dist/{marver-id-gate-D6By7XHj.mjs → marver-id-gate-B_idGdHm.mjs} +1 -1
  12. package/dist/{plugin-D2msH1cj.mjs → plugin-DI-7NAnx.mjs} +108 -29
  13. package/dist/{poster-BEjUcQP3.mjs → poster-DNh6N27C.mjs} +1 -1
  14. package/dist/publish-bakes-Dp-ZFk3d.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-BzQ0PXKH.mjs → shot-BFEuYbaz.mjs} +1 -1
  18. package/dist/{shot-DlmTO8AF.mjs → shot-DMDvDbeP.mjs} +14 -7
  19. package/dist/{work-lzC-lPY0.mjs → work-0YopuMt9.mjs} +1 -1
  20. package/docs/publish.md +27 -5
  21. package/docs/sticky-notes.md +43 -0
  22. package/package.json +2 -1
  23. package/src/client/content/diagram.tsx +27 -1
  24. package/src/client/content/index.tsx +2 -2
  25. package/src/client/content/md.ts +48 -0
  26. package/src/client/frame-host/bridge.js +4 -9
  27. package/src/client/frame-host/main.tsx +20 -6
  28. package/src/client/shell/App.tsx +14 -63
  29. package/src/client/shell/Comments.tsx +77 -14
  30. package/src/client/shell/Play.tsx +6 -3
  31. package/src/client/shell/canvas/Canvas.tsx +12 -3
  32. package/src/client/shell/canvas/FrameNode.tsx +99 -89
  33. package/src/client/shell/canvas/Sticky.tsx +284 -0
  34. package/src/client/shell/canvas/admission.ts +70 -0
  35. package/src/client/shell/canvas/sleep.ts +194 -0
  36. package/src/client/shell/goto.ts +72 -0
  37. package/src/client/shell/notes.ts +151 -0
  38. package/src/client/shell/store.ts +36 -18
  39. package/src/client/shell/styles.css +92 -25
  40. package/src/client/shell/tidy.ts +20 -2
  41. package/src/shared/sleep-rule.ts +41 -0
  42. package/templates/AGENTS-embedded.md +15 -0
  43. package/templates/AGENTS-studio.md +15 -0
  44. package/templates/instructions/craft.md +9 -0
  45. package/templates/instructions/publish.md +62 -9
  46. package/templates/instructions/shape.md +62 -0
  47. package/src/client/frame-host/serialize.ts +0 -195
  48. package/src/client/shell/canvas/snapshots.ts +0 -233
@@ -1,12 +1,205 @@
1
- import { n as NAME, r as PKG } from "./cli.mjs";
2
- import { l as detectHost, s as loadConfig } from "./manifest-CaslQIAO.mjs";
3
- import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-D2msH1cj.mjs";
4
- import { realpathSync } from "node:fs";
1
+ import { i as PKG, r as NAME } from "./cli.mjs";
2
+ import { c as loadConfig, u as detectHost } from "./manifest-B01PSyDc.mjs";
3
+ import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-DI-7NAnx.mjs";
4
+ import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
5
5
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- import { createLogger, createServer, searchForWorkspaceRoot } from "vite";
7
+ import { createLogger, createServer, parseAst, searchForWorkspaceRoot } from "vite";
8
8
  import react from "@vitejs/plugin-react";
9
9
  import { createServer as createServer$1 } from "node:net";
10
+ //#region src/server/icons.ts
11
+ /**
12
+ * Icon barrels, unrolled.
13
+ *
14
+ * `import { Truck } from '@phosphor-icons/react'` makes Vite hand the frame the WHOLE prebundled
15
+ * package - 3000 icons, 6.5 MB of JavaScript - and every frame on a board is its own realm, so every
16
+ * frame parses and evaluates all of it: ~400 ms, the whole boot of a lo-fi frame (measured,
17
+ * research/hifi/frameres.ts: the scene import is 530 of a 550 ms boot; a board of 128 frames spends
18
+ * 50 s on it). The packages ship one module per icon and their barrel says which: this reads the
19
+ * INSTALLED barrel once (every alias, whatever the version), rewrites the import declarations of
20
+ * design sources to the per-icon modules, one line for one line, and names those modules to the
21
+ * dependency optimizer at server start so nothing is discovered - and reloaded for - on first use.
22
+ * A name the barrel does not map, a default or namespace import, a re-export: left as it is.
23
+ */
24
+ const LIBS = [{
25
+ barrel: "@phosphor-icons/react",
26
+ spec: (rel) => {
27
+ const m = rel.match(/^\.\/csr\/([\w-]+)\.es\.js$/);
28
+ return m ? `@phosphor-icons/react/dist/csr/${m[1]}` : null;
29
+ }
30
+ }, {
31
+ barrel: "lucide-react",
32
+ spec: (rel) => {
33
+ const m = rel.match(/^\.\/icons\/([\w-]+\.m?js)$/);
34
+ return m ? `lucide-react/dist/esm/icons/${m[1]}` : null;
35
+ }
36
+ }];
37
+ /** The map read from a barrel's source: `import { A as x } from './f'` + `export { x as Name }`
38
+ * (phosphor) and `export { default as Name } from './f'` (lucide). */
39
+ function parseBarrel(source, lib) {
40
+ const map = /* @__PURE__ */ new Map();
41
+ const locals = /* @__PURE__ */ new Map();
42
+ for (const m of source.matchAll(/import\s*\{([^}]*)\}\s*from\s*["']([^"']+)["']/g)) {
43
+ const spec = lib.spec(m[2]);
44
+ if (!spec) continue;
45
+ for (const s of m[1].split(",")) {
46
+ const [name, local = name] = s.trim().split(/\s+as\s+/);
47
+ if (name) locals.set(local, {
48
+ spec,
49
+ name
50
+ });
51
+ }
52
+ }
53
+ for (const m of source.matchAll(/export\s*\{([^}]*)\}(?:\s*from\s*["']([^"']+)["'])?/g)) {
54
+ const spec = m[2] ? lib.spec(m[2]) : null;
55
+ if (m[2] && !spec) continue;
56
+ for (const s of m[1].split(",")) {
57
+ const [name, exported = name] = s.trim().split(/\s+as\s+/);
58
+ if (!name) continue;
59
+ if (spec) map.set(exported, {
60
+ spec,
61
+ name
62
+ });
63
+ else {
64
+ const l = locals.get(name);
65
+ if (l) map.set(exported, l);
66
+ }
67
+ }
68
+ }
69
+ return map;
70
+ }
71
+ const maps = /* @__PURE__ */ new Map();
72
+ /** The maps for every icon package installed under `root` (read once per server). */
73
+ function iconMaps(root) {
74
+ let m = maps.get(root);
75
+ if (m) return m;
76
+ m = /* @__PURE__ */ new Map();
77
+ for (const lib of LIBS) {
78
+ const dir = join(root, "node_modules", lib.barrel);
79
+ try {
80
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
81
+ const dot = pkg.exports?.["."];
82
+ const entry = (typeof dot === "object" ? dot.import : typeof dot === "string" ? dot : void 0) ?? pkg.module;
83
+ if (!entry) continue;
84
+ const map = parseBarrel(readFileSync(join(dir, entry), "utf8"), lib);
85
+ if (map.size) m.set(lib.barrel, map);
86
+ } catch {}
87
+ }
88
+ maps.set(root, m);
89
+ return m;
90
+ }
91
+ /** The code with every mapped icon import unrolled and the per-icon specifiers it now imports;
92
+ * null when nothing changed. Real import declarations only (a template literal is content). */
93
+ function unrollIcons(code, maps) {
94
+ if (![...maps.keys()].some((b) => code.includes(b))) return null;
95
+ let ast;
96
+ try {
97
+ ast = parseAst(code);
98
+ } catch {
99
+ return null;
100
+ }
101
+ const edits = [];
102
+ const modules = [];
103
+ for (const node of ast.body) {
104
+ if (node.type !== "ImportDeclaration" || !node.source) continue;
105
+ const map = maps.get(node.source.value);
106
+ if (!map || !node.specifiers?.length) continue;
107
+ const lines = [], keep = [];
108
+ let icons = 0;
109
+ for (const s of node.specifiers) {
110
+ const imported = s.type === "ImportSpecifier" ? s.imported?.name ?? s.imported?.value : void 0;
111
+ const hit = imported ? map.get(imported) : void 0;
112
+ if (!hit) {
113
+ if (s.type !== "ImportSpecifier") {
114
+ lines.length = 0;
115
+ icons = 0;
116
+ break;
117
+ }
118
+ keep.push(imported === s.local.name ? imported : `${imported} as ${s.local.name}`);
119
+ continue;
120
+ }
121
+ icons++;
122
+ modules.push(hit.spec);
123
+ lines.push(hit.name === "default" ? `import ${s.local.name} from '${hit.spec}';` : `import { ${hit.name === s.local.name ? hit.name : `${hit.name} as ${s.local.name}`} } from '${hit.spec}';`);
124
+ }
125
+ if (!icons) continue;
126
+ if (keep.length) lines.push(`import { ${keep.join(", ")} } from '${node.source.value}';`);
127
+ const original = code.slice(node.start, node.end);
128
+ edits.push({
129
+ start: node.start,
130
+ end: node.end,
131
+ text: lines.join(" ") + "\n".repeat((original.match(/\n/g) ?? []).length)
132
+ });
133
+ }
134
+ if (!edits.length) return null;
135
+ let out = "", at = 0;
136
+ for (const e of edits) {
137
+ out += code.slice(at, e.start) + e.text;
138
+ at = e.end;
139
+ }
140
+ return {
141
+ code: out + code.slice(at),
142
+ modules
143
+ };
144
+ }
145
+ /** Every per-icon module the design sources import (for the optimizer, at server start). Raw
146
+ * TypeScript is scanned by a plain pattern; only names the barrel maps count. */
147
+ function iconModules(root) {
148
+ const m = iconMaps(root);
149
+ if (!m.size) return [];
150
+ const found = /* @__PURE__ */ new Set();
151
+ const walk = (dir) => {
152
+ let names;
153
+ try {
154
+ names = readdirSync(dir);
155
+ } catch {
156
+ return;
157
+ }
158
+ for (const n of names) {
159
+ if (n === "node_modules" || n.startsWith(".")) continue;
160
+ const p = join(dir, n);
161
+ let st;
162
+ try {
163
+ st = statSync(p);
164
+ } catch {
165
+ continue;
166
+ }
167
+ if (st.isDirectory()) walk(p);
168
+ else if (/\.[jt]sx?$/.test(n)) {
169
+ const src = readFileSync(p, "utf8").replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, (c) => " ".repeat(c.length));
170
+ for (const im of src.matchAll(/import\s*\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g)) {
171
+ const map = m.get(im[2]);
172
+ if (!map) continue;
173
+ for (const s of im[1].split(",")) {
174
+ const name = s.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0];
175
+ const hit = map.get(name);
176
+ if (hit) found.add(hit.spec);
177
+ }
178
+ }
179
+ }
180
+ }
181
+ };
182
+ walk(join(root, "design"));
183
+ return [...found].sort();
184
+ }
185
+ /** The Vite plugin: design sources only, after the TS/JSX transform (a normal plugin runs after
186
+ * the core ones), before import analysis resolves the specifiers. The rewrite keeps every line
187
+ * where it was, so the file's map still lands (no map of its own). */
188
+ function iconsPlugin(root) {
189
+ const design = join(root, "design") + "/";
190
+ return {
191
+ name: "marver:icons",
192
+ transform(code, id) {
193
+ if (!id.startsWith(design) || !/\.[jt]sx?(\?|$)/.test(id)) return;
194
+ const r = unrollIcons(code, iconMaps(root));
195
+ return r ? {
196
+ code: r.code,
197
+ map: null
198
+ } : void 0;
199
+ }
200
+ };
201
+ }
202
+ //#endregion
10
203
  //#region src/server/dev.ts
11
204
  /** packageDir = the installed marver package root (dist/cli.js lives one level down). */
12
205
  function packageDir() {
@@ -174,7 +367,7 @@ async function dev(root, portFlag) {
174
367
  clientDir,
175
368
  config,
176
369
  detectedThemeCss: host.themeCss
177
- }));
370
+ }), iconsPlugin(root));
178
371
  const logger = createLogger("info");
179
372
  const noise = (msg) => /Sourcemap for .*node_modules.*points to/.test(msg);
180
373
  const warnBase = logger.warn.bind(logger);
@@ -244,7 +437,9 @@ async function dev(root, portFlag) {
244
437
  "react/jsx-dev-runtime",
245
438
  `${PKG} > marked`,
246
439
  `${PKG} > mermaid`,
247
- `${PKG} > html-to-image`
440
+ `${PKG} > roughjs`,
441
+ `${PKG} > html-to-image`,
442
+ ...iconModules(root)
248
443
  ],
249
444
  entries: [join(clientDir, "frame-host", "index.html"), "design/**/*.{tsx,jsx}"]
250
445
  },
@@ -257,12 +452,12 @@ async function dev(root, portFlag) {
257
452
  console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
258
453
  setTimeout(async () => {
259
454
  try {
260
- await (await import("./shot-DlmTO8AF.mjs")).sweepGhosts((m) => console.log(` ${m}`));
455
+ await (await import("./shot-DMDvDbeP.mjs")).sweepGhosts((m) => console.log(` ${m}`));
261
456
  } catch {}
262
457
  }, 0);
263
458
  try {
264
459
  const { staleManagedInstructions } = await import("./managed-HwHVNI3h.mjs");
265
- const { installedVersion } = await import("./plugin-D2msH1cj.mjs").then((n) => n.i);
460
+ const { installedVersion } = await import("./plugin-DI-7NAnx.mjs").then((n) => n.i);
266
461
  const stale = staleManagedInstructions(root);
267
462
  if (stale.length) {
268
463
  const shown = stale.slice(0, 4).join(", ") + (stale.length > 4 ? `, +${stale.length - 4} more` : "");
@@ -300,7 +495,7 @@ async function dev(root, portFlag) {
300
495
  });
301
496
  }
302
497
  if (config.jam) {
303
- const { startJam } = await import("./daemon-Bbh_jmui.mjs");
498
+ const { startJam } = await import("./daemon-DbHvLQUL.mjs");
304
499
  const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
305
500
  if (jam) {
306
501
  const close = server.close.bind(server);
@@ -1,5 +1,5 @@
1
- import { n as NAME } from "./cli.mjs";
2
- import { a as writeManifest, c as detectAgent, l as detectHost, n as scanFrames, o as DEFAULTS, u as readJson } from "./manifest-CaslQIAO.mjs";
1
+ import { r as NAME } from "./cli.mjs";
2
+ import { d as readJson, l as detectAgent, o as writeManifest, r as scanFrames, s as DEFAULTS, u as detectHost } from "./manifest-B01PSyDc.mjs";
3
3
  import { MANAGED_PREFIX, enumerateInstructionTemplates, hashBody, managedFile, pkgDir } from "./managed-HwHVNI3h.mjs";
4
4
  import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, join, relative } from "node:path";
@@ -1,4 +1,4 @@
1
- import { r as PKG, t as CONTENT_WIDTH } from "./cli.mjs";
1
+ import { i as PKG, n as CONTENT_WIDTH } from "./cli.mjs";
2
2
  import { a as listBoardFiles, d as flatten, f as isBoardName, g as hash, m as readTitle, n as checkBoardsDir, p as readDescription, r as checkRealDirs, s as readRegistry, t as boardFields, u as buildTree } from "./boards-BmxcT3Lc.mjs";
3
3
  import { accessSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import { delimiter, isAbsolute, join, relative, sep } from "node:path";
@@ -375,6 +375,23 @@ function validViewports(v) {
375
375
  //#region src/server/manifest.ts
376
376
  const FRAME_EXT = /\.(tsx|jsx|html)$/;
377
377
  const RESERVED_SCENES = /* @__PURE__ */ new Set(["components", "screens"]);
378
+ /** Sticky notes (spec 18): the longest note the manifest carries - a note is an aside, not a spec. */
379
+ const NOTE_MAX = 2e4;
380
+ /** The note file beside a frame file: `cart.tsx` -> `cart.note.md`; a scene's is `_note.md`. */
381
+ const noteFileFor = (frameFile) => frameFile.replace(FRAME_EXT, ".note.md");
382
+ /** Is this path a note file - the dev watcher's "regenerate on change" predicate. */
383
+ const isNoteFile = (file) => /(^|[\\/])(_note|[^\\/]+\.note)\.md$/.test(file);
384
+ /** A note's text: BOM stripped, trimmed, capped at NOTE_MAX. Undefined when the file is missing,
385
+ * empty, or not a regular file (a symlink is never followed - the `_brief.md` rule). */
386
+ function readNote(abs) {
387
+ try {
388
+ if (!lstatSync(abs).isFile()) return void 0;
389
+ const text = readFileSync(abs, "utf8").replace(/^/, "").trim();
390
+ return text ? text.slice(0, NOTE_MAX) : void 0;
391
+ } catch {
392
+ return;
393
+ }
394
+ }
378
395
  /** Extract `export const meta = {...}` with literal string values only. Anything else is silently omitted. */
379
396
  function extractMeta(src) {
380
397
  const m = /export\s+const\s+meta\s*=\s*\{([\s\S]*?)\}/.exec(src);
@@ -526,6 +543,10 @@ function sceneBrief(root, scene) {
526
543
  ...description ? { description } : {}
527
544
  };
528
545
  }
546
+ /** A scene's sticky note: `design/scenes/<scene>/_note.md` (spec 18). */
547
+ function sceneNote(root, scene) {
548
+ return scene ? readNote(join(root, "design", "scenes", scene, "_note.md")) : void 0;
549
+ }
529
550
  /** Write a scene's title into its brief's front matter - only that line changes; the body,
530
551
  * every other field and the file's own line endings are kept. No brief yet = a front-matter-only
531
552
  * brief. An empty title removes the line, and the file when nothing else is in it. The write
@@ -649,6 +670,8 @@ function scanFrames(root, project) {
649
670
  kind,
650
671
  scene
651
672
  };
673
+ const note = readNote(noteFileFor(abs));
674
+ if (note) entry.note = note;
652
675
  if (kind === "tsx") {
653
676
  const src = readFileSync(abs, "utf8");
654
677
  const meta = extractMeta(src);
@@ -684,11 +707,15 @@ function scanFrames(root, project) {
684
707
  inferVariantGroups(frames);
685
708
  const sceneCounts = /* @__PURE__ */ new Map();
686
709
  for (const f of frames) sceneCounts.set(f.scene, (sceneCounts.get(f.scene) ?? 0) + 1);
687
- const scenes = [...sceneCounts.entries()].map(([name, n]) => ({
688
- name,
689
- frames: n,
690
- ...sceneBrief(root, name)
691
- })).sort((a, b) => a.name.localeCompare(b.name));
710
+ const scenes = [...sceneCounts.entries()].map(([name, n]) => {
711
+ const note = sceneNote(root, name);
712
+ return {
713
+ name,
714
+ frames: n,
715
+ ...sceneBrief(root, name),
716
+ ...note ? { note } : {}
717
+ };
718
+ }).sort((a, b) => a.name.localeCompare(b.name));
692
719
  return {
693
720
  ...project && (project.name || project.description) ? { project: {
694
721
  ...project.name ? { name: project.name } : {},
@@ -779,4 +806,4 @@ function writeManifest(root, manifest) {
779
806
  return true;
780
807
  }
781
808
  //#endregion
782
- export { writeManifest as a, detectAgent as c, toFrameId as i, detectHost as l, scanFrames as n, DEFAULTS as o, setSceneTitle as r, loadConfig as s, affectedFrameIds as t, readJson as u };
809
+ export { toFrameId as a, loadConfig as c, readJson as d, setSceneTitle as i, detectAgent as l, isNoteFile as n, writeManifest as o, scanFrames as r, DEFAULTS as s, affectedFrameIds as t, detectHost as u };
@@ -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 { c as loadConfig, i as setSceneTitle, n as isNoteFile, o as writeManifest, r as scanFrames, t as affectedFrameIds } from "./manifest-B01PSyDc.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-DlmTO8AF.mjs");
425
+ const { shootFrame } = await import("./shot-DMDvDbeP.mjs");
426
426
  const r = await shootFrame({
427
427
  root,
428
428
  viewports: opts.viewports ?? {},
@@ -472,7 +472,7 @@ function apiMiddleware(root, opts = {}) {
472
472
  if (typeof theme !== "string" || !/^[a-z0-9-]+$/i.test(theme)) return json(res, 400, { error: "invalid theme" });
473
473
  const scale = body.scale === void 0 ? void 0 : body.scale;
474
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-DlmTO8AF.mjs");
475
+ const { resolveFrames, shootBatch } = await import("./shot-DMDvDbeP.mjs");
476
476
  const sel = resolveFrames(root, body);
477
477
  if (!sel.ok) return json(res, sel.status, { error: sel.error });
478
478
  const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
@@ -489,11 +489,63 @@ function apiMiddleware(root, opts = {}) {
489
489
  return json(res, 503, { error: err.message });
490
490
  }
491
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-kaf5kGZ7.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
+ }
492
544
  if (path === "poster" && req.method === "GET") {
493
545
  if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
494
546
  const src = url.searchParams.get("src") ?? "";
495
- const { ensurePoster, isLocalClip } = await import("./poster-BEjUcQP3.mjs");
496
- const { isLocalAssetRef } = await import("./build-D_g53Bp2.mjs");
547
+ const { ensurePoster, isLocalClip } = await import("./poster-DNh6N27C.mjs");
548
+ const { isLocalAssetRef } = await import("./build-7ed5H2vT.mjs");
497
549
  if (!isLocalAssetRef(src) || !isLocalClip(src)) return json(res, 400, { error: "src must be a clip under design/assets/" });
498
550
  const r = await ensurePoster(join(root, "design", "assets"), src);
499
551
  if (!r.ok) return json(res, r.error.includes("does not exist") ? 404 : 503, { error: r.error });
@@ -681,6 +733,24 @@ function routesMiddleware(server, clientDir) {
681
733
  } catch {
682
734
  return next();
683
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
+ }
684
754
  } else if (path === `/__mv/bridge.js`) {
685
755
  res.setHeader("content-type", "text/javascript");
686
756
  return res.end(readFileSync(join(clientDir, "frame-host", "bridge.js"), "utf8"));
@@ -846,27 +916,19 @@ function marverPlugin(ctx) {
846
916
  const bridge = "/@fs/" + join(clientDir, "frame-host", "bridge.js").split("\\").join("/") + "?html=1";
847
917
  return {
848
918
  html,
849
- tags: [
850
- {
851
- tag: "script",
852
- 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)};})();`,
853
- 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
854
929
  },
855
- {
856
- tag: "script",
857
- attrs: { type: "module" },
858
- children: `import '${VIRTUAL_THEME}'`,
859
- injectTo: "head-prepend"
860
- },
861
- {
862
- tag: "script",
863
- attrs: {
864
- type: "module",
865
- src: bridge
866
- },
867
- injectTo: "head-prepend"
868
- }
869
- ]
930
+ injectTo: "head-prepend"
931
+ }]
870
932
  };
871
933
  }
872
934
  },
@@ -888,6 +950,7 @@ function marverPlugin(ctx) {
888
950
  return [];
889
951
  },
890
952
  configureServer(server) {
953
+ let bakeGen = Date.now();
891
954
  server.middlewares.use((req, res, next) => {
892
955
  if (req.url && req.url.includes("/frame-host/registry.ts")) {
893
956
  const orig = res.setHeader.bind(res);
@@ -917,7 +980,8 @@ function marverPlugin(ctx) {
917
980
  };
918
981
  server.middlewares.use(apiMiddleware(root, {
919
982
  viewports: config.viewports,
920
- origin: listeningOrigin
983
+ origin: listeningOrigin,
984
+ bakeGen: () => bakeGen
921
985
  }));
922
986
  server.middlewares.use(routesMiddleware(server, clientDir));
923
987
  devServer = server;
@@ -955,6 +1019,21 @@ function marverPlugin(ctx) {
955
1019
  for (const mod of themeModules()) server.moduleGraph.invalidateModule(mod);
956
1020
  pushTheme();
957
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-kaf5kGZ7.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);
958
1037
  server.watcher.on("add", (f) => {
959
1038
  if (inScope(f)) {
960
1039
  regen();
@@ -967,7 +1046,7 @@ function marverPlugin(ctx) {
967
1046
  rescanTheme();
968
1047
  }
969
1048
  });
970
- server.watcher.on("change", (f) => inScope(f) && (/\.(tsx|jsx)$/.test(f) || f.endsWith("_brief.md")) && regen());
1049
+ server.watcher.on("change", (f) => inScope(f) && (/\.(tsx|jsx)$/.test(f) || f.endsWith("_brief.md") || isNoteFile(f)) && regen());
971
1050
  const configFile = join(root, "design", "config.ts");
972
1051
  server.watcher.on("change", (f) => {
973
1052
  if (f !== configFile) return;
@@ -1087,7 +1166,7 @@ function marverPlugin(ctx) {
1087
1166
  });
1088
1167
  return;
1089
1168
  }
1090
- const { shootFrame, shootBatch, resolveFrames } = await import("./shot-DlmTO8AF.mjs");
1169
+ const { shootFrame, shootBatch, resolveFrames } = await import("./shot-DMDvDbeP.mjs");
1091
1170
  if (ways[0] === "frame") {
1092
1171
  if (typeof spec.frame !== "string") {
1093
1172
  write({
@@ -1,4 +1,4 @@
1
- import { capture, t as findChrome } from "./shot-DlmTO8AF.mjs";
1
+ import { capture, t as findChrome } from "./shot-DMDvDbeP.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";