@marver-design/marver 0.2.1 → 0.2.3
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/README.md +1 -0
- package/dist/{build-BYZDMiIS.mjs → build-Bqd6OEsQ.mjs} +2 -2
- package/dist/cli.mjs +3 -3
- package/dist/{dev-Dt9D4O7Z.mjs → dev-9-80L5i5.mjs} +2 -2
- package/dist/init-ClhCgn4v.mjs +358 -0
- package/dist/{manifest-CHmKAAtG.mjs → manifest-mYlO_1Pj.mjs} +78 -2
- package/dist/{plugin-lVQoABEx.mjs → plugin-YVpBNTB3.mjs} +103 -9
- package/package.json +1 -1
- package/src/client/shell/App.tsx +123 -19
- package/src/client/shell/Play.tsx +36 -0
- package/src/client/shell/canvas/Canvas.tsx +54 -7
- package/src/client/shell/canvas/FrameNode.tsx +14 -1
- package/src/client/shell/icons.tsx +2 -0
- package/src/client/shell/store.ts +119 -15
- package/src/client/shell/styles.css +107 -2
- package/src/client/shell/tidy.ts +347 -17
- package/src/client/stage/main.tsx +11 -3
- package/templates/AGENTS-embedded.md +38 -33
- package/templates/AGENTS-studio.md +38 -33
- package/templates/design-tsconfig.json +7 -2
- package/templates/instructions/boards.md +84 -0
- package/templates/instructions/brand.md +60 -0
- package/templates/instructions/components.md +52 -0
- package/templates/instructions/configure.md +44 -0
- package/templates/instructions/craft.md +90 -0
- package/templates/instructions/discover.md +58 -0
- package/templates/instructions/reference/color.md +53 -0
- package/templates/instructions/reference/concepts.md +68 -0
- package/templates/instructions/reference/copy.md +57 -0
- package/templates/instructions/reference/critique.md +53 -0
- package/templates/instructions/reference/delight.md +35 -0
- package/templates/instructions/reference/layout.md +51 -0
- package/templates/instructions/reference/motion.md +66 -0
- package/templates/instructions/reference/operate.md +38 -0
- package/templates/instructions/reference/slop.md +76 -0
- package/templates/instructions/reference/states.md +48 -0
- package/templates/instructions/reference/tune.md +61 -0
- package/templates/instructions/reference/typography.md +45 -0
- package/templates/instructions/review.md +51 -0
- package/templates/instructions/wireframe.md +49 -0
- package/dist/init-CHUZTYG7.mjs +0 -224
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Then, to your agent:
|
|
|
18
18
|
- **Boards**: one canvas on screen at a time. Agents write `design/boards/<name>.json` (a frame list is enough); switch boards at the top of the sidebar. `all-scenes` is auto-managed.
|
|
19
19
|
- **Devices view**: the Devices menu (or hotkeys `1`-`5`) sizes every frame to mobile / tablet / laptop / monitor / tv to sweep your breakpoints; `0` restores your own layout exactly. Widths live in `design/config.ts`.
|
|
20
20
|
- `data-goto="scene/frame"` on any element links frames into a walkable prototype.
|
|
21
|
+
- **Upgrade**: `npm i -D @marver-design/marver@latest && npx marver init`. The canvas tells you when a new version is out (one anonymous registry check per day, cached in `design/.local/`; `MARVER_NO_UPDATE_CHECK=1` disables). Re-running init refreshes the managed files (AGENTS.md, `design/instructions/`) - your edits to them are detected and preserved; when both you and a release changed a file, the fresh version is staged at `design/.local/latest/` for you (or your agent) to merge. Everything else in `design/` is yours and never touched.
|
|
21
22
|
- Uninstall: delete `design/`, remove the dependency. (If `init` patched your tsconfig `exclude`, revert that one line.)
|
|
22
23
|
|
|
23
24
|
**Next.js**: supported with one caveat - frames render in Vite, outside Next. `next/font` CSS variables are undefined inside frames (give font tokens a fallback chain), `next/image`/`next/link` should be plain `img`/`data-goto` in frames, and Server Components cannot run there. `init` writes the specifics into `design/AGENTS.md` when it detects Next.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as ROUTE, t as NAME } from "./cli.mjs";
|
|
2
|
-
import { a as loadConfig, n as scanFrames, o as detectHost } from "./manifest-
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { a as loadConfig, n as scanFrames, o as detectHost } from "./manifest-mYlO_1Pj.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-YVpBNTB3.mjs";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
package/dist/cli.mjs
CHANGED
|
@@ -32,14 +32,14 @@ function version() {
|
|
|
32
32
|
}
|
|
33
33
|
const cli = cac(NAME);
|
|
34
34
|
cli.command("init", "Scaffold design/ in this repo").option("--mode <mode>", "studio | embedded", { default: "studio" }).option("--no-demo", "Skip the demo scene (the demo ships unless this flag is passed)").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
|
|
35
|
-
const { init } = await import("./init-
|
|
35
|
+
const { init } = await import("./init-ClhCgn4v.mjs");
|
|
36
36
|
init(resolve(opts.root), {
|
|
37
37
|
mode: opts.mode === "embedded" ? "embedded" : "studio",
|
|
38
38
|
demo: opts.demo !== false
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
41
|
cli.command("dev", "Start the canvas").option("--root <dir>", "Host repo root", { default: "." }).option("--port <port>", "Port (default 5199)").action(async (opts) => {
|
|
42
|
-
const { dev } = await import("./dev-
|
|
42
|
+
const { dev } = await import("./dev-9-80L5i5.mjs");
|
|
43
43
|
let port;
|
|
44
44
|
if (opts.port !== void 0) {
|
|
45
45
|
const n = Number(opts.port);
|
|
@@ -49,7 +49,7 @@ cli.command("dev", "Start the canvas").option("--root <dir>", "Host repo root",
|
|
|
49
49
|
await dev(resolve(opts.root), port);
|
|
50
50
|
});
|
|
51
51
|
cli.command("build", "Static export → design/.dist").option("--boards <names>", "Publish only these boards (comma-separated); the frame filter is applied at build time").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
|
|
52
|
-
const { buildSite } = await import("./build-
|
|
52
|
+
const { buildSite } = await import("./build-Bqd6OEsQ.mjs");
|
|
53
53
|
try {
|
|
54
54
|
const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
|
|
55
55
|
await buildSite(resolve(opts.root), boards);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as PKG, t as NAME } from "./cli.mjs";
|
|
2
|
-
import { a as loadConfig, o as detectHost } from "./manifest-
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { a as loadConfig, o as detectHost } from "./manifest-mYlO_1Pj.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-YVpBNTB3.mjs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { createLogger, createServer, searchForWorkspaceRoot } from "vite";
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { t as NAME } from "./cli.mjs";
|
|
2
|
+
import { i as DEFAULTS, n as scanFrames, o as detectHost, r as writeManifest, s as readJson } from "./manifest-mYlO_1Pj.mjs";
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, relative } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
//#region src/cli/init.ts
|
|
8
|
+
/** Package root = the nearest ancestor holding templates/ (one hop from dist/, two
|
|
9
|
+
* from src/cli/ - the walk serves both, and tests run init from source). */
|
|
10
|
+
const pkgDir = () => {
|
|
11
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
for (let i = 0; i < 4; i++) {
|
|
13
|
+
if (existsSync(join(dir, "templates"))) return dir;
|
|
14
|
+
dir = dirname(dir);
|
|
15
|
+
}
|
|
16
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
+
};
|
|
18
|
+
/** Idempotent scaffolder: never overwrites existing files; every host-repo touch prints a diff. */
|
|
19
|
+
function init(root, opts) {
|
|
20
|
+
const host = detectHost(root);
|
|
21
|
+
const design = join(root, "design");
|
|
22
|
+
const templates = join(pkgDir(), "templates");
|
|
23
|
+
const created = [];
|
|
24
|
+
const fileHas = (rel, needle) => {
|
|
25
|
+
try {
|
|
26
|
+
return readFileSync(join(design, rel), "utf8").includes(needle);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const marverShaped = fileHas("config.ts", "marver") || fileHas("AGENTS.md", "agent contract");
|
|
32
|
+
if (existsSync(design) && !marverShaped && readdirSync(design).some((f) => !f.startsWith("."))) {
|
|
33
|
+
console.error(`
|
|
34
|
+
[${NAME}] design/ already exists in this repo and does not look like a ${NAME} workspace.
|
|
35
|
+
Refusing to merge into it - your files and ${NAME}'s would interleave, and
|
|
36
|
+
"uninstall = delete design/" would stop being safe.
|
|
37
|
+
|
|
38
|
+
Move or rename the existing design/ folder, then re-run \`npx ${NAME} init\`.
|
|
39
|
+
(If you need marver to live in a differently-named folder, say so at
|
|
40
|
+
github.com/TNEP4/marver - a --dir flag is planned.)`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const write = (rel, content) => {
|
|
44
|
+
const file = join(design, rel);
|
|
45
|
+
if (existsSync(file)) return;
|
|
46
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
47
|
+
writeFileSync(file, content);
|
|
48
|
+
created.push(`design/${rel}`);
|
|
49
|
+
};
|
|
50
|
+
const writeManaged = (rel, body) => {
|
|
51
|
+
const file = join(design, rel);
|
|
52
|
+
const next = managedFile(body);
|
|
53
|
+
const latest = join(design, ".local", "latest", rel);
|
|
54
|
+
if (!existsSync(file)) return write(rel, next);
|
|
55
|
+
const current = readFileSync(file, "utf8");
|
|
56
|
+
if (current === next) {
|
|
57
|
+
rmSync(latest, { force: true });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (current.startsWith(MANAGED_PREFIX)) {
|
|
61
|
+
const recorded = current.slice(MANAGED_PREFIX.length).split(" ")[0];
|
|
62
|
+
const nl = current.indexOf("\n");
|
|
63
|
+
const currentBody = nl >= 0 ? current.slice(nl + 1) : "";
|
|
64
|
+
if (nl >= 0 && hashBody(currentBody) === recorded) {
|
|
65
|
+
writeFileSync(file, next);
|
|
66
|
+
rmSync(latest, { force: true });
|
|
67
|
+
created.push(`design/${rel} (updated)`);
|
|
68
|
+
} else if (recorded !== hashBody(body)) {
|
|
69
|
+
mkdirSync(dirname(latest), { recursive: true });
|
|
70
|
+
writeFileSync(latest, body);
|
|
71
|
+
if (nl >= 0) {
|
|
72
|
+
const tmp = file + ".tmp";
|
|
73
|
+
writeFileSync(tmp, managedFile(body).split("\n")[0] + "\n" + currentBody);
|
|
74
|
+
renameSync(tmp, file);
|
|
75
|
+
}
|
|
76
|
+
console.warn(` ~ design/${rel}: you customized it and a newer version exists - your edits are untouched. Merge what you want from design/.local/latest/${rel}`);
|
|
77
|
+
}
|
|
78
|
+
} else if (current.startsWith(LEGACY_PREFIX)) {
|
|
79
|
+
writeFileSync(file, next);
|
|
80
|
+
rmSync(latest, { force: true });
|
|
81
|
+
created.push(`design/${rel} (updated)`);
|
|
82
|
+
} else if (current !== body) {
|
|
83
|
+
rmSync(latest, { force: true });
|
|
84
|
+
console.warn(` note: design/${rel} exists without a marver marker - left untouched. If you did not author it, delete it and re-run init to restore the managed version.`);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
write("config.ts", configTemplate(opts.mode));
|
|
88
|
+
if (host.themeCss) {
|
|
89
|
+
const relCss = relative(design, join(root, host.themeCss)).split("\\").join("/");
|
|
90
|
+
write("theme.css", themeWrapper(relCss, host.tailwind === 4));
|
|
91
|
+
} else console.warn(`[${NAME}] no theme CSS detected - create design/theme.css importing your app's stylesheet when you have one (or set \`theme\` in design/config.ts).`);
|
|
92
|
+
write("providers.tsx", providersTemplate(host.router, host.toaster, host.routerPkg));
|
|
93
|
+
writeManaged("AGENTS.md", readFileSync(join(templates, `AGENTS-${opts.mode}.md`), "utf8").replaceAll("{{UI_GUIDANCE}}", uiGuidance(host, noApp(host))).replace(/\{\{NEXT_NOTES\}\}\n?/, host.router === "next" ? NEXT_NOTES : ""));
|
|
94
|
+
const agentsNow = readFileSync(join(design, "AGENTS.md"), "utf8");
|
|
95
|
+
if (!agentsNow.startsWith(MANAGED_PREFIX) && !agentsNow.startsWith(LEGACY_PREFIX) && agentsNow.includes("# Design canvas - agent contract") && !agentsNow.includes("## The method (binding)")) console.warn(` note: design/AGENTS.md predates managed regeneration - if you never edited it, delete it and re-run init to get the current contract (incl. the design/instructions routing).`);
|
|
96
|
+
const instrRoot = join(templates, "instructions");
|
|
97
|
+
for (const e of readdirSync(instrRoot, { withFileTypes: true })) if (e.isDirectory()) {
|
|
98
|
+
for (const f of readdirSync(join(instrRoot, e.name))) if (f.endsWith(".md")) writeManaged(`instructions/${e.name}/${f}`, readFileSync(join(instrRoot, e.name, f), "utf8"));
|
|
99
|
+
} else if (e.name.endsWith(".md")) writeManaged(`instructions/${e.name}`, readFileSync(join(instrRoot, e.name), "utf8"));
|
|
100
|
+
const setupPath = join(design, "instructions", "setup.md");
|
|
101
|
+
const ourSetup = () => {
|
|
102
|
+
try {
|
|
103
|
+
const s = readFileSync(setupPath, "utf8");
|
|
104
|
+
return s.startsWith("# Setup required") && s.includes("marver init");
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
if (noApp(host)) {
|
|
110
|
+
if (!existsSync(setupPath)) write("instructions/setup.md", SETUP_MD);
|
|
111
|
+
} else if (existsSync(setupPath) && ourSetup()) {
|
|
112
|
+
rmSync(setupPath);
|
|
113
|
+
console.log(` - design/instructions/setup.md removed (app detected - setup complete)`);
|
|
114
|
+
}
|
|
115
|
+
write("tsconfig.json", existsSync(join(root, "tsconfig.json")) ? readFileSync(join(templates, "design-tsconfig.json"), "utf8").replace("{{PATHS}}", designPaths(root)) : STANDALONE_TSCONFIG);
|
|
116
|
+
write(".gitignore", ".local/\n.dist/\n");
|
|
117
|
+
write("scenes/_layout.tsx", readFileSync(join(templates, "root-layout.tsx"), "utf8"));
|
|
118
|
+
if (!existsSync(join(design, "boards"))) {
|
|
119
|
+
mkdirSync(join(design, "boards"), { recursive: true });
|
|
120
|
+
writeFileSync(join(design, "boards", ".gitkeep"), "");
|
|
121
|
+
created.push("design/boards/");
|
|
122
|
+
}
|
|
123
|
+
if (opts.demo && !existsSync(join(design, "scenes", "demo"))) {
|
|
124
|
+
cpSync(join(templates, "demo"), join(design, "scenes", "demo"), { recursive: true });
|
|
125
|
+
created.push("design/scenes/demo/ (3 frames)");
|
|
126
|
+
}
|
|
127
|
+
if (host.tsconfigSweepsDesign) patchTsconfigExclude(root);
|
|
128
|
+
writeManifest(root, scanFrames(root));
|
|
129
|
+
console.log(`\n${NAME} initialized (${opts.mode} mode). Created:`);
|
|
130
|
+
for (const f of created) console.log(` + ${f}`);
|
|
131
|
+
if (host.router === "next") console.log(`\n note: Next.js support is partial - frames render outside Next, so next/font, next/image and Server Components do not exist inside them (details in design/AGENTS.md).`);
|
|
132
|
+
if (noApp(host)) console.warn(`
|
|
133
|
+
┌─ NO APP DETECTED ─────────────────────────────────────────────────────┐
|
|
134
|
+
│ No framework, no theme CSS, no component library. ${NAME} builds │
|
|
135
|
+
│ frames from YOUR components - with none, designs get thrown away. │
|
|
136
|
+
│ │
|
|
137
|
+
│ Setup instructions: design/instructions/setup.md. Set up the app, │
|
|
138
|
+
│ re-run init, and that file removes itself. AGENTS.md carries a STOP │
|
|
139
|
+
│ so your agent does not design against components that do not exist. │
|
|
140
|
+
└───────────────────────────────────────────────────────────────────────┘`);
|
|
141
|
+
console.log(`\n commit design/ - only .local/ is ignored`);
|
|
142
|
+
console.log(` uninstall = delete design/, remove the ${NAME} dependency${host.tsconfigSweepsDesign ? ", revert the \"design\" line in tsconfig exclude" : ""}`);
|
|
143
|
+
if (!noApp(host) && !existsSync(join(design, "DESIGN.md"))) console.log(`\n note: design/DESIGN.md (the brand doc) does not exist yet - have your agent create it from the app's tokens (instructions/brand.md, Path A) to reach the idle state.`);
|
|
144
|
+
console.log(`\n next: npx ${NAME} dev (canvas on http://localhost:${DEFAULTS.port} by default)\n`);
|
|
145
|
+
if (!noApp(host)) console.log(` then, to your agent: "Read design/AGENTS.md. Build an onboarding scene - welcome, form, done - mobile-first, using our components."\n`);
|
|
146
|
+
}
|
|
147
|
+
const MANAGED_PREFIX = "<!-- marver:managed ";
|
|
148
|
+
const LEGACY_PREFIX = "<!-- generated by marver init";
|
|
149
|
+
const hashBody = (s) => createHash("sha256").update(s).digest("hex");
|
|
150
|
+
/** The marker carries a hash of the generated body: edits are DETECTED, not assumed.
|
|
151
|
+
* Edit freely - init preserves edits and stages upstream updates for merging.
|
|
152
|
+
* Deleting the marker line detaches the file from updates entirely. */
|
|
153
|
+
const managedFile = (body) => `${MANAGED_PREFIX}${hashBody(body)} - edit freely: init preserves your edits and stages upstream updates at design/.local/latest/ for you to merge. Delete this line to detach this file from updates entirely. -->\n${body}`;
|
|
154
|
+
/** The host's path aliases, re-rooted one level down for design/tsconfig.json.
|
|
155
|
+
* "./src/*" becomes "../src/*" so `@/` imports resolve from inside design/. */
|
|
156
|
+
function designPaths(root) {
|
|
157
|
+
const hostPaths = readJson(join(root, "tsconfig.json"))?.compilerOptions?.paths;
|
|
158
|
+
if (!hostPaths || typeof hostPaths !== "object") return "";
|
|
159
|
+
const rerooted = {};
|
|
160
|
+
for (const [alias, targets] of Object.entries(hostPaths)) {
|
|
161
|
+
if (!Array.isArray(targets)) continue;
|
|
162
|
+
rerooted[alias] = targets.map((t) => typeof t === "string" ? t.startsWith("./") ? `../${t.slice(2)}` : `../${t}` : t);
|
|
163
|
+
}
|
|
164
|
+
if (!Object.keys(rerooted).length) return "";
|
|
165
|
+
return `,\n // the host's aliases, re-rooted (inherited paths resolve against the WRONG dir)\n "paths": ${JSON.stringify(rerooted)}`;
|
|
166
|
+
}
|
|
167
|
+
/** No framework, no theme, no component alias = nothing to build frames FROM. */
|
|
168
|
+
const noApp = (host) => !host.router && !host.tailwind && !host.shadcn && !host.themeCss;
|
|
169
|
+
/** The UI line of AGENTS.md, matched to what detection actually found (friction log #1).
|
|
170
|
+
* The STOP branch fires only on the same condition that creates SETUP.md - an app
|
|
171
|
+
* without Tailwind (plain React + CSS) gets guidance, never a dead pointer. */
|
|
172
|
+
function uiGuidance(host, isNoApp) {
|
|
173
|
+
if (isNoApp) return `STOP - this repo has no app yet. Read design/instructions/setup.md before designing anything.`;
|
|
174
|
+
if (host.shadcn) return `Use the app's UI: import from ${host.shadcn.uiAlias}; style with the app's Tailwind classes.`;
|
|
175
|
+
if (host.tailwind) return `Style with the app's Tailwind classes and design tokens; there is no detected component library - extract shared pieces into design/components/.`;
|
|
176
|
+
return `Use the app's existing components and stylesheets (import them directly); there is no Tailwind or component library detected - extract shared pieces into design/components/.`;
|
|
177
|
+
}
|
|
178
|
+
const SETUP_MD = `# Setup required - this repo has no app yet
|
|
179
|
+
|
|
180
|
+
> This file exists because \`${NAME} init\` ran in a repo with no framework, no theme
|
|
181
|
+
> CSS, and no component library. It disappears automatically: set up the app, re-run
|
|
182
|
+
> \`npx ${NAME} init\`, and init deletes this file and regenerates AGENTS.md against
|
|
183
|
+
> the real stack. While this file exists, DO NOT design.
|
|
184
|
+
|
|
185
|
+
${NAME} builds frames from YOUR components and YOUR theme. With none, frames become
|
|
186
|
+
hand-rolled CSS that shares nothing with the future app and cannot be promoted into
|
|
187
|
+
it later - work that gets thrown away.
|
|
188
|
+
|
|
189
|
+
## Do this first
|
|
190
|
+
|
|
191
|
+
For a web app or marketing site, the blessed stack. NOTE: create-next-app refuses a
|
|
192
|
+
non-empty directory (this repo already holds design/ and a package.json), so
|
|
193
|
+
scaffold into a temp dir and merge:
|
|
194
|
+
|
|
195
|
+
\`\`\`bash
|
|
196
|
+
npx create-next-app@latest app-scaffold --ts --tailwind --app --src-dir --yes
|
|
197
|
+
# move everything from app-scaffold/ up to the repo root EXCEPT its package.json;
|
|
198
|
+
# merge app-scaffold/package.json's dependencies+scripts into the existing one,
|
|
199
|
+
# then: rm -rf app-scaffold && npm install
|
|
200
|
+
npx shadcn@latest init
|
|
201
|
+
\`\`\`
|
|
202
|
+
|
|
203
|
+
shadcn's flags change between versions - if a flag errors or it prompts despite
|
|
204
|
+
--yes, answer the prompts with its defaults. Any React + CSS setup works; the
|
|
205
|
+
point is that components and a theme EXIST.
|
|
206
|
+
|
|
207
|
+
## Then
|
|
208
|
+
|
|
209
|
+
\`\`\`bash
|
|
210
|
+
npx ${NAME} init
|
|
211
|
+
\`\`\`
|
|
212
|
+
|
|
213
|
+
init is idempotent: it fills in what it now detects (theme wrapper, providers, a
|
|
214
|
+
shadcn-aware AGENTS.md), deletes this file, and you design from real parts.
|
|
215
|
+
`;
|
|
216
|
+
/** Next.js frames render OUTSIDE Next - say concretely what that means (friction log #10/#11). */
|
|
217
|
+
const NEXT_NOTES = `- Next.js caveats (frames render in Vite, outside Next):
|
|
218
|
+
next/font does not exist here - CSS variables it injects (e.g. --font-geist-sans) are
|
|
219
|
+
undefined in frames, so give every font token a real fallback chain in the app's CSS:
|
|
220
|
+
--font-sans: var(--font-geist-sans, ui-sans-serif, system-ui, sans-serif).
|
|
221
|
+
next/image and next/link render as plain img/a via shims at best - prefer <img> and
|
|
222
|
+
data-goto in frames. Server Components and server actions cannot run: frames are
|
|
223
|
+
client components importing client components.
|
|
224
|
+
`;
|
|
225
|
+
const STANDALONE_TSCONFIG = `{
|
|
226
|
+
"compilerOptions": {
|
|
227
|
+
"target": "ES2022",
|
|
228
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
229
|
+
"module": "ESNext",
|
|
230
|
+
"moduleResolution": "bundler",
|
|
231
|
+
"jsx": "react-jsx",
|
|
232
|
+
"strict": true,
|
|
233
|
+
"skipLibCheck": true,
|
|
234
|
+
"noEmit": true
|
|
235
|
+
},
|
|
236
|
+
"include": ["."]
|
|
237
|
+
}
|
|
238
|
+
`;
|
|
239
|
+
function patchTsconfigExclude(root) {
|
|
240
|
+
const file = join(root, "tsconfig.json");
|
|
241
|
+
if (!existsSync(file)) return;
|
|
242
|
+
const raw = readFileSync(file, "utf8");
|
|
243
|
+
try {
|
|
244
|
+
let next;
|
|
245
|
+
if (/"exclude"\s*:/.test(raw)) next = raw.replace(/("exclude"\s*:\s*\[)/, "$1\"design\", ");
|
|
246
|
+
else {
|
|
247
|
+
const at = firstJsonBrace(raw);
|
|
248
|
+
if (at < 0) throw new Error("no object brace found");
|
|
249
|
+
next = raw.slice(0, at + 1) + "\n \"exclude\": [\"design\"]," + raw.slice(at + 1);
|
|
250
|
+
}
|
|
251
|
+
if (next === raw) throw new Error("no anchor matched");
|
|
252
|
+
writeFileSync(file, next);
|
|
253
|
+
console.log(`\n patched tsconfig.json (the only host file touched):`);
|
|
254
|
+
console.log(` + "design" added to "exclude" (revert this line to fully uninstall)`);
|
|
255
|
+
} catch {
|
|
256
|
+
console.warn(` could not patch tsconfig.json - add "design" to its "exclude" yourself.`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** Index of the first `{` outside //, /* *\/ comments and strings (tsconfig is JSONC). */
|
|
260
|
+
function firstJsonBrace(src) {
|
|
261
|
+
let inLine = false, inBlock = false, inStr = false;
|
|
262
|
+
for (let i = 0; i < src.length; i++) {
|
|
263
|
+
const c = src[i], n = src[i + 1];
|
|
264
|
+
if (inLine) {
|
|
265
|
+
if (c === "\n") inLine = false;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (inBlock) {
|
|
269
|
+
if (c === "*" && n === "/") {
|
|
270
|
+
inBlock = false;
|
|
271
|
+
i++;
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (inStr) {
|
|
276
|
+
if (c === "\\") i++;
|
|
277
|
+
else if (c === "\"") inStr = false;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (c === "/" && n === "/") {
|
|
281
|
+
inLine = true;
|
|
282
|
+
i++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (c === "/" && n === "*") {
|
|
286
|
+
inBlock = true;
|
|
287
|
+
i++;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (c === "\"") {
|
|
291
|
+
inStr = true;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (c === "{") return i;
|
|
295
|
+
}
|
|
296
|
+
return -1;
|
|
297
|
+
}
|
|
298
|
+
const configTemplate = (mode) => `// ${NAME} config - OPTIONAL. Delete this file and everything still works on defaults.
|
|
299
|
+
// Theme lives in design/theme.css (it imports your app's real stylesheet) - not here.
|
|
300
|
+
// Sharp edges (native Node TS import): erasable syntax only (no enums/namespaces),
|
|
301
|
+
// relative imports need extensions, tsconfig paths are ignored here.
|
|
302
|
+
export default {
|
|
303
|
+
mode: ${JSON.stringify(mode)},
|
|
304
|
+
// Device widths for frames and the Devices view. Rename, retune, or uncomment tv.
|
|
305
|
+
viewports: {
|
|
306
|
+
mobile: { width: 390, height: 844 },
|
|
307
|
+
tablet: { width: 768, height: 1024 },
|
|
308
|
+
laptop: { width: 1280, height: 800 },
|
|
309
|
+
monitor: { width: 1920, height: 1080 },
|
|
310
|
+
// tv: { width: 3840, height: 2160 },
|
|
311
|
+
},
|
|
312
|
+
themes: ["light", "dark"],
|
|
313
|
+
port: ${DEFAULTS.port},
|
|
314
|
+
// Canvas zoom feel: 1 = default, 1.2 = 20% faster, 0.8 = 20% slower.
|
|
315
|
+
// zoomSpeed: 1,
|
|
316
|
+
// Publishing (\`${NAME} build\` + \`${NAME} serve\`): gate identity + branding footer.
|
|
317
|
+
// name/logo default to the host package.json name and design/logo.svg (then public/).
|
|
318
|
+
// branding is the small "Powered by Marver.design" line under the gate. Marver is
|
|
319
|
+
// free, and that line is how it spreads - we'd love it if you leave it on, but it
|
|
320
|
+
// is yours to remove, no strings: share: { branding: false }.
|
|
321
|
+
// share: { name: "My App", logo: "design/logo.svg", branding: true },
|
|
322
|
+
}
|
|
323
|
+
`;
|
|
324
|
+
const themeWrapper = (relCss, v4) => `/* ${NAME} theme wrapper - imports the app's real theme; the app's own build never sees design/. */
|
|
325
|
+
@import "${relCss}";
|
|
326
|
+
${v4 ? `@source "./";\n` : ""}`;
|
|
327
|
+
function providersTemplate(router, toaster, routerPkg = "react-router-dom") {
|
|
328
|
+
const imports = [`import type { ReactNode } from 'react'`];
|
|
329
|
+
let open = "", close = "";
|
|
330
|
+
if (router === "react-router") {
|
|
331
|
+
imports.push(`import { MemoryRouter } from '${routerPkg}'`);
|
|
332
|
+
open += "<MemoryRouter>";
|
|
333
|
+
close = "</MemoryRouter>" + close;
|
|
334
|
+
}
|
|
335
|
+
let toasterEl = "";
|
|
336
|
+
if (toaster === "sonner") {
|
|
337
|
+
imports.push(`import { Toaster } from 'sonner'`);
|
|
338
|
+
toasterEl = "<Toaster />";
|
|
339
|
+
}
|
|
340
|
+
if (toaster === "react-hot-toast") {
|
|
341
|
+
imports.push(`import { Toaster } from 'react-hot-toast'`);
|
|
342
|
+
toasterEl = "<Toaster />";
|
|
343
|
+
}
|
|
344
|
+
return `// Mock contexts wrapped around every frame. Scaffolded by ${NAME} init from what it detected - yours to edit.
|
|
345
|
+
${imports.join("\n")}
|
|
346
|
+
|
|
347
|
+
export default function Providers({ children }: { children: ReactNode }) {
|
|
348
|
+
return (
|
|
349
|
+
${open || "<>"}
|
|
350
|
+
{children}
|
|
351
|
+
${toasterEl}
|
|
352
|
+
${close || "</>"}
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
`;
|
|
356
|
+
}
|
|
357
|
+
//#endregion
|
|
358
|
+
export { init };
|
|
@@ -204,7 +204,7 @@ function extractMeta(src) {
|
|
|
204
204
|
if (!m) return {};
|
|
205
205
|
const body = m[1];
|
|
206
206
|
const pick = (key) => {
|
|
207
|
-
const r = new RegExp(
|
|
207
|
+
const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(['"\`])([^'"\`]*)\\1`).exec(body);
|
|
208
208
|
return r ? r[2] : void 0;
|
|
209
209
|
};
|
|
210
210
|
const out = {};
|
|
@@ -214,6 +214,10 @@ function extractMeta(src) {
|
|
|
214
214
|
if (viewport) out.viewport = viewport;
|
|
215
215
|
const theme = pick("theme");
|
|
216
216
|
if (theme) out.theme = theme;
|
|
217
|
+
const of = pick("of");
|
|
218
|
+
if (of) out.of = of;
|
|
219
|
+
const variant = pick("variant");
|
|
220
|
+
if (variant) out.variant = variant;
|
|
217
221
|
return out;
|
|
218
222
|
}
|
|
219
223
|
/** id = path relative to design/, extension dropped, `scenes/` prefix dropped. Always `/`-separated. */
|
|
@@ -256,6 +260,8 @@ function scanFrames(root) {
|
|
|
256
260
|
if (meta.title) entry.title = meta.title;
|
|
257
261
|
if (meta.viewport) entry.viewport = meta.viewport;
|
|
258
262
|
if (meta.theme) entry.theme = meta.theme;
|
|
263
|
+
if (meta.of) entry.variantGroup = meta.of;
|
|
264
|
+
if (meta.variant) entry.variant = meta.variant;
|
|
259
265
|
}
|
|
260
266
|
frames.push(entry);
|
|
261
267
|
}
|
|
@@ -273,6 +279,7 @@ function scanFrames(root) {
|
|
|
273
279
|
});
|
|
274
280
|
frames.length = 0;
|
|
275
281
|
frames.push(...deduped);
|
|
282
|
+
inferVariantGroups(frames);
|
|
276
283
|
const sceneCounts = /* @__PURE__ */ new Map();
|
|
277
284
|
for (const f of frames) sceneCounts.set(f.scene, (sceneCounts.get(f.scene) ?? 0) + 1);
|
|
278
285
|
return {
|
|
@@ -283,6 +290,75 @@ function scanFrames(root) {
|
|
|
283
290
|
})).sort((a, b) => a.name.localeCompare(b.name))
|
|
284
291
|
};
|
|
285
292
|
}
|
|
293
|
+
/** Variant groups (SPEC-023 §1). A group = 2+ frames in one DIRECTORY whose basenames
|
|
294
|
+
* are letter-prefixed (`a-terminal`), or frames declaring `meta.of`. Group id = the
|
|
295
|
+
* directory's id prefix (or meta.of); variant key = the letter (or meta.variant).
|
|
296
|
+
* Nested directories scope alternatives inside a busy scene (checkout/payment/a-card).
|
|
297
|
+
* States (empty.tsx, error.tsx) never letter-prefix, so they never misgroup.
|
|
298
|
+
* Mutates entries in place: only frames whose group materializes keep the fields. */
|
|
299
|
+
function inferVariantGroups(frames) {
|
|
300
|
+
const dirOf = (id) => {
|
|
301
|
+
const i = id.lastIndexOf("/");
|
|
302
|
+
return i >= 0 ? id.slice(0, i) : "";
|
|
303
|
+
};
|
|
304
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
305
|
+
for (const f of frames) {
|
|
306
|
+
if (f.kind !== "tsx") {
|
|
307
|
+
delete f.variantGroup;
|
|
308
|
+
delete f.variant;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
let group = f.variantGroup;
|
|
312
|
+
let key = f.variant;
|
|
313
|
+
if (!group) {
|
|
314
|
+
const base = f.id.slice(f.id.lastIndexOf("/") + 1);
|
|
315
|
+
const m = /^([a-z])-.+$/.exec(base);
|
|
316
|
+
if (!m) continue;
|
|
317
|
+
group = dirOf(f.id);
|
|
318
|
+
if (!group) continue;
|
|
319
|
+
key = key ?? m[1];
|
|
320
|
+
} else if (!key) {
|
|
321
|
+
const m = /^([a-z])-.+$/.exec(f.id.slice(f.id.lastIndexOf("/") + 1));
|
|
322
|
+
if (!m) {
|
|
323
|
+
console.warn(`[marver] ${f.file} declares of:"${group}" but no variant key (add meta.variant or a letter prefix) - not grouped.`);
|
|
324
|
+
delete f.variantGroup;
|
|
325
|
+
delete f.variant;
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
key = m[1];
|
|
329
|
+
}
|
|
330
|
+
f.variantGroup = group;
|
|
331
|
+
f.variant = key;
|
|
332
|
+
candidates.set(group, [...candidates.get(group) ?? [], f]);
|
|
333
|
+
}
|
|
334
|
+
for (const [group, members] of candidates) {
|
|
335
|
+
if (new Set(members.map((m) => dirOf(m.id))).size > 1) {
|
|
336
|
+
console.warn(`[marver] group "${group}" spans directories - variants must be siblings; not grouped.`);
|
|
337
|
+
for (const m of members) {
|
|
338
|
+
delete m.variantGroup;
|
|
339
|
+
delete m.variant;
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const seen = /* @__PURE__ */ new Set();
|
|
344
|
+
const kept = [];
|
|
345
|
+
for (const m of members.sort((a, b) => (a.variant ?? "").localeCompare(b.variant ?? ""))) {
|
|
346
|
+
if (seen.has(m.variant)) {
|
|
347
|
+
console.warn(`[marver] duplicate variant "${m.variant}" in group "${group}" (${m.file}) - not grouped.`);
|
|
348
|
+
delete m.variantGroup;
|
|
349
|
+
delete m.variant;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
seen.add(m.variant);
|
|
353
|
+
kept.push(m);
|
|
354
|
+
}
|
|
355
|
+
if (kept.length < 2) for (const m of kept) {
|
|
356
|
+
delete m.variantGroup;
|
|
357
|
+
delete m.variant;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
for (const f of frames) if (f.variant && !f.variantGroup) delete f.variant;
|
|
361
|
+
}
|
|
286
362
|
/** Write design/manifest.json only when content changed. Returns the manifest either way. */
|
|
287
363
|
function writeManifest(root, manifest) {
|
|
288
364
|
const file = join(root, "design", "manifest.json");
|
|
@@ -295,4 +371,4 @@ function writeManifest(root, manifest) {
|
|
|
295
371
|
}
|
|
296
372
|
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
297
373
|
//#endregion
|
|
298
|
-
export { loadConfig as a, DEFAULTS as i, scanFrames as n, detectHost as o, writeManifest as r, hash as t };
|
|
374
|
+
export { loadConfig as a, DEFAULTS as i, scanFrames as n, detectHost as o, writeManifest as r, readJson as s, hash as t };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { r as ROUTE } from "./cli.mjs";
|
|
2
|
-
import { n as scanFrames, r as writeManifest, t as hash } from "./manifest-
|
|
1
|
+
import { n as PKG, r as ROUTE, t as NAME } from "./cli.mjs";
|
|
2
|
+
import { n as scanFrames, r as writeManifest, t as hash } from "./manifest-mYlO_1Pj.mjs";
|
|
3
3
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
4
|
-
import { join, resolve, sep } from "node:path";
|
|
4
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { randomBytes } from "node:crypto";
|
|
6
7
|
//#region src/server/api.ts
|
|
7
8
|
const BOARD_NAME = /^[a-z0-9][a-z0-9-]*$/;
|
|
@@ -185,6 +186,73 @@ function routesMiddleware(server, clientDir) {
|
|
|
185
186
|
};
|
|
186
187
|
}
|
|
187
188
|
//#endregion
|
|
189
|
+
//#region src/server/update.ts
|
|
190
|
+
/**
|
|
191
|
+
* Update discovery - dev only, and deliberately boring about privacy: one anonymous
|
|
192
|
+
* registry metadata GET per day (the same request `npm view` makes), cached in
|
|
193
|
+
* design/.local/, nothing sent beyond the request itself. Offline, slow, or
|
|
194
|
+
* firewalled registries degrade to silence. MARVER_NO_UPDATE_CHECK=1 disables it.
|
|
195
|
+
* Published bundles never check anything - viewers are not the owner.
|
|
196
|
+
*/
|
|
197
|
+
const TTL = 864e5;
|
|
198
|
+
/** Installed version: walk up from this module to the package's own package.json
|
|
199
|
+
* (one level from dist/, two from src/server/ - the walk covers both). */
|
|
200
|
+
function installedVersion() {
|
|
201
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
202
|
+
for (let i = 0; i < 4; i++) {
|
|
203
|
+
try {
|
|
204
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
205
|
+
if (pkg.name === "@marver-design/marver") return pkg.version;
|
|
206
|
+
} catch {}
|
|
207
|
+
dir = dirname(dir);
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
/** Strictly-newer numeric compare; anything non-numeric (prerelease tags) compares false. */
|
|
212
|
+
const newer = (a, b) => {
|
|
213
|
+
const pa = a.split(".").map((n) => parseInt(n, 10));
|
|
214
|
+
const pb = b.split(".").map((n) => parseInt(n, 10));
|
|
215
|
+
for (let i = 0; i < 3; i++) {
|
|
216
|
+
const x = pa[i] ?? 0, y = pb[i] ?? 0;
|
|
217
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return false;
|
|
218
|
+
if (x !== y) return x > y;
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
};
|
|
222
|
+
/** The latest published version when strictly newer than installed, else null. Never throws. */
|
|
223
|
+
async function checkUpdate(root) {
|
|
224
|
+
if (process.env.MARVER_NO_UPDATE_CHECK) return null;
|
|
225
|
+
const current = installedVersion();
|
|
226
|
+
if (!current) return null;
|
|
227
|
+
const latest = await latestVersion(root);
|
|
228
|
+
return latest && newer(latest, current) ? latest : null;
|
|
229
|
+
}
|
|
230
|
+
async function latestVersion(root) {
|
|
231
|
+
const cacheFile = join(root, "design", ".local", "update-check.json");
|
|
232
|
+
const wellFormed = (v) => typeof v === "string" && /^\d+\.\d+\.\d+$/.test(v);
|
|
233
|
+
try {
|
|
234
|
+
const c = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
235
|
+
const age = Date.now() - c.checkedAt;
|
|
236
|
+
if (typeof c.checkedAt === "number" && age >= 0 && age < TTL) return wellFormed(c.latest) ? c.latest : null;
|
|
237
|
+
} catch {}
|
|
238
|
+
let latest = null;
|
|
239
|
+
try {
|
|
240
|
+
const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(PKG)}/latest`, { signal: AbortSignal.timeout(3e3) });
|
|
241
|
+
if (res.ok) {
|
|
242
|
+
const v = (await res.json())?.version;
|
|
243
|
+
if (wellFormed(v)) latest = v;
|
|
244
|
+
}
|
|
245
|
+
} catch {}
|
|
246
|
+
try {
|
|
247
|
+
mkdirSync(join(root, "design", ".local"), { recursive: true });
|
|
248
|
+
writeFileSync(cacheFile, JSON.stringify({
|
|
249
|
+
checkedAt: Date.now(),
|
|
250
|
+
latest
|
|
251
|
+
}) + "\n");
|
|
252
|
+
} catch {}
|
|
253
|
+
return latest;
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
188
256
|
//#region src/server/plugin.ts
|
|
189
257
|
const VIRTUAL_THEME = "virtual:sh-theme";
|
|
190
258
|
const VIRTUAL_CONFIG = "virtual:sh-config";
|
|
@@ -211,12 +279,23 @@ function marverPlugin(ctx) {
|
|
|
211
279
|
console.warn("[marver] no theme detected - frames render unstyled. Create design/theme.css importing your app's stylesheet (or set `theme` in design/config.ts).");
|
|
212
280
|
return "/* marver: no theme configured */";
|
|
213
281
|
}
|
|
214
|
-
if (id === "\0virtual:sh-config")
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
282
|
+
if (id === "\0virtual:sh-config") {
|
|
283
|
+
const setupPending = (() => {
|
|
284
|
+
try {
|
|
285
|
+
const s = readFileSync(join(root, "design", "instructions", "setup.md"), "utf8");
|
|
286
|
+
return s.startsWith("# Setup required") && s.includes("marver init");
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
})();
|
|
291
|
+
return `export default ${JSON.stringify({
|
|
292
|
+
viewports: config.viewports,
|
|
293
|
+
themes: config.themes,
|
|
294
|
+
zoomSpeed: config.zoomSpeed,
|
|
295
|
+
noTheme: themeFile() == null,
|
|
296
|
+
setup: setupPending
|
|
297
|
+
})}`;
|
|
298
|
+
}
|
|
220
299
|
if (id === "\0virtual:sh-data") return "export default null";
|
|
221
300
|
},
|
|
222
301
|
/** HTML frames: inject theme + bridge into any design/**.html Vite serves.
|
|
@@ -263,6 +342,21 @@ function marverPlugin(ctx) {
|
|
|
263
342
|
}
|
|
264
343
|
next();
|
|
265
344
|
});
|
|
345
|
+
const update = checkUpdate(root).catch(() => null);
|
|
346
|
+
update.then((latest) => {
|
|
347
|
+
if (latest) console.log(`\n update: ${PKG} ${latest} is out (installed ${installedVersion() ?? "?"}) → npm i -D ${PKG}@latest && npx ${NAME} init\n`);
|
|
348
|
+
});
|
|
349
|
+
server.middlewares.use((req, res, next) => {
|
|
350
|
+
if (new URL(req.url ?? "/", "http://x").pathname !== `/__mv/api/update`) return next();
|
|
351
|
+
update.then((latest) => {
|
|
352
|
+
res.setHeader("content-type", "application/json");
|
|
353
|
+
res.setHeader("cache-control", "no-store");
|
|
354
|
+
res.end(JSON.stringify({
|
|
355
|
+
latest,
|
|
356
|
+
current: installedVersion()
|
|
357
|
+
}));
|
|
358
|
+
});
|
|
359
|
+
});
|
|
266
360
|
server.middlewares.use(apiMiddleware(root));
|
|
267
361
|
server.middlewares.use(routesMiddleware(server, clientDir));
|
|
268
362
|
const regen = debounce(() => {
|