@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.
- package/CHANGELOG.md +93 -0
- package/README.md +3 -2
- package/dist/bake-jr38C_pX.mjs +747 -0
- package/dist/{build-B4yPgFNF.mjs → build-ER_7T6Cw.mjs} +179 -10
- package/dist/cli.mjs +21 -13
- package/dist/{comments-oYcZ3cE-.mjs → comments-ClVgfQib.mjs} +1 -1
- package/dist/{daemon-Bbh_jmui.mjs → daemon-CKhg0zuT.mjs} +1 -1
- package/dist/{dev-DH2W7Ffw.mjs → dev-BvLbY98O.mjs} +208 -9
- package/dist/{init-B7YhcN2o.mjs → init-BWbHqVng.mjs} +2 -2
- package/dist/{manifest-CaslQIAO.mjs → manifest-DAnEL8_a.mjs} +1 -1
- package/dist/{marver-id-gate-D6By7XHj.mjs → marver-id-gate-B_idGdHm.mjs} +1 -1
- package/dist/{plugin-BeBGu3gH.mjs → plugin-Cai5C1WV.mjs} +244 -47
- package/dist/{poster-CoyobbGW.mjs → poster-CIuz_PwH.mjs} +13 -5
- package/dist/publish-bakes-D0LhbQQ3.mjs +216 -0
- package/dist/{serve-Bcwfpvhl.mjs → serve-z5qtj_wJ.mjs} +3 -3
- package/dist/{share-Gqo_Ygqw.mjs → share--bdSc4G5.mjs} +1 -1
- package/dist/shot-BFEuYbaz.mjs +86 -0
- package/dist/shot-iicees2e.mjs +933 -0
- package/dist/{work-lzC-lPY0.mjs → work-0YopuMt9.mjs} +1 -1
- package/docs/live-jam.md +20 -8
- package/docs/publish.md +27 -5
- package/package.json +1 -1
- package/src/client/frame-host/bridge.js +4 -9
- package/src/client/frame-host/main.tsx +20 -6
- package/src/client/shell/App.tsx +7 -0
- package/src/client/shell/Comments.tsx +2 -2
- package/src/client/shell/canvas/Canvas.tsx +2 -2
- package/src/client/shell/canvas/FrameNode.tsx +82 -89
- package/src/client/shell/canvas/admission.ts +70 -0
- package/src/client/shell/canvas/sleep.ts +194 -0
- package/src/client/shell/store.ts +14 -0
- package/src/client/shell/styles.css +9 -25
- package/src/shared/sleep-rule.ts +41 -0
- package/templates/AGENTS-embedded.md +4 -1
- package/templates/AGENTS-studio.md +4 -1
- package/templates/instructions/craft.md +9 -0
- package/templates/instructions/jam.md +15 -3
- package/templates/instructions/publish.md +62 -9
- package/templates/instructions/shape.md +2 -1
- package/dist/shot-By1AItpD.mjs +0 -30
- package/dist/shot-z-d-zMzf.mjs +0 -528
- package/src/client/frame-host/serialize.ts +0 -195
- 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-
|
|
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 {
|
|
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-
|
|
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-
|
|
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-
|
|
460
|
-
const { isLocalAssetRef } = await import("./build-
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
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(
|
|
989
|
-
|
|
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-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
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-
|
|
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
|
-
/**
|
|
56
|
-
*
|
|
57
|
-
|
|
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 {
|
|
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-
|
|
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 };
|