@marver-design/marver 0.3.1 → 0.5.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 +151 -0
- package/README.md +2 -0
- package/dist/auth-B36fMCM3.mjs +245 -0
- package/dist/{build-Ckmyci3O.mjs → build-BrCl9hJS.mjs} +56 -14
- package/dist/cli.mjs +15 -6
- package/dist/collab-CXy8gqoz.mjs +297 -0
- package/dist/comments-Ba8mU600.mjs +90 -0
- package/dist/comments-odHzYdO3.mjs +179 -0
- package/dist/dev-DdeU-Jst.mjs +162 -0
- package/dist/{init-DolP_Ld4.mjs → init-DsCUmlCW.mjs} +2 -1
- package/dist/{manifest-DW-T52MM.mjs → manifest-C8FODq2S.mjs} +26 -1
- package/dist/{plugin-Crp4CAma.mjs → plugin-BtSGAm2h.mjs} +169 -19
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/serve-D_KBK7Oy.mjs +431 -0
- package/dist/sync-CkBk-tUk.mjs +248 -0
- package/package.json +3 -1
- package/src/client/content/diagram.tsx +46 -2
- package/src/client/content/index.tsx +6 -2
- package/src/client/content/md.ts +29 -0
- package/src/client/content/palette.ts +6 -0
- package/src/client/frame-host/bridge.js +269 -3
- package/src/client/frame-host/serialize.ts +195 -0
- package/src/client/shell/App.tsx +131 -17
- package/src/client/shell/Comments.tsx +464 -0
- package/src/client/shell/Play.tsx +24 -0
- package/src/client/shell/canvas/Canvas.tsx +66 -29
- package/src/client/shell/canvas/FrameNode.tsx +130 -10
- package/src/client/shell/canvas/frame-registry.ts +18 -0
- package/src/client/shell/canvas/snapshots.ts +233 -0
- package/src/client/shell/comments-store.ts +180 -0
- package/src/client/shell/cursor-arrow-dark.svg +1 -0
- package/src/client/shell/cursor-arrow.svg +1 -0
- package/src/client/shell/hash.ts +14 -3
- package/src/client/shell/icons.tsx +6 -0
- package/src/client/shell/labels.ts +10 -0
- package/src/client/shell/perf.ts +92 -0
- package/src/client/shell/store.ts +101 -6
- package/src/client/shell/styles.css +196 -7
- package/src/client/stage/main.tsx +2 -0
- package/src/shared/events.ts +103 -0
- package/templates/AGENTS-embedded.md +13 -1
- package/templates/AGENTS-studio.md +13 -1
- package/templates/instructions/boards.md +6 -3
- package/templates/instructions/configure.md +28 -0
- package/templates/instructions/iterate.md +38 -0
- package/templates/instructions/publish.md +129 -0
- package/templates/instructions/reference/color.md +22 -1
- package/templates/instructions/shape.md +23 -16
- package/dist/dev-C2oTKuXe.mjs +0 -97
- package/dist/serve-OtA9Nlow.mjs +0 -207
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { n as NAME, r as PKG } from "./cli.mjs";
|
|
2
|
+
import { o as loadConfig, s as detectHost } from "./manifest-C8FODq2S.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-BtSGAm2h.mjs";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createLogger, createServer, searchForWorkspaceRoot } from "vite";
|
|
7
|
+
import react from "@vitejs/plugin-react";
|
|
8
|
+
import { createServer as createServer$1 } from "node:net";
|
|
9
|
+
//#region src/server/dev.ts
|
|
10
|
+
/** packageDir = the installed marver package root (dist/cli.js lives one level down). */
|
|
11
|
+
function packageDir() {
|
|
12
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
}
|
|
14
|
+
const portFree = (p) => new Promise((res) => {
|
|
15
|
+
const s = createServer$1();
|
|
16
|
+
s.once("error", () => res(false));
|
|
17
|
+
s.once("listening", () => s.close(() => res(true)));
|
|
18
|
+
s.listen(p, "127.0.0.1");
|
|
19
|
+
});
|
|
20
|
+
/** C1: a deterministic per-project port in [5200,5399] derived from the root path. Two projects
|
|
21
|
+
* never collide, and the same project always lands on the same port across restarts. */
|
|
22
|
+
const projectPort = (root) => {
|
|
23
|
+
let h = 0;
|
|
24
|
+
for (let i = 0; i < root.length; i++) h = Math.imul(h, 31) + root.charCodeAt(i) | 0;
|
|
25
|
+
return 5200 + Math.abs(h) % 200;
|
|
26
|
+
};
|
|
27
|
+
/** Pick the port to serve on: the desired one if free, else a DETERMINISTIC per-project fallback -
|
|
28
|
+
* never a silent next-free drift, which made a bookmarked tab silently serve a DIFFERENT project. */
|
|
29
|
+
async function pickPort(root, desired) {
|
|
30
|
+
if (await portFree(desired)) return {
|
|
31
|
+
port: desired,
|
|
32
|
+
fellBack: false
|
|
33
|
+
};
|
|
34
|
+
let p = projectPort(root);
|
|
35
|
+
for (let i = 0; i < 200; i++) {
|
|
36
|
+
if (p !== desired && await portFree(p)) return {
|
|
37
|
+
port: p,
|
|
38
|
+
fellBack: true
|
|
39
|
+
};
|
|
40
|
+
p = 5200 + (p - 5200 + 1) % 200;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
port: desired,
|
|
44
|
+
fellBack: true
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function dev(root, portFlag) {
|
|
48
|
+
const config = await loadConfig(root);
|
|
49
|
+
const host = detectHost(root);
|
|
50
|
+
const pkgDir = packageDir();
|
|
51
|
+
const clientDir = join(pkgDir, "src", "client");
|
|
52
|
+
const projectName = basename(root);
|
|
53
|
+
const desiredPort = portFlag ?? (config.port !== 5199 ? config.port : void 0) ?? projectPort(root);
|
|
54
|
+
const picked = await pickPort(root, desiredPort);
|
|
55
|
+
const plugins = [react()];
|
|
56
|
+
if (host.tailwind === 4) {
|
|
57
|
+
const tw = await tailwind4Plugin(root);
|
|
58
|
+
if (tw) plugins.push(...tw);
|
|
59
|
+
else console.warn(`[marver] tailwindcss v4 detected but @tailwindcss/vite could not be loaded - utility classes WILL be missing from frames. Fix: npm i -D @tailwindcss/vite (used only by the canvas, never by your app's build).`);
|
|
60
|
+
}
|
|
61
|
+
let css;
|
|
62
|
+
if (host.tailwind === 3) css = await tailwind3Css(root) ?? void 0;
|
|
63
|
+
plugins.push(marverPlugin({
|
|
64
|
+
root,
|
|
65
|
+
clientDir,
|
|
66
|
+
config,
|
|
67
|
+
detectedThemeCss: host.themeCss
|
|
68
|
+
}));
|
|
69
|
+
const logger = createLogger("info");
|
|
70
|
+
const noise = (msg) => /Sourcemap for .*node_modules.*points to/.test(msg);
|
|
71
|
+
const warnBase = logger.warn.bind(logger);
|
|
72
|
+
const warnOnceBase = logger.warnOnce.bind(logger);
|
|
73
|
+
logger.warn = (msg, opts) => {
|
|
74
|
+
if (!noise(msg)) warnBase(msg, opts);
|
|
75
|
+
};
|
|
76
|
+
logger.warnOnce = (msg, opts) => {
|
|
77
|
+
if (!noise(msg)) warnOnceBase(msg, opts);
|
|
78
|
+
};
|
|
79
|
+
const server = await createServer({
|
|
80
|
+
configFile: false,
|
|
81
|
+
root,
|
|
82
|
+
css,
|
|
83
|
+
customLogger: logger,
|
|
84
|
+
plugins,
|
|
85
|
+
server: {
|
|
86
|
+
port: picked.port,
|
|
87
|
+
strictPort: false,
|
|
88
|
+
fs: { allow: [.../* @__PURE__ */ new Set([
|
|
89
|
+
root,
|
|
90
|
+
pkgDir,
|
|
91
|
+
searchForWorkspaceRoot(root)
|
|
92
|
+
])] },
|
|
93
|
+
watch: { ignored: [
|
|
94
|
+
"**/design/manifest.json",
|
|
95
|
+
"**/design/boards/**",
|
|
96
|
+
"**/design/.local/**",
|
|
97
|
+
"**/design/.dist/**",
|
|
98
|
+
"**/design/comments/**",
|
|
99
|
+
"**/.next/**",
|
|
100
|
+
"**/.turbo/**",
|
|
101
|
+
"**/.vercel/**",
|
|
102
|
+
"**/.output/**",
|
|
103
|
+
"**/dist/**",
|
|
104
|
+
"**/build/**",
|
|
105
|
+
"**/out/**",
|
|
106
|
+
"**/coverage/**",
|
|
107
|
+
"**/.gstack/**",
|
|
108
|
+
"**/.git/**",
|
|
109
|
+
"**/.playwright-mcp/**"
|
|
110
|
+
] },
|
|
111
|
+
warmup: { clientFiles: [
|
|
112
|
+
join(clientDir, "frame-host", "main.tsx"),
|
|
113
|
+
join(clientDir, "frame-host", "bridge.js"),
|
|
114
|
+
join(clientDir, "content", "index.tsx"),
|
|
115
|
+
join(clientDir, "content", "diagram.tsx"),
|
|
116
|
+
join(clientDir, "content", "md.ts")
|
|
117
|
+
] }
|
|
118
|
+
},
|
|
119
|
+
resolve: {
|
|
120
|
+
dedupe: ["react", "react-dom"],
|
|
121
|
+
tsconfigPaths: true
|
|
122
|
+
},
|
|
123
|
+
optimizeDeps: {
|
|
124
|
+
exclude: [PKG],
|
|
125
|
+
include: [
|
|
126
|
+
"react",
|
|
127
|
+
"react-dom",
|
|
128
|
+
"react-dom/client",
|
|
129
|
+
"react/jsx-runtime",
|
|
130
|
+
"react/jsx-dev-runtime",
|
|
131
|
+
`${PKG} > marked`,
|
|
132
|
+
`${PKG} > mermaid`,
|
|
133
|
+
`${PKG} > html-to-image`
|
|
134
|
+
],
|
|
135
|
+
entries: [join(clientDir, "frame-host", "index.html"), "design/**/*.{tsx,jsx}"]
|
|
136
|
+
},
|
|
137
|
+
logLevel: "info"
|
|
138
|
+
});
|
|
139
|
+
await server.listen();
|
|
140
|
+
const addr = server.httpServer?.address();
|
|
141
|
+
const port = typeof addr === "object" && addr ? addr.port : picked.port;
|
|
142
|
+
if (picked.fellBack) console.log(`\n port ${desiredPort} is in use - serving "${projectName}" on ${port} instead`);
|
|
143
|
+
console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
|
|
144
|
+
const { loadCollab, syncOnce } = await import("./sync-CkBk-tUk.mjs").then((n) => n.a);
|
|
145
|
+
if (loadCollab(root)) console.log(` comments: syncing with the published canvas (design/.local/collab.json)\n`);
|
|
146
|
+
let syncing = false;
|
|
147
|
+
const tick = async () => {
|
|
148
|
+
if (syncing) return;
|
|
149
|
+
syncing = true;
|
|
150
|
+
try {
|
|
151
|
+
const collab = loadCollab(root);
|
|
152
|
+
if (collab) await syncOnce(root, collab).catch(() => {});
|
|
153
|
+
} finally {
|
|
154
|
+
syncing = false;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
tick();
|
|
158
|
+
setInterval(tick, 3e4).unref();
|
|
159
|
+
return server;
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
export { dev };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as NAME } from "./cli.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { a as DEFAULTS, c as readJson, i as writeManifest, r as scanFrames, s as detectHost } from "./manifest-C8FODq2S.mjs";
|
|
3
3
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, relative } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -133,6 +133,7 @@ function init(root, opts) {
|
|
|
133
133
|
refresh("tsconfig.json", [STANDALONE_TSCONFIG], tsconfigNow());
|
|
134
134
|
}
|
|
135
135
|
write(".gitignore", ".local/\n.dist/\n");
|
|
136
|
+
write(".gitattributes", "comments/*.jsonl merge=union\n");
|
|
136
137
|
write("scenes/_layout.tsx", readFileSync(join(templates, "root-layout.tsx"), "utf8"));
|
|
137
138
|
if (!existsSync(join(design, "boards"))) {
|
|
138
139
|
mkdirSync(join(design, "boards"), { recursive: true });
|
|
@@ -247,6 +247,31 @@ function toFrameId(designRelPath) {
|
|
|
247
247
|
const noExt = designRelPath.split(sep).join("/").replace(FRAME_EXT, "");
|
|
248
248
|
return noExt.startsWith("scenes/") ? noExt.slice(7) : noExt;
|
|
249
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* A7: which frames does a changed `design/**` file affect? Manifest + directory conventions,
|
|
252
|
+
* NOT a module-graph walk (deterministic, cheap, may over-reload but never misses a
|
|
253
|
+
* conventionally-affected frame). Returns:
|
|
254
|
+
* null -> uncontrolled: leave to default Vite HMR (src/** deps, theme.css, config, assets)
|
|
255
|
+
* [] -> controlled but affects no current frame (e.g. a layout in an empty dir)
|
|
256
|
+
* [ids] -> controlled: the shell drives a rev-stamped reload of these frames
|
|
257
|
+
*/
|
|
258
|
+
function affectedFrameIds(absFile, root, manifest) {
|
|
259
|
+
const design = join(root, "design");
|
|
260
|
+
if (absFile !== design && !absFile.startsWith(design + sep)) return null;
|
|
261
|
+
const rel = relative(design, absFile).split(sep).join("/");
|
|
262
|
+
const name = rel.split("/").pop() ?? rel;
|
|
263
|
+
const designRel = `design/${rel}`;
|
|
264
|
+
const tsxFrames = manifest.frames.filter((f) => f.kind === "tsx");
|
|
265
|
+
if (name === "theme.css") return null;
|
|
266
|
+
if (rel === "providers.tsx" || rel === "providers.jsx") return tsxFrames.map((f) => f.id);
|
|
267
|
+
if (/(^|\/)(_layout\.(tsx|jsx)|_fixtures\.(ts|tsx|js|jsx|json))$/.test(rel)) {
|
|
268
|
+
const prefix = `design/${rel.slice(0, rel.length - name.length)}`;
|
|
269
|
+
return tsxFrames.filter((f) => f.file.startsWith(prefix)).map((f) => f.id);
|
|
270
|
+
}
|
|
271
|
+
const direct = manifest.frames.find((f) => f.file === designRel);
|
|
272
|
+
if (direct) return [direct.id];
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
250
275
|
function walk(dir, out = []) {
|
|
251
276
|
if (!existsSync(dir)) return out;
|
|
252
277
|
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -399,4 +424,4 @@ function writeManifest(root, manifest) {
|
|
|
399
424
|
}
|
|
400
425
|
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
401
426
|
//#endregion
|
|
402
|
-
export {
|
|
427
|
+
export { DEFAULTS as a, readJson as c, writeManifest as i, hash as n, loadConfig as o, scanFrames as r, detectHost as s, affectedFrameIds as t };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { i as ROUTE, n as NAME, r as PKG } from "./cli.mjs";
|
|
2
|
-
import { n as
|
|
2
|
+
import { i as writeManifest, n as hash, r as scanFrames, t as affectedFrameIds } from "./manifest-C8FODq2S.mjs";
|
|
3
3
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
4
|
-
import { dirname, join, resolve, sep } from "node:path";
|
|
4
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
7
7
|
//#region src/server/api.ts
|
|
@@ -110,6 +110,10 @@ function apiMiddleware(root) {
|
|
|
110
110
|
} catch {
|
|
111
111
|
return json(res, 400, { error: "malformed JSON" });
|
|
112
112
|
}
|
|
113
|
+
if (body.mustExist && !existsSync(p)) return json(res, 409, {
|
|
114
|
+
error: "board no longer exists on disk",
|
|
115
|
+
gone: true
|
|
116
|
+
});
|
|
113
117
|
const current = existsSync(p) ? readFileSync(p, "utf8") : "";
|
|
114
118
|
if (current && body.baseHash !== hash(current)) {
|
|
115
119
|
let disk = null;
|
|
@@ -128,12 +132,126 @@ function apiMiddleware(root) {
|
|
|
128
132
|
return json(res, 200, { sha256: hash(next2) });
|
|
129
133
|
}
|
|
130
134
|
}
|
|
135
|
+
if (path === "me" && req.method === "GET") return json(res, 200, {
|
|
136
|
+
user: localProfile(root),
|
|
137
|
+
role: "owner",
|
|
138
|
+
local: true
|
|
139
|
+
});
|
|
140
|
+
if (path === "profile" && req.method === "POST") {
|
|
141
|
+
const raw = await readBody(req);
|
|
142
|
+
if (raw == null) return json(res, 400, { error: "body too large" });
|
|
143
|
+
let b;
|
|
144
|
+
try {
|
|
145
|
+
b = JSON.parse(raw);
|
|
146
|
+
} catch {
|
|
147
|
+
return json(res, 400, { error: "malformed JSON" });
|
|
148
|
+
}
|
|
149
|
+
const dir = join(root, "design", ".local");
|
|
150
|
+
mkdirSync(dir, { recursive: true });
|
|
151
|
+
const prof = {
|
|
152
|
+
...localProfile(root),
|
|
153
|
+
...typeof b.name === "string" && b.name.trim() ? { name: b.name.trim() } : {},
|
|
154
|
+
...typeof b.email === "string" ? { email: b.email.trim() } : {},
|
|
155
|
+
...typeof b.avatar === "string" ? { avatar: b.avatar || void 0 } : {}
|
|
156
|
+
};
|
|
157
|
+
atomicWrite(join(dir, "profile.json"), JSON.stringify(prof, null, 2) + "\n");
|
|
158
|
+
return json(res, 200, { user: prof });
|
|
159
|
+
}
|
|
160
|
+
if (path === "boards.rights" && req.method === "GET") {
|
|
161
|
+
const { listBoards } = await import("./comments-Ba8mU600.mjs").then((n) => n.n);
|
|
162
|
+
return json(res, 200, {
|
|
163
|
+
local: true,
|
|
164
|
+
stored: listBoards(join(root, "design", "comments"))
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const cm = /^comments\/([a-z0-9][a-z0-9-]*)$/.exec(path);
|
|
168
|
+
if (cm) {
|
|
169
|
+
const { appendEvents, readLog } = await import("./comments-Ba8mU600.mjs").then((n) => n.n);
|
|
170
|
+
const dir = join(root, "design", "comments");
|
|
171
|
+
if (req.method === "GET") return json(res, 200, { events: readLog(dir, cm[1]) });
|
|
172
|
+
if (req.method === "POST") {
|
|
173
|
+
const raw = await readBody(req);
|
|
174
|
+
if (raw == null) return json(res, 400, { error: "body too large" });
|
|
175
|
+
let b;
|
|
176
|
+
try {
|
|
177
|
+
b = JSON.parse(raw);
|
|
178
|
+
} catch {
|
|
179
|
+
return json(res, 400, { error: "malformed JSON" });
|
|
180
|
+
}
|
|
181
|
+
const incoming = Array.isArray(b.events) ? b.events : [];
|
|
182
|
+
const me = localProfile(root);
|
|
183
|
+
const stamped = incoming.map((ev) => ({
|
|
184
|
+
...ev,
|
|
185
|
+
board: ev.board ?? cm[1],
|
|
186
|
+
author: ev.author ?? ([
|
|
187
|
+
"create",
|
|
188
|
+
"reply",
|
|
189
|
+
"react",
|
|
190
|
+
"edit"
|
|
191
|
+
].includes(ev.type) ? me : void 0)
|
|
192
|
+
}));
|
|
193
|
+
const fresh = appendEvents(dir, cm[1], stamped);
|
|
194
|
+
backgroundPush(root);
|
|
195
|
+
return json(res, 200, { accepted: fresh.length });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (path === "sync" && req.method === "POST") {
|
|
199
|
+
const { loadCollab, syncOnce } = await import("./sync-CkBk-tUk.mjs").then((n) => n.a);
|
|
200
|
+
const collab = loadCollab(root);
|
|
201
|
+
if (!collab) return json(res, 200, { connected: false });
|
|
202
|
+
try {
|
|
203
|
+
return json(res, 200, {
|
|
204
|
+
connected: true,
|
|
205
|
+
boards: await syncOnce(root, collab)
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
return json(res, 502, {
|
|
209
|
+
connected: true,
|
|
210
|
+
error: err.message
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
131
214
|
json(res, 404, { error: "unknown endpoint" });
|
|
132
215
|
} catch (err) {
|
|
133
216
|
json(res, 500, { error: err.message });
|
|
134
217
|
}
|
|
135
218
|
};
|
|
136
219
|
}
|
|
220
|
+
function localProfile(root) {
|
|
221
|
+
try {
|
|
222
|
+
const c = JSON.parse(readFileSync(join(root, "design", ".local", "collab.json"), "utf8"));
|
|
223
|
+
if (typeof c?.email === "string" && c.email) return {
|
|
224
|
+
email: c.email,
|
|
225
|
+
name: c.name ?? "Designer"
|
|
226
|
+
};
|
|
227
|
+
} catch {}
|
|
228
|
+
try {
|
|
229
|
+
const p = JSON.parse(readFileSync(join(root, "design", ".local", "profile.json"), "utf8"));
|
|
230
|
+
if (typeof p?.name === "string") return {
|
|
231
|
+
email: p.email ?? "",
|
|
232
|
+
name: p.name,
|
|
233
|
+
avatar: p.avatar
|
|
234
|
+
};
|
|
235
|
+
} catch {}
|
|
236
|
+
return {
|
|
237
|
+
email: "",
|
|
238
|
+
name: "Designer"
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
let pushTimer = null;
|
|
242
|
+
/** Debounced fire-and-forget push after a local write; failures are silent - the
|
|
243
|
+
* 30s sync loop is the reliability layer, this is just latency. */
|
|
244
|
+
function backgroundPush(root) {
|
|
245
|
+
if (pushTimer) return;
|
|
246
|
+
pushTimer = setTimeout(async () => {
|
|
247
|
+
pushTimer = null;
|
|
248
|
+
try {
|
|
249
|
+
const { loadCollab, syncOnce } = await import("./sync-CkBk-tUk.mjs").then((n) => n.a);
|
|
250
|
+
const collab = loadCollab(root);
|
|
251
|
+
if (collab) await syncOnce(root, collab);
|
|
252
|
+
} catch {}
|
|
253
|
+
}, 1500);
|
|
254
|
+
}
|
|
137
255
|
//#endregion
|
|
138
256
|
//#region src/server/routes.ts
|
|
139
257
|
/**
|
|
@@ -259,6 +377,20 @@ const VIRTUAL_CONFIG = "virtual:sh-config";
|
|
|
259
377
|
const VIRTUAL_DATA = "virtual:sh-data";
|
|
260
378
|
function marverPlugin(ctx) {
|
|
261
379
|
const { root, clientDir, config } = ctx;
|
|
380
|
+
let manifest = scanFrames(root);
|
|
381
|
+
let devServer = null;
|
|
382
|
+
const bootId = String(process.hrtime.bigint());
|
|
383
|
+
let invRev = 0;
|
|
384
|
+
const pendingInv = /* @__PURE__ */ new Set();
|
|
385
|
+
const flushInv = debounce(() => {
|
|
386
|
+
if (!devServer || !pendingInv.size) return;
|
|
387
|
+
const frameIds = [...pendingInv];
|
|
388
|
+
pendingInv.clear();
|
|
389
|
+
devServer.ws.send("sh:frame-invalidated", {
|
|
390
|
+
frameIds,
|
|
391
|
+
revision: `${bootId}:${++invRev}`
|
|
392
|
+
});
|
|
393
|
+
}, 120);
|
|
262
394
|
/** Theme resolution: design/theme.css wrapper > configured > detected > empty (spec §5.4). */
|
|
263
395
|
const themeFile = () => {
|
|
264
396
|
const wrapper = join(root, "design", "theme.css");
|
|
@@ -269,6 +401,7 @@ function marverPlugin(ctx) {
|
|
|
269
401
|
};
|
|
270
402
|
return {
|
|
271
403
|
name: "marver",
|
|
404
|
+
enforce: "pre",
|
|
272
405
|
resolveId(id) {
|
|
273
406
|
if (id === VIRTUAL_THEME) return themeFile() ?? "\0virtual:sh-theme.css";
|
|
274
407
|
if (id === VIRTUAL_CONFIG) return "\0virtual:sh-config";
|
|
@@ -293,7 +426,8 @@ function marverPlugin(ctx) {
|
|
|
293
426
|
themes: config.themes,
|
|
294
427
|
zoomSpeed: config.zoomSpeed,
|
|
295
428
|
noTheme: themeFile() == null,
|
|
296
|
-
setup: setupPending
|
|
429
|
+
setup: setupPending,
|
|
430
|
+
projectName: basename(root)
|
|
297
431
|
})}`;
|
|
298
432
|
}
|
|
299
433
|
if (id === "\0virtual:sh-data") return "export default null";
|
|
@@ -308,19 +442,27 @@ function marverPlugin(ctx) {
|
|
|
308
442
|
const bridge = "/@fs/" + join(clientDir, "frame-host", "bridge.js").split("\\").join("/") + "?html=1";
|
|
309
443
|
return {
|
|
310
444
|
html,
|
|
311
|
-
tags: [
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}, {
|
|
317
|
-
tag: "script",
|
|
318
|
-
attrs: {
|
|
319
|
-
type: "module",
|
|
320
|
-
src: bridge
|
|
445
|
+
tags: [
|
|
446
|
+
{
|
|
447
|
+
tag: "script",
|
|
448
|
+
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)};})();`,
|
|
449
|
+
injectTo: "head-prepend"
|
|
321
450
|
},
|
|
322
|
-
|
|
323
|
-
|
|
451
|
+
{
|
|
452
|
+
tag: "script",
|
|
453
|
+
attrs: { type: "module" },
|
|
454
|
+
children: `import '${VIRTUAL_THEME}'`,
|
|
455
|
+
injectTo: "head-prepend"
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
tag: "script",
|
|
459
|
+
attrs: {
|
|
460
|
+
type: "module",
|
|
461
|
+
src: bridge
|
|
462
|
+
},
|
|
463
|
+
injectTo: "head-prepend"
|
|
464
|
+
}
|
|
465
|
+
]
|
|
324
466
|
};
|
|
325
467
|
}
|
|
326
468
|
},
|
|
@@ -330,9 +472,16 @@ function marverPlugin(ctx) {
|
|
|
330
472
|
* Edits to loaded frames keep normal HMR (modules present + file exists → pass through).
|
|
331
473
|
*/
|
|
332
474
|
handleHotUpdate(hctx) {
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
|
|
475
|
+
const affected = affectedFrameIds(hctx.file, root, manifest);
|
|
476
|
+
if (affected === null) {
|
|
477
|
+
if ([join(root, "design", "scenes"), join(root, "design", "components")].some((w) => hctx.file.startsWith(w)) && !existsSync(hctx.file)) return [];
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (affected.length) {
|
|
481
|
+
for (const id of affected) pendingInv.add(id);
|
|
482
|
+
flushInv();
|
|
483
|
+
}
|
|
484
|
+
return [];
|
|
336
485
|
},
|
|
337
486
|
configureServer(server) {
|
|
338
487
|
server.middlewares.use((req, res, next) => {
|
|
@@ -359,8 +508,9 @@ function marverPlugin(ctx) {
|
|
|
359
508
|
});
|
|
360
509
|
server.middlewares.use(apiMiddleware(root));
|
|
361
510
|
server.middlewares.use(routesMiddleware(server, clientDir));
|
|
511
|
+
devServer = server;
|
|
362
512
|
const regen = debounce(() => {
|
|
363
|
-
|
|
513
|
+
manifest = scanFrames(root);
|
|
364
514
|
if (writeManifest(root, manifest)) server.ws.send("sh:manifest", manifest);
|
|
365
515
|
}, 150);
|
|
366
516
|
const watched = [join(root, "design", "scenes"), join(root, "design", "components")];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|