truecanvas 0.6.5 → 0.6.6
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 -1
- package/dist/{chunk-7KJVGN4O.js → chunk-O6YS366Z.js} +52 -6
- package/dist/{chunk-FJO64PGM.js → chunk-S5PJFVUC.js} +14 -3
- package/dist/cli.js +396 -174
- package/dist/editor/assets/{Menu-ikkNcA3s.css → Menu-1oiiMScp.css} +1 -1
- package/dist/editor/assets/hub-JHnH5D_d.js +2 -0
- package/dist/editor/assets/main-NN-va8GV.js +27 -0
- package/dist/editor/hub.html +3 -3
- package/dist/editor/index.html +3 -3
- package/dist/hub.d.ts +1 -0
- package/dist/hub.js +1 -1
- package/dist/share/index.js +1 -1
- package/package.json +1 -1
- package/dist/editor/assets/hub-C_YkICvy.js +0 -2
- package/dist/editor/assets/main-Dx3BpXRE.js +0 -27
- /package/dist/editor/assets/{Menu-TTrH9CSM.js → Menu-BCzLjvGk.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
readPackage,
|
|
15
15
|
runScript,
|
|
16
16
|
startHub
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-S5PJFVUC.js";
|
|
18
18
|
import {
|
|
19
19
|
CHUNK,
|
|
20
20
|
decodeFrame,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
responseHeaders,
|
|
23
23
|
safePath,
|
|
24
24
|
viewerHtml
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-O6YS366Z.js";
|
|
26
26
|
import {
|
|
27
27
|
DEVICES,
|
|
28
28
|
Workspace,
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
detectPm,
|
|
53
53
|
findByPath,
|
|
54
54
|
findNode,
|
|
55
|
+
nodeBin,
|
|
55
56
|
parseCanvas,
|
|
56
57
|
run,
|
|
57
58
|
toolEnv
|
|
@@ -70,15 +71,15 @@ import {
|
|
|
70
71
|
} from "./chunk-C4SATRZT.js";
|
|
71
72
|
|
|
72
73
|
// src/cli/index.ts
|
|
73
|
-
import { spawn } from "child_process";
|
|
74
|
-
import
|
|
75
|
-
import
|
|
74
|
+
import { spawn as spawn2 } from "child_process";
|
|
75
|
+
import fs10 from "fs";
|
|
76
|
+
import path12 from "path";
|
|
76
77
|
import { parseArgs } from "util";
|
|
77
78
|
|
|
78
79
|
// src/server/http.ts
|
|
79
80
|
import http from "http";
|
|
80
|
-
import
|
|
81
|
-
import
|
|
81
|
+
import fs7 from "fs";
|
|
82
|
+
import path9 from "path";
|
|
82
83
|
import { randomUUID } from "crypto";
|
|
83
84
|
import { fileURLToPath } from "url";
|
|
84
85
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -86,7 +87,7 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
|
86
87
|
import launchEditor from "launch-editor";
|
|
87
88
|
|
|
88
89
|
// src/server/mcp.ts
|
|
89
|
-
import
|
|
90
|
+
import path4 from "path";
|
|
90
91
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
91
92
|
import { z } from "zod";
|
|
92
93
|
|
|
@@ -284,6 +285,7 @@ async function replaceAsync(input, re, fn) {
|
|
|
284
285
|
}
|
|
285
286
|
|
|
286
287
|
// src/share/publish.ts
|
|
288
|
+
import { createHash } from "crypto";
|
|
287
289
|
import fs2 from "fs";
|
|
288
290
|
import os from "os";
|
|
289
291
|
import path2 from "path";
|
|
@@ -341,36 +343,57 @@ async function publishSnapshot(site, dir, manifest, opts = {}) {
|
|
|
341
343
|
for (const f of manifest.frames) for (const p of [f.html, f.image]) files.push({ local: path2.join(dir, p), remote: `${manifest.id}/${p}` });
|
|
342
344
|
const skipped = [];
|
|
343
345
|
let uploaded = 0;
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
346
|
+
const fitting = files.filter((f) => {
|
|
347
|
+
if (fs2.statSync(f.local).size <= MAX_FILE) return true;
|
|
348
|
+
skipped.push(f.remote);
|
|
349
|
+
return false;
|
|
350
|
+
});
|
|
351
|
+
if (can.includes("batch")) {
|
|
352
|
+
uploaded += await sendBatches(site, base, fitting.map((f) => ({ local: f.local, path: f.remote, type: contentType(f.local) })));
|
|
353
|
+
} else {
|
|
354
|
+
const queue2 = [...fitting];
|
|
355
|
+
const worker = async () => {
|
|
356
|
+
for (let f = queue2.shift(); f; f = queue2.shift()) {
|
|
357
|
+
await request(site, "PUT", `${base}/files`, fs2.readFileSync(f.local), { "x-path": f.remote, "content-type": contentType(f.local) });
|
|
358
|
+
uploaded++;
|
|
351
359
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
};
|
|
356
|
-
await Promise.all(Array.from({ length: 4 }, worker));
|
|
360
|
+
};
|
|
361
|
+
await Promise.all(Array.from({ length: 4 }, worker));
|
|
362
|
+
}
|
|
357
363
|
if (skipped.some((s) => s.endsWith(".html"))) throw new Error(`A frame is too large to upload (over 4.4 MB): ${skipped.filter((s) => s.endsWith(".html")).join(", ")}`);
|
|
358
364
|
if (opts.live && "dir" in opts.live) {
|
|
359
365
|
const liveDir = opts.live.dir;
|
|
360
|
-
const liveFiles = walk(liveDir)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
366
|
+
const liveFiles = walk(liveDir).filter((rel) => {
|
|
367
|
+
if (fs2.statSync(path2.join(liveDir, rel)).size <= MAX_FILE) return true;
|
|
368
|
+
skipped.push(`live/${rel}`);
|
|
369
|
+
return false;
|
|
370
|
+
});
|
|
371
|
+
if (can.includes("live-blobs")) {
|
|
372
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
373
|
+
for (const rel of liveFiles) hashes.set(rel, createHash("sha256").update(fs2.readFileSync(path2.join(liveDir, rel))).digest("hex"));
|
|
374
|
+
const { missing } = await request(site, "POST", `${base}/live/manifest?version=${manifest.id}`, { files: Object.fromEntries(hashes) });
|
|
375
|
+
const byHash = new Map([...hashes].map(([rel, hash]) => [hash, rel]));
|
|
376
|
+
const pending = missing.map((h) => byHash.get(h)).filter(Boolean);
|
|
377
|
+
if (can.includes("batch")) uploaded += await sendBatches(site, base, pending.map((rel) => ({ local: path2.join(liveDir, rel), hash: hashes.get(rel) })));
|
|
378
|
+
else {
|
|
379
|
+
const blobWorker = async () => {
|
|
380
|
+
for (let rel = pending.shift(); rel; rel = pending.shift()) {
|
|
381
|
+
await request(site, "PUT", `${base}/live/blobs`, fs2.readFileSync(path2.join(liveDir, rel)), { "x-hash": hashes.get(rel), "content-type": "application/octet-stream" });
|
|
382
|
+
uploaded++;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
await Promise.all(Array.from({ length: 6 }, blobWorker));
|
|
371
386
|
}
|
|
372
|
-
}
|
|
373
|
-
|
|
387
|
+
} else {
|
|
388
|
+
const pending = [...liveFiles];
|
|
389
|
+
const liveWorker = async () => {
|
|
390
|
+
for (let rel = pending.shift(); rel; rel = pending.shift()) {
|
|
391
|
+
await request(site, "PUT", `${base}/live/files?version=${manifest.id}`, fs2.readFileSync(path2.join(liveDir, rel)), { "x-path": rel, "content-type": contentType(rel) });
|
|
392
|
+
uploaded++;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
await Promise.all(Array.from({ length: 6 }, liveWorker));
|
|
396
|
+
}
|
|
374
397
|
}
|
|
375
398
|
const change = {
|
|
376
399
|
...opts.password !== void 0 ? { password: opts.password } : {},
|
|
@@ -389,6 +412,35 @@ async function publishSnapshot(site, dir, manifest, opts = {}) {
|
|
|
389
412
|
const invited = opts.invite?.length ? await inviteTo(site, share.slug, opts.invite) : [];
|
|
390
413
|
return { url: done.url, version: manifest.id, versions: done.versions, uploaded, skipped, password, live: done.live ?? null, access, invited };
|
|
391
414
|
}
|
|
415
|
+
async function sendBatches(site, base, items) {
|
|
416
|
+
const LIMIT = 4 * 1024 * 1024;
|
|
417
|
+
const batches = [];
|
|
418
|
+
let current = [];
|
|
419
|
+
let size = 0;
|
|
420
|
+
for (const item of items) {
|
|
421
|
+
const n = fs2.statSync(item.local).size;
|
|
422
|
+
if (current.length && (size + n > LIMIT || current.length >= 150)) {
|
|
423
|
+
batches.push(current);
|
|
424
|
+
current = [];
|
|
425
|
+
size = 0;
|
|
426
|
+
}
|
|
427
|
+
current.push(item);
|
|
428
|
+
size += n;
|
|
429
|
+
}
|
|
430
|
+
if (current.length) batches.push(current);
|
|
431
|
+
const send = async (batch) => {
|
|
432
|
+
const bodies = batch.map((i) => fs2.readFileSync(i.local));
|
|
433
|
+
const head = Buffer.from(JSON.stringify({ files: batch.map((i, k) => ({ path: i.path, hash: i.hash, type: i.type, size: bodies[k].length })) }));
|
|
434
|
+
const len = Buffer.alloc(4);
|
|
435
|
+
len.writeUInt32BE(head.length);
|
|
436
|
+
await request(site, "POST", `${base}/files/batch`, Buffer.concat([len, head, ...bodies]), { "content-type": "application/octet-stream" });
|
|
437
|
+
};
|
|
438
|
+
const queue2 = [...batches];
|
|
439
|
+
await Promise.all(Array.from({ length: 3 }, async () => {
|
|
440
|
+
for (let b = queue2.shift(); b; b = queue2.shift()) await send(b);
|
|
441
|
+
}));
|
|
442
|
+
return items.length;
|
|
443
|
+
}
|
|
392
444
|
async function inviteTo(site, slug, emails) {
|
|
393
445
|
const res = await request(site, "POST", `/api/shares/${slug}/invites`, { emails });
|
|
394
446
|
return res.invited;
|
|
@@ -414,6 +466,21 @@ async function updateAccess(site, project, canvas, change) {
|
|
|
414
466
|
const share = await request(site, "GET", `/api/shares/${found.slug}`);
|
|
415
467
|
return { url: `${site.url}/s/${found.slug}`, access: access ?? share.access ?? null, invited };
|
|
416
468
|
}
|
|
469
|
+
async function linkInfo(site, project, canvas) {
|
|
470
|
+
const found = await request(site, "GET", `/api/shares?project=${encodeURIComponent(project)}&canvas=${encodeURIComponent(canvas)}`);
|
|
471
|
+
if (!found.slug) return null;
|
|
472
|
+
const share = await request(site, "GET", `/api/shares/${found.slug}`);
|
|
473
|
+
const last = share.versions?.at(-1);
|
|
474
|
+
return {
|
|
475
|
+
url: `${site.url}/s/${found.slug}`,
|
|
476
|
+
title: share.title ?? canvas,
|
|
477
|
+
access: share.access ?? null,
|
|
478
|
+
password: !!share.password,
|
|
479
|
+
versions: share.versions?.length ?? 0,
|
|
480
|
+
updatedAt: last?.createdAt ?? null,
|
|
481
|
+
live: !!last?.live
|
|
482
|
+
};
|
|
483
|
+
}
|
|
417
484
|
async function deleteShare(site, project, canvas) {
|
|
418
485
|
const found = await request(site, "GET", `/api/shares?project=${encodeURIComponent(project)}&canvas=${encodeURIComponent(canvas)}`);
|
|
419
486
|
if (!found.slug) return false;
|
|
@@ -485,6 +552,114 @@ function contentType(file) {
|
|
|
485
552
|
return types[ext] ?? "application/octet-stream";
|
|
486
553
|
}
|
|
487
554
|
|
|
555
|
+
// src/share/export.ts
|
|
556
|
+
import { spawn } from "child_process";
|
|
557
|
+
import fs3 from "fs";
|
|
558
|
+
import path3 from "path";
|
|
559
|
+
var SKIP = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", ".truecanvas", "out", ".turbo", ".vercel", ".swc", "pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb"]);
|
|
560
|
+
var CONFIGS = ["next.config.ts", "next.config.mts", "next.config.mjs", "next.config.js", "next.config.cjs"];
|
|
561
|
+
function canExport(config) {
|
|
562
|
+
return config.framework === "next" || config.framework === "vite";
|
|
563
|
+
}
|
|
564
|
+
async function exportSite(config) {
|
|
565
|
+
const root = config.root;
|
|
566
|
+
if (config.framework === "vite") {
|
|
567
|
+
const out = path3.join(root, "node_modules", ".cache", "truecanvas", "live-export");
|
|
568
|
+
const cleanup2 = () => fs3.rmSync(out, { recursive: true, force: true });
|
|
569
|
+
try {
|
|
570
|
+
await run2(root, bin(root, "vite", "bin/vite.js"), ["build", "--outDir", out, "--emptyOutDir"]);
|
|
571
|
+
return { dir: out, cleanup: cleanup2 };
|
|
572
|
+
} catch (err) {
|
|
573
|
+
cleanup2();
|
|
574
|
+
throw err;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const work = path3.join(root, ".truecanvas", "live-export");
|
|
578
|
+
fs3.rmSync(work, { recursive: true, force: true });
|
|
579
|
+
fs3.mkdirSync(work, { recursive: true });
|
|
580
|
+
const ignore = path3.join(root, ".truecanvas", ".gitignore");
|
|
581
|
+
if (!fs3.existsSync(ignore)) fs3.writeFileSync(ignore, "# Truecanvas: generated files\n*\n");
|
|
582
|
+
const cleanup = () => fs3.rmSync(work, { recursive: true, force: true });
|
|
583
|
+
try {
|
|
584
|
+
copyProject(config, work);
|
|
585
|
+
wrapConfig(work);
|
|
586
|
+
await run2(work, bin(root, "next", "dist/bin/next"), ["build"]);
|
|
587
|
+
const out = path3.join(work, "out");
|
|
588
|
+
if (!fs3.existsSync(path3.join(out, "index.html"))) throw new Error("The build finished without a static site (no out/index.html).");
|
|
589
|
+
return { dir: out, cleanup };
|
|
590
|
+
} catch (err) {
|
|
591
|
+
cleanup();
|
|
592
|
+
throw err;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function copyProject(config, work) {
|
|
596
|
+
const root = config.root;
|
|
597
|
+
const leaveOut = new Set([config.routeDir, config.canvasDir].map((p) => path3.normalize(p)));
|
|
598
|
+
for (const name of fs3.readdirSync(root)) {
|
|
599
|
+
if (SKIP.has(name) || leaveOut.has(name)) continue;
|
|
600
|
+
fs3.cpSync(path3.join(root, name), path3.join(work, name), {
|
|
601
|
+
recursive: true,
|
|
602
|
+
filter: (src) => path3.basename(src) !== "node_modules" && !leaveOut.has(path3.normalize(path3.relative(root, src)))
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
const appDir = path3.join(work, config.appDir);
|
|
606
|
+
fs3.rmSync(path3.join(appDir, "api"), { recursive: true, force: true });
|
|
607
|
+
for (const dir of [work, path3.join(work, "src")]) for (const f of ["middleware.ts", "middleware.js", "proxy.ts", "proxy.js"]) fs3.rmSync(path3.join(dir, f), { force: true });
|
|
608
|
+
}
|
|
609
|
+
function wrapConfig(work) {
|
|
610
|
+
const found = CONFIGS.find((f) => fs3.existsSync(path3.join(work, f)));
|
|
611
|
+
const ts = found ? /\.m?ts$/.test(found) : true;
|
|
612
|
+
let imports = "const base = {};";
|
|
613
|
+
if (found) {
|
|
614
|
+
const renamed = `next.config.truecanvas-base${path3.extname(found)}`;
|
|
615
|
+
fs3.renameSync(path3.join(work, found), path3.join(work, renamed));
|
|
616
|
+
imports = `import base from "./${renamed}";`;
|
|
617
|
+
}
|
|
618
|
+
const body = `// @ts-nocheck
|
|
619
|
+
// Generated by Truecanvas: the project's config, built as a static site for a share link.
|
|
620
|
+
${imports}
|
|
621
|
+
export default async function config(phase, ctx) {
|
|
622
|
+
let c = base;
|
|
623
|
+
if (typeof c === "function") c = await c(phase, ctx);
|
|
624
|
+
const { rewrites, redirects, headers, ...rest } = c ?? {};
|
|
625
|
+
return { ...rest, output: "export", typescript: { ...rest.typescript, ignoreBuildErrors: true }, images: { ...rest.images, unoptimized: true } };
|
|
626
|
+
}
|
|
627
|
+
`;
|
|
628
|
+
fs3.writeFileSync(path3.join(work, ts ? "next.config.ts" : "next.config.mjs"), body);
|
|
629
|
+
}
|
|
630
|
+
function bin(root, pkg, file) {
|
|
631
|
+
const p = path3.join(root, "node_modules", pkg, file);
|
|
632
|
+
if (!fs3.existsSync(p)) throw new Error(`Couldn't find ${pkg} in ${path3.join(root, "node_modules")}. Install the project's dependencies first.`);
|
|
633
|
+
return p;
|
|
634
|
+
}
|
|
635
|
+
function run2(cwd, script, args) {
|
|
636
|
+
return new Promise((resolve, reject) => {
|
|
637
|
+
const env = toolEnv({ NODE_ENV: "production", NEXT_TELEMETRY_DISABLED: "1" });
|
|
638
|
+
const node = nodeBin();
|
|
639
|
+
if (!node) return reject(new Error("Couldn't find Node.js to build the app."));
|
|
640
|
+
const child = spawn(node, [script, ...args], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
641
|
+
const log = [];
|
|
642
|
+
const keep = (b) => {
|
|
643
|
+
for (const line of b.toString().split("\n")) if (line.trim()) log.push(line);
|
|
644
|
+
if (log.length > 40) log.splice(0, log.length - 40);
|
|
645
|
+
};
|
|
646
|
+
child.stdout.on("data", keep);
|
|
647
|
+
child.stderr.on("data", keep);
|
|
648
|
+
const timer = setTimeout(() => child.kill("SIGTERM"), 15 * 6e4);
|
|
649
|
+
child.on("error", (err) => {
|
|
650
|
+
clearTimeout(timer);
|
|
651
|
+
reject(err);
|
|
652
|
+
});
|
|
653
|
+
child.on("close", (code) => {
|
|
654
|
+
clearTimeout(timer);
|
|
655
|
+
if (code === 0) return resolve();
|
|
656
|
+
const tail = log.filter((l) => !/^\s*(▲|✓|○|ƒ|●|Route|Creating|Collecting|Generating|Finalizing|Linting|Compiled)/.test(l)).slice(-6).join("\n");
|
|
657
|
+
reject(new Error(`The app couldn't be built as a static site:
|
|
658
|
+
${tail || `exit code ${code}`}`));
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
488
663
|
// src/server/mcp.ts
|
|
489
664
|
var INSTRUCTIONS = `Truecanvas is a design canvas whose layers are the project's real React components.
|
|
490
665
|
A canvas is a .tsx file (canvas/<name>.canvas.tsx) with <Frame> artboards; every edit you make is written to that file as clean TSX and appears live in the user's editor window.
|
|
@@ -1111,13 +1286,14 @@ ${msgs}`;
|
|
|
1111
1286
|
...canvasArg,
|
|
1112
1287
|
frames: z.array(z.string()).optional().describe("Frame names to share; omit for all."),
|
|
1113
1288
|
title: z.string().optional().describe('Title the client sees, e.g. "Homepage, round 2".'),
|
|
1114
|
-
live: z.string().optional().describe(
|
|
1289
|
+
live: z.string().optional().describe("Another real site to use: the https URL where the app runs, or a static build folder (Next `out/`, Vite `dist/`, relative to the project). Usually omitted: the app is built as static pages automatically."),
|
|
1290
|
+
frozen: z.boolean().optional().describe("Leave the real site out (frozen frames only). By default the app is built as static pages so clients scroll and click the actual pages."),
|
|
1115
1291
|
invite: z.array(z.string()).optional().describe("Emails of people to invite (the client). Each gets an email with a link that signs them in and opens the design. Only invite addresses the user gave you."),
|
|
1116
1292
|
access: z.enum(["invited", "password", "public"]).optional().describe("Who can open the link: invited people (the default for new links on review sites with sign-in), a password (the user gives it to you), or anyone with the link. Existing links keep theirs unless this is set."),
|
|
1117
1293
|
password: z.string().optional().describe(`The link's password, with access "password".`)
|
|
1118
1294
|
}
|
|
1119
1295
|
},
|
|
1120
|
-
async ({ canvas, frames, title, live, invite, access, password }) => {
|
|
1296
|
+
async ({ canvas, frames, title, live, frozen, invite, access, password }) => {
|
|
1121
1297
|
try {
|
|
1122
1298
|
const c2 = resolveCanvas(canvas);
|
|
1123
1299
|
presence(c2, "looking", "Preparing a share link");
|
|
@@ -1129,14 +1305,24 @@ ${msgs}`;
|
|
|
1129
1305
|
].filter(Boolean);
|
|
1130
1306
|
const site = reviewSite();
|
|
1131
1307
|
if (!site) return text([`\u2713 Snapshot ready (${manifest.frames.length} frames). No review site is set up, so this is a local preview: ${preview}`, "To get client links, the user runs `npx truecanvas share setup`.", ...notes].join("\n"));
|
|
1132
|
-
|
|
1133
|
-
|
|
1308
|
+
let liveSite = live ? /^https?:\/\//.test(live) ? { url: live } : { dir: path4.resolve(ws.config.root, live) } : void 0;
|
|
1309
|
+
let built = null;
|
|
1310
|
+
if (!liveSite && !frozen && canExport(ws.config) && (await siteFeatures(site).catch(() => [])).includes("live-files")) {
|
|
1311
|
+
presence(c2, "looking", "Building the real site");
|
|
1312
|
+
try {
|
|
1313
|
+
built = await exportSite(ws.config);
|
|
1314
|
+
liveSite = { dir: built.dir };
|
|
1315
|
+
} catch (err) {
|
|
1316
|
+
notes.push(`The real site couldn't be built, so this version has frozen frames only: ${err.message}`);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
const published = await publishSnapshot(site, dir, manifest, { title, live: liveSite, access, password, invite: invite?.length ? parseEmails(invite) : void 0 }).finally(() => built?.cleanup());
|
|
1134
1320
|
const who = published.access === "invited" ? ", invited people only" : published.access === "public" ? ", anyone with the link" : published.password ? ", password protected" : "";
|
|
1135
1321
|
return text(
|
|
1136
1322
|
[
|
|
1137
1323
|
`\u2713 Shared ${manifest.frames.length} frame(s): ${published.url}`,
|
|
1138
1324
|
`Version ${published.versions} of this link${who}.`,
|
|
1139
|
-
published.live ?
|
|
1325
|
+
published.live ? "The real site is included: clients scroll and click the actual pages." : "",
|
|
1140
1326
|
...published.invited.map((i) => i.error ? `Couldn't email ${i.email}: ${i.error}` : `Invited ${i.email} (emailed a sign-in link).`),
|
|
1141
1327
|
published.access === "invited" && !published.invited.length ? "Only invited people and the studio can open it: share again with `invite` to invite the client." : "",
|
|
1142
1328
|
...notes
|
|
@@ -1354,13 +1540,13 @@ ${out.out.slice(-1500)}`));
|
|
|
1354
1540
|
|
|
1355
1541
|
// src/server/thumbnails.ts
|
|
1356
1542
|
import crypto2 from "crypto";
|
|
1357
|
-
import
|
|
1358
|
-
import
|
|
1543
|
+
import fs4 from "fs";
|
|
1544
|
+
import path5 from "path";
|
|
1359
1545
|
var Thumbnails = class _Thumbnails {
|
|
1360
1546
|
constructor(ws, shots) {
|
|
1361
1547
|
this.ws = ws;
|
|
1362
1548
|
this.shots = shots;
|
|
1363
|
-
this.dir =
|
|
1549
|
+
this.dir = path5.join(ws.config.root, "node_modules", ".cache", "truecanvas", "thumbs");
|
|
1364
1550
|
}
|
|
1365
1551
|
ws;
|
|
1366
1552
|
shots;
|
|
@@ -1386,8 +1572,8 @@ var Thumbnails = class _Thumbnails {
|
|
|
1386
1572
|
key(spec, props) {
|
|
1387
1573
|
let stamp = "";
|
|
1388
1574
|
try {
|
|
1389
|
-
const file = spec.library ?
|
|
1390
|
-
stamp = String(
|
|
1575
|
+
const file = spec.library ? path5.join(this.ws.config.root, "node_modules", spec.library, "package.json") : path5.join(this.ws.config.root, spec.file);
|
|
1576
|
+
stamp = String(fs4.statSync(file).mtimeMs);
|
|
1391
1577
|
} catch {
|
|
1392
1578
|
stamp = "0";
|
|
1393
1579
|
}
|
|
@@ -1398,14 +1584,14 @@ var Thumbnails = class _Thumbnails {
|
|
|
1398
1584
|
const spec = this.ws.catalog.get(name);
|
|
1399
1585
|
if (!spec) throw new Error(`Unknown component ${name}`);
|
|
1400
1586
|
const props = _Thumbnails.previewProps(spec, preset);
|
|
1401
|
-
const file =
|
|
1402
|
-
if (
|
|
1587
|
+
const file = path5.join(this.dir, `${name}-${this.key(spec, props)}.png`);
|
|
1588
|
+
if (fs4.existsSync(file)) return fs4.readFileSync(file);
|
|
1403
1589
|
const job = this.queue.then(async () => {
|
|
1404
|
-
if (
|
|
1590
|
+
if (fs4.existsSync(file)) return fs4.readFileSync(file);
|
|
1405
1591
|
const url = `${this.ws.config.appUrl}/truecanvas/__preview?component=${encodeURIComponent(name)}&props=${encodeURIComponent(JSON.stringify(props))}`;
|
|
1406
1592
|
const png = await this.shots.element(url, { width: 640, height: 480 });
|
|
1407
|
-
|
|
1408
|
-
|
|
1593
|
+
fs4.mkdirSync(this.dir, { recursive: true });
|
|
1594
|
+
fs4.writeFileSync(file, png);
|
|
1409
1595
|
return png;
|
|
1410
1596
|
});
|
|
1411
1597
|
this.queue = job.catch(() => void 0);
|
|
@@ -1414,14 +1600,14 @@ var Thumbnails = class _Thumbnails {
|
|
|
1414
1600
|
};
|
|
1415
1601
|
|
|
1416
1602
|
// src/server/screenshot.ts
|
|
1417
|
-
import
|
|
1603
|
+
import fs5 from "fs";
|
|
1418
1604
|
import os2 from "os";
|
|
1419
|
-
import
|
|
1605
|
+
import path6 from "path";
|
|
1420
1606
|
function findChromium() {
|
|
1421
1607
|
if (process.env.TRUECANVAS_CHROME) return process.env.TRUECANVAS_CHROME;
|
|
1422
|
-
const cache2 = process.env.PLAYWRIGHT_BROWSERS_PATH || (process.platform === "win32" ?
|
|
1423
|
-
if (
|
|
1424
|
-
const shells =
|
|
1608
|
+
const cache2 = process.env.PLAYWRIGHT_BROWSERS_PATH || (process.platform === "win32" ? path6.join(process.env.LOCALAPPDATA ?? path6.join(os2.homedir(), "AppData", "Local"), "ms-playwright") : process.platform === "darwin" ? path6.join(os2.homedir(), "Library", "Caches", "ms-playwright") : path6.join(os2.homedir(), ".cache", "ms-playwright"));
|
|
1609
|
+
if (fs5.existsSync(cache2)) {
|
|
1610
|
+
const shells = fs5.readdirSync(cache2).filter((d) => d.startsWith("chromium_headless_shell-")).sort().reverse();
|
|
1425
1611
|
for (const d of shells) {
|
|
1426
1612
|
for (const sub of [
|
|
1427
1613
|
"chrome-headless-shell-linux64/chrome-headless-shell",
|
|
@@ -1431,14 +1617,14 @@ function findChromium() {
|
|
|
1431
1617
|
"chrome-headless-shell-win64/chrome-headless-shell.exe",
|
|
1432
1618
|
"chrome-win/headless_shell.exe"
|
|
1433
1619
|
]) {
|
|
1434
|
-
const p =
|
|
1435
|
-
if (
|
|
1620
|
+
const p = path6.join(cache2, d, sub);
|
|
1621
|
+
if (fs5.existsSync(p)) return p;
|
|
1436
1622
|
}
|
|
1437
1623
|
}
|
|
1438
1624
|
}
|
|
1439
1625
|
const programFiles = [process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"], process.env.LOCALAPPDATA].filter(Boolean);
|
|
1440
1626
|
for (const p of [
|
|
1441
|
-
...programFiles.flatMap((dir) => [
|
|
1627
|
+
...programFiles.flatMap((dir) => [path6.join(dir, "Google", "Chrome", "Application", "chrome.exe"), path6.join(dir, "Microsoft", "Edge", "Application", "msedge.exe")]),
|
|
1442
1628
|
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
1443
1629
|
"/usr/bin/chromium-browser",
|
|
1444
1630
|
"/usr/bin/chromium",
|
|
@@ -1447,7 +1633,7 @@ function findChromium() {
|
|
|
1447
1633
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
1448
1634
|
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
1449
1635
|
]) {
|
|
1450
|
-
if (
|
|
1636
|
+
if (fs5.existsSync(p)) return p;
|
|
1451
1637
|
}
|
|
1452
1638
|
return void 0;
|
|
1453
1639
|
}
|
|
@@ -1665,9 +1851,9 @@ ${root.outerHTML}`, css, canvases, height };
|
|
|
1665
1851
|
var appRun = { state: "external", log: [] };
|
|
1666
1852
|
|
|
1667
1853
|
// src/server/review.ts
|
|
1668
|
-
import
|
|
1669
|
-
import
|
|
1670
|
-
import { createHash } from "crypto";
|
|
1854
|
+
import fs6 from "fs";
|
|
1855
|
+
import path7 from "path";
|
|
1856
|
+
import { createHash as createHash2 } from "crypto";
|
|
1671
1857
|
var actorName = (e) => e.actor.kind === "agent" ? e.actor.name : e.actor.kind === "file" ? "code editor" : "you";
|
|
1672
1858
|
async function versionAt(ws, canvas, ref) {
|
|
1673
1859
|
const rel = ws.relFile(canvas);
|
|
@@ -1683,14 +1869,14 @@ async function versionAt(ws, canvas, ref) {
|
|
|
1683
1869
|
}
|
|
1684
1870
|
var readNow = (ws, file) => {
|
|
1685
1871
|
try {
|
|
1686
|
-
return
|
|
1872
|
+
return fs6.readFileSync(path7.join(ws.config.root, file), "utf8");
|
|
1687
1873
|
} catch {
|
|
1688
1874
|
return null;
|
|
1689
1875
|
}
|
|
1690
1876
|
};
|
|
1691
1877
|
async function reviewPage(ws, canvas, ref, since) {
|
|
1692
1878
|
const rel = ws.relFile(canvas);
|
|
1693
|
-
const exists =
|
|
1879
|
+
const exists = fs6.existsSync(ws.file(canvas));
|
|
1694
1880
|
const old = await versionAt(ws, canvas, ref);
|
|
1695
1881
|
if (!exists && old.source === null) return null;
|
|
1696
1882
|
const now = exists ? ws.read(canvas) : null;
|
|
@@ -1760,7 +1946,7 @@ function renderFrame(ws, shots, canvas, frame, side, ref) {
|
|
|
1760
1946
|
async function render(ws, shots, canvas, frame, side, ref) {
|
|
1761
1947
|
let source;
|
|
1762
1948
|
let files = {};
|
|
1763
|
-
if (side === "after") source =
|
|
1949
|
+
if (side === "after") source = fs6.existsSync(ws.file(canvas)) ? ws.read(canvas) : null;
|
|
1764
1950
|
else {
|
|
1765
1951
|
const old = await versionAt(ws, canvas, ref);
|
|
1766
1952
|
source = old.source;
|
|
@@ -1771,12 +1957,12 @@ async function render(ws, shots, canvas, frame, side, ref) {
|
|
|
1771
1957
|
const f = doc.frames.find((x) => x.frameName === frame);
|
|
1772
1958
|
if (!f) return null;
|
|
1773
1959
|
const linkedNow = f.page ? [f.page, ...layoutFiles(ws.config, f.page)].map((x) => readNow(ws, x) ?? "").join("\0") : "";
|
|
1774
|
-
const key =
|
|
1960
|
+
const key = createHash2("sha1").update([canvas, frame, side, source, JSON.stringify(files), side === "after" ? linkedNow : ""].join("\0")).digest("hex");
|
|
1775
1961
|
const hit = cache.get(key);
|
|
1776
1962
|
if (hit) return hit;
|
|
1777
1963
|
let name = canvas;
|
|
1778
1964
|
const snapshot = `${canvas}.${side === "before" ? "base" : "now"}`;
|
|
1779
|
-
if (side === "before" || !
|
|
1965
|
+
if (side === "before" || !fs6.existsSync(ws.file(canvas))) name = writeCompare(ws.config, canvas, source, files, snapshot);
|
|
1780
1966
|
try {
|
|
1781
1967
|
const device = findDevice(f.device);
|
|
1782
1968
|
const png = await shots.frame({ canvas: name, frame, width: f.width, height: f.height, theme: f.theme ?? "light", scale: Math.min(2, Math.max(0.4, 1e3 / f.width)), mobile: device ? device.kind !== "desktop" : false, jpeg: true, maxHeight: 6e3 });
|
|
@@ -1811,7 +1997,7 @@ function prDesignSection(pages, images) {
|
|
|
1811
1997
|
}
|
|
1812
1998
|
|
|
1813
1999
|
// src/share/sync.ts
|
|
1814
|
-
import
|
|
2000
|
+
import path8 from "path";
|
|
1815
2001
|
var EVERY = 2e4;
|
|
1816
2002
|
var LOOKUP_EVERY = 5 * 6e4;
|
|
1817
2003
|
var CommentSync = class {
|
|
@@ -1863,7 +2049,7 @@ var CommentSync = class {
|
|
|
1863
2049
|
async slugOf(site, canvas) {
|
|
1864
2050
|
const known = this.links.get(canvas);
|
|
1865
2051
|
if (known && (known.slug || Date.now() - known.at < LOOKUP_EVERY)) return known.slug;
|
|
1866
|
-
const project =
|
|
2052
|
+
const project = path8.basename(this.ws.config.root);
|
|
1867
2053
|
const res = await call(site, "GET", `/api/shares?project=${encodeURIComponent(project)}&canvas=${encodeURIComponent(canvas)}`);
|
|
1868
2054
|
this.links.set(canvas, { slug: res.slug, at: Date.now() });
|
|
1869
2055
|
return res.slug;
|
|
@@ -2091,8 +2277,8 @@ var LiveSession = class {
|
|
|
2091
2277
|
}
|
|
2092
2278
|
// ---------- the tunnel ----------
|
|
2093
2279
|
/** The app URL for a tunneled path, or null if it would leave the app. */
|
|
2094
|
-
target(
|
|
2095
|
-
const safe = safePath(
|
|
2280
|
+
target(path13, ws = false) {
|
|
2281
|
+
const safe = safePath(path13);
|
|
2096
2282
|
if (!safe) return null;
|
|
2097
2283
|
const url = new URL(safe, this.app);
|
|
2098
2284
|
if (url.origin !== this.app.origin) return null;
|
|
@@ -2217,8 +2403,8 @@ var LiveSession = class {
|
|
|
2217
2403
|
}
|
|
2218
2404
|
});
|
|
2219
2405
|
}
|
|
2220
|
-
openRelay(id,
|
|
2221
|
-
const target = this.target(
|
|
2406
|
+
openRelay(id, path13, protocols) {
|
|
2407
|
+
const target = this.target(path13, true);
|
|
2222
2408
|
if (!target) {
|
|
2223
2409
|
this.send({ t: "ws-close", id, code: 1008, reason: "Not part of the app" });
|
|
2224
2410
|
return;
|
|
@@ -2335,17 +2521,23 @@ var MIME = {
|
|
|
2335
2521
|
".ico": "image/x-icon"
|
|
2336
2522
|
};
|
|
2337
2523
|
async function startServer(ws) {
|
|
2524
|
+
let shareStep = null;
|
|
2338
2525
|
const { config } = ws;
|
|
2339
2526
|
const editorDir = fileURLToPath(new URL("./editor/", import.meta.url));
|
|
2340
2527
|
const shots = new Screenshotter(config.appUrl);
|
|
2341
2528
|
let prCache = null;
|
|
2342
2529
|
const thumbs = new Thumbnails(ws, shots);
|
|
2343
2530
|
const hub = process.env.TRUECANVAS_HUB_URL;
|
|
2344
|
-
const
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2531
|
+
const clients = /* @__PURE__ */ new Set();
|
|
2532
|
+
const commentSync = new CommentSync(ws, (e) => {
|
|
2533
|
+
const data2 = `data: ${JSON.stringify({ type: "client-comment", ...e })}
|
|
2534
|
+
|
|
2535
|
+
`;
|
|
2536
|
+
for (const res of clients) res.write(data2);
|
|
2537
|
+
if (hub)
|
|
2538
|
+
void fetch(`${hub}/api/hub/notify`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ path: config.root, ...e }), signal: AbortSignal.timeout(5e3) }).catch(() => {
|
|
2539
|
+
});
|
|
2540
|
+
});
|
|
2349
2541
|
commentSync.start();
|
|
2350
2542
|
const syncRegistry = () => ws.catalog.load().then((list) => syncComponentRegistry(config, list)).catch(() => {
|
|
2351
2543
|
});
|
|
@@ -2353,7 +2545,6 @@ async function startServer(ws) {
|
|
|
2353
2545
|
const origin = `http://localhost:${config.port}`;
|
|
2354
2546
|
const allowedHosts = /* @__PURE__ */ new Set([`localhost:${config.port}`, `127.0.0.1:${config.port}`]);
|
|
2355
2547
|
const allowedOrigins = /* @__PURE__ */ new Set([origin, `http://127.0.0.1:${config.port}`]);
|
|
2356
|
-
const clients = /* @__PURE__ */ new Set();
|
|
2357
2548
|
const broadcast = (e) => {
|
|
2358
2549
|
const data2 = `data: ${JSON.stringify(e)}
|
|
2359
2550
|
|
|
@@ -2367,7 +2558,7 @@ async function startServer(ws) {
|
|
|
2367
2558
|
`;
|
|
2368
2559
|
for (const res of clients) res.write(data2);
|
|
2369
2560
|
};
|
|
2370
|
-
const sessions = new Sessions(
|
|
2561
|
+
const sessions = new Sessions(path9.basename(config.root), config.appUrl, {
|
|
2371
2562
|
onEvent: (canvas, e) => {
|
|
2372
2563
|
if (e.t === "comments") void commentSync.syncAll();
|
|
2373
2564
|
else sendRaw({ type: "room", canvas, event: e });
|
|
@@ -2429,7 +2620,7 @@ async function startServer(ws) {
|
|
|
2429
2620
|
appUrl: config.appUrl,
|
|
2430
2621
|
framework: config.framework,
|
|
2431
2622
|
root: config.root,
|
|
2432
|
-
projectName:
|
|
2623
|
+
projectName: path9.basename(config.root),
|
|
2433
2624
|
canvases: ws.canvases(),
|
|
2434
2625
|
darkMode: config.darkMode,
|
|
2435
2626
|
agents: [...ws.agents.values()],
|
|
@@ -2579,7 +2770,7 @@ async function startServer(ws) {
|
|
|
2579
2770
|
for (const file of [f.page, ...layoutFiles(config, f.page)]) {
|
|
2580
2771
|
const old = await ws.git.show(ref, file);
|
|
2581
2772
|
if (old === null) continue;
|
|
2582
|
-
const now =
|
|
2773
|
+
const now = fs7.existsSync(path9.join(config.root, file)) ? fs7.readFileSync(path9.join(config.root, file), "utf8") : null;
|
|
2583
2774
|
if (old !== now) {
|
|
2584
2775
|
files[file] = old;
|
|
2585
2776
|
touched.add(f.frameName);
|
|
@@ -2709,26 +2900,53 @@ async function startServer(ws) {
|
|
|
2709
2900
|
}
|
|
2710
2901
|
case "POST /api/open-file": {
|
|
2711
2902
|
const { file } = await readJson(req);
|
|
2712
|
-
const abs =
|
|
2713
|
-
const rel =
|
|
2714
|
-
if (rel.startsWith("..") ||
|
|
2903
|
+
const abs = path9.resolve(config.root, file);
|
|
2904
|
+
const rel = path9.relative(config.root, abs);
|
|
2905
|
+
if (rel.startsWith("..") || path9.isAbsolute(rel)) return json(res, 400, { error: "Outside project." });
|
|
2715
2906
|
launchEditor(abs, config.editor);
|
|
2716
2907
|
return json(res, 200, { ok: true });
|
|
2717
2908
|
}
|
|
2718
2909
|
case "GET /api/share/status": {
|
|
2719
2910
|
const site = reviewSite();
|
|
2720
2911
|
const features = site ? await siteFeatures(site).catch(() => []) : [];
|
|
2721
|
-
return json(res, 200, { site: site ? site.url : null, features });
|
|
2912
|
+
return json(res, 200, { site: site ? site.url : null, features, realSite: canExport(config) && features.includes("live-files") });
|
|
2722
2913
|
}
|
|
2914
|
+
case "GET /api/share/link": {
|
|
2915
|
+
const canvas = url.searchParams.get("canvas") ?? "";
|
|
2916
|
+
assertCanvasName(canvas);
|
|
2917
|
+
const site = reviewSite();
|
|
2918
|
+
return json(res, 200, { link: site ? await linkInfo(site, path9.basename(config.root), canvas).catch(() => null) : null });
|
|
2919
|
+
}
|
|
2920
|
+
case "GET /api/share/progress":
|
|
2921
|
+
return json(res, 200, { step: shareStep });
|
|
2723
2922
|
case "POST /api/share/snapshot": {
|
|
2724
|
-
const { canvas, frames, local, title, password, live, access, invite } = await readJson(req);
|
|
2923
|
+
const { canvas, frames, local, title, password, live, access, invite, realSite } = await readJson(req);
|
|
2725
2924
|
assertCanvasName(canvas);
|
|
2726
|
-
const { dir, manifest } = await createSnapshot(ws, shots, canvas, { frames });
|
|
2727
|
-
const preview = `${origin}/share/${encodeURIComponent(canvas)}/${manifest.id}/`;
|
|
2728
2925
|
const site = local ? null : reviewSite();
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2926
|
+
let built = null;
|
|
2927
|
+
let realSiteError = null;
|
|
2928
|
+
try {
|
|
2929
|
+
shareStep = "Rendering every frame through your app";
|
|
2930
|
+
const { dir, manifest } = await createSnapshot(ws, shots, canvas, { frames });
|
|
2931
|
+
const preview = `${origin}/share/${encodeURIComponent(canvas)}/${manifest.id}/`;
|
|
2932
|
+
let liveSite = live;
|
|
2933
|
+
if (site && !live && realSite !== false && canExport(config) && (await siteFeatures(site).catch(() => [])).includes("live-files")) {
|
|
2934
|
+
shareStep = "Building the real site";
|
|
2935
|
+
try {
|
|
2936
|
+
built = await exportSite(config);
|
|
2937
|
+
liveSite = { dir: built.dir };
|
|
2938
|
+
} catch (err) {
|
|
2939
|
+
realSiteError = err.message;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
if (site) shareStep = `Uploading to ${site.url.replace(/^https?:\/\//, "")}`;
|
|
2943
|
+
const published = site ? await publishSnapshot(site, dir, manifest, { title, password, live: liveSite, access, invite: invite?.length ? parseEmails(invite) : void 0 }) : null;
|
|
2944
|
+
if (published) commentSync.forget(canvas);
|
|
2945
|
+
return json(res, 200, { manifest, preview, published, realSiteError });
|
|
2946
|
+
} finally {
|
|
2947
|
+
built?.cleanup();
|
|
2948
|
+
shareStep = null;
|
|
2949
|
+
}
|
|
2732
2950
|
}
|
|
2733
2951
|
// live sessions: clients follow the canvas live on its link (editor button, CLI, MCP, desktop tray)
|
|
2734
2952
|
case "GET /api/session":
|
|
@@ -2770,24 +2988,24 @@ async function startServer(ws) {
|
|
|
2770
2988
|
return json(res, 404, { error: `No route ${route}` });
|
|
2771
2989
|
}
|
|
2772
2990
|
function serveStatic(res, pathname) {
|
|
2773
|
-
let file =
|
|
2774
|
-
if (!inside(editorDir, file) || !
|
|
2775
|
-
if (!
|
|
2991
|
+
let file = path9.join(editorDir, pathname === "/" ? "index.html" : pathname);
|
|
2992
|
+
if (!inside(editorDir, file) || !fs7.existsSync(file) || fs7.statSync(file).isDirectory()) file = path9.join(editorDir, "index.html");
|
|
2993
|
+
if (!fs7.existsSync(file)) {
|
|
2776
2994
|
res.writeHead(500, { "content-type": "text/plain" });
|
|
2777
2995
|
return res.end("Editor bundle missing. Run `pnpm build` in packages/truecanvas.");
|
|
2778
2996
|
}
|
|
2779
|
-
const ext =
|
|
2997
|
+
const ext = path9.extname(file);
|
|
2780
2998
|
const immutable = pathname.startsWith("/assets/");
|
|
2781
2999
|
res.writeHead(200, { "content-type": MIME[ext] ?? "application/octet-stream", "cache-control": immutable ? "public, max-age=31536000, immutable" : "no-cache" });
|
|
2782
|
-
|
|
3000
|
+
fs7.createReadStream(file).on("error", () => res.destroy()).pipe(res);
|
|
2783
3001
|
}
|
|
2784
3002
|
function serveShare(res, rest) {
|
|
2785
3003
|
const root = sharesDir(config.root);
|
|
2786
|
-
let file =
|
|
2787
|
-
if (rest.endsWith("/")) file =
|
|
2788
|
-
if (!inside(root, file) || !
|
|
2789
|
-
res.writeHead(200, { "content-type": MIME[
|
|
2790
|
-
|
|
3004
|
+
let file = path9.join(root, decodeURIComponent(rest));
|
|
3005
|
+
if (rest.endsWith("/")) file = path9.join(file, "index.html");
|
|
3006
|
+
if (!inside(root, file) || !fs7.existsSync(file) || fs7.statSync(file).isDirectory()) return json(res, 404, { error: "Not found" });
|
|
3007
|
+
res.writeHead(200, { "content-type": MIME[path9.extname(file)] ?? "application/octet-stream", "cache-control": "no-cache", "x-content-type-options": "nosniff", "access-control-allow-origin": "*" });
|
|
3008
|
+
fs7.createReadStream(file).on("error", () => res.destroy()).pipe(res);
|
|
2791
3009
|
}
|
|
2792
3010
|
const server = http.createServer(async (req, res) => {
|
|
2793
3011
|
try {
|
|
@@ -2807,8 +3025,8 @@ async function startServer(ws) {
|
|
|
2807
3025
|
else res.end();
|
|
2808
3026
|
}
|
|
2809
3027
|
});
|
|
2810
|
-
const canvasDir =
|
|
2811
|
-
|
|
3028
|
+
const canvasDir = path9.join(config.root, config.canvasDir);
|
|
3029
|
+
fs7.mkdirSync(canvasDir, { recursive: true });
|
|
2812
3030
|
const pending = /* @__PURE__ */ new Map();
|
|
2813
3031
|
const onCanvasFile = (filename) => {
|
|
2814
3032
|
if (!filename) return;
|
|
@@ -2833,7 +3051,7 @@ async function startServer(ws) {
|
|
|
2833
3051
|
const watchCanvasDir = () => {
|
|
2834
3052
|
let ino = null;
|
|
2835
3053
|
try {
|
|
2836
|
-
ino =
|
|
3054
|
+
ino = fs7.statSync(canvasDir).ino;
|
|
2837
3055
|
} catch {
|
|
2838
3056
|
ino = null;
|
|
2839
3057
|
}
|
|
@@ -2842,21 +3060,21 @@ async function startServer(ws) {
|
|
|
2842
3060
|
canvasWatcher = null;
|
|
2843
3061
|
watchedIno = ino;
|
|
2844
3062
|
if (ino === null) return;
|
|
2845
|
-
canvasWatcher =
|
|
3063
|
+
canvasWatcher = fs7.watch(canvasDir, (_event, filename) => onCanvasFile(filename));
|
|
2846
3064
|
canvasWatcher.on("error", () => {
|
|
2847
3065
|
canvasWatcher?.close();
|
|
2848
3066
|
canvasWatcher = null;
|
|
2849
3067
|
});
|
|
2850
|
-
for (const f of
|
|
3068
|
+
for (const f of fs7.readdirSync(canvasDir)) onCanvasFile(f);
|
|
2851
3069
|
};
|
|
2852
3070
|
watchCanvasDir();
|
|
2853
3071
|
setInterval(watchCanvasDir, 2e3).unref();
|
|
2854
|
-
const appDirAbs =
|
|
2855
|
-
if (config.framework === "next" &&
|
|
3072
|
+
const appDirAbs = path9.join(config.root, config.appDir);
|
|
3073
|
+
if (config.framework === "next" && fs7.existsSync(appDirAbs)) {
|
|
2856
3074
|
const appPending = /* @__PURE__ */ new Map();
|
|
2857
|
-
const appWatcher =
|
|
3075
|
+
const appWatcher = fs7.watch(appDirAbs, { recursive: true }, (_event, filename) => {
|
|
2858
3076
|
if (!filename || !/(^|[\\/])(page|layout)\.[jt]sx$/.test(filename) || /^truecanvas[\\/]/.test(filename)) return;
|
|
2859
|
-
const rel =
|
|
3077
|
+
const rel = path9.join(config.appDir, filename).split(path9.sep).join("/");
|
|
2860
3078
|
clearTimeout(appPending.get(rel));
|
|
2861
3079
|
appPending.set(
|
|
2862
3080
|
rel,
|
|
@@ -2873,9 +3091,9 @@ async function startServer(ws) {
|
|
|
2873
3091
|
}
|
|
2874
3092
|
let catalogTimer;
|
|
2875
3093
|
for (const dir of componentRoots(config.components)) {
|
|
2876
|
-
const abs =
|
|
2877
|
-
if (!
|
|
2878
|
-
const componentWatcher =
|
|
3094
|
+
const abs = path9.join(config.root, dir);
|
|
3095
|
+
if (!fs7.existsSync(abs)) continue;
|
|
3096
|
+
const componentWatcher = fs7.watch(abs, { recursive: true }, () => {
|
|
2879
3097
|
clearTimeout(catalogTimer);
|
|
2880
3098
|
catalogTimer = setTimeout(() => {
|
|
2881
3099
|
ws.catalog.invalidate();
|
|
@@ -2911,8 +3129,8 @@ function dedent(code, col) {
|
|
|
2911
3129
|
return code.split("\n").map((l, i) => i > 0 && l.startsWith(pad) ? l.slice(col) : l).join("\n");
|
|
2912
3130
|
}
|
|
2913
3131
|
function inside(dir, file) {
|
|
2914
|
-
const rel =
|
|
2915
|
-
return !!rel && !rel.startsWith("..") && !
|
|
3132
|
+
const rel = path9.relative(dir, file);
|
|
3133
|
+
return !!rel && !rel.startsWith("..") && !path9.isAbsolute(rel);
|
|
2916
3134
|
}
|
|
2917
3135
|
|
|
2918
3136
|
// src/server/stdio.ts
|
|
@@ -2954,9 +3172,9 @@ async function runStdioBridge(mcpUrl) {
|
|
|
2954
3172
|
import { createRequire } from "module";
|
|
2955
3173
|
|
|
2956
3174
|
// src/hub/desktop.ts
|
|
2957
|
-
import
|
|
3175
|
+
import fs8 from "fs";
|
|
2958
3176
|
import os3 from "os";
|
|
2959
|
-
import
|
|
3177
|
+
import path10 from "path";
|
|
2960
3178
|
var ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
|
2961
3179
|
<defs>
|
|
2962
3180
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#ff9466"/><stop offset="1" stop-color="#ea6a3c"/></linearGradient>
|
|
@@ -2970,15 +3188,15 @@ var ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
|
|
2970
3188
|
<path fill="#f6f5f1" fill-rule="evenodd" d="M236 236h552v168H236z M428 316h168v472H428z"/>
|
|
2971
3189
|
</svg>
|
|
2972
3190
|
`;
|
|
2973
|
-
var data = () => process.env.XDG_DATA_HOME ??
|
|
3191
|
+
var data = () => process.env.XDG_DATA_HOME ?? path10.join(os3.homedir(), ".local", "share");
|
|
2974
3192
|
function installDesktop(cli, port) {
|
|
2975
|
-
const dir =
|
|
2976
|
-
|
|
2977
|
-
const icon =
|
|
2978
|
-
|
|
2979
|
-
const launcher =
|
|
3193
|
+
const dir = path10.join(data(), "truecanvas");
|
|
3194
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
3195
|
+
const icon = path10.join(dir, "icon.svg");
|
|
3196
|
+
fs8.writeFileSync(icon, ICON);
|
|
3197
|
+
const launcher = path10.join(dir, "launch.sh");
|
|
2980
3198
|
const url = `http://localhost:${port}`;
|
|
2981
|
-
|
|
3199
|
+
fs8.writeFileSync(
|
|
2982
3200
|
launcher,
|
|
2983
3201
|
`#!/bin/sh
|
|
2984
3202
|
# Truecanvas launcher: start the hub if needed, then open its window.
|
|
@@ -2987,7 +3205,7 @@ CLI=${JSON.stringify(cli)}
|
|
|
2987
3205
|
URL=${JSON.stringify(url)}
|
|
2988
3206
|
LOG="\${XDG_STATE_HOME:-$HOME/.local/state}/truecanvas-hub.log"
|
|
2989
3207
|
# apps started from the desktop don't get your shell's PATH: next, pnpm and npx need it
|
|
2990
|
-
export PATH=${JSON.stringify([
|
|
3208
|
+
export PATH=${JSON.stringify([path10.dirname(process.execPath), ...(process.env.PATH ?? "").split(path10.delimiter)].filter((p, i, a) => p && a.indexOf(p) === i).join(path10.delimiter))}
|
|
2991
3209
|
mkdir -p "$(dirname "$LOG")"
|
|
2992
3210
|
|
|
2993
3211
|
if ! curl -sf "$URL/api/hub/state" >/dev/null 2>&1; then
|
|
@@ -2996,7 +3214,7 @@ if ! curl -sf "$URL/api/hub/state" >/dev/null 2>&1; then
|
|
|
2996
3214
|
while [ $i -lt 60 ] && ! curl -sf "$URL/api/hub/state" >/dev/null 2>&1; do sleep 0.25; i=$((i+1)); done
|
|
2997
3215
|
fi
|
|
2998
3216
|
|
|
2999
|
-
PROFILE=${JSON.stringify(
|
|
3217
|
+
PROFILE=${JSON.stringify(path10.join(dir, "window"))}
|
|
3000
3218
|
for b in google-chrome google-chrome-stable chromium-browser chromium brave-browser microsoft-edge; do
|
|
3001
3219
|
if command -v "$b" >/dev/null 2>&1; then
|
|
3002
3220
|
exec "$b" --app="$URL" --class=Truecanvas --user-data-dir="$PROFILE" --no-first-run --no-default-browser-check
|
|
@@ -3006,10 +3224,10 @@ exec xdg-open "$URL"
|
|
|
3006
3224
|
`,
|
|
3007
3225
|
{ mode: 493 }
|
|
3008
3226
|
);
|
|
3009
|
-
const apps =
|
|
3010
|
-
|
|
3011
|
-
const desktop =
|
|
3012
|
-
|
|
3227
|
+
const apps = path10.join(data(), "applications");
|
|
3228
|
+
fs8.mkdirSync(apps, { recursive: true });
|
|
3229
|
+
const desktop = path10.join(apps, "truecanvas.desktop");
|
|
3230
|
+
fs8.writeFileSync(
|
|
3013
3231
|
desktop,
|
|
3014
3232
|
`[Desktop Entry]
|
|
3015
3233
|
Type=Application
|
|
@@ -3025,11 +3243,11 @@ StartupWMClass=Truecanvas
|
|
|
3025
3243
|
StartupNotify=true
|
|
3026
3244
|
`
|
|
3027
3245
|
);
|
|
3028
|
-
let command =
|
|
3029
|
-
const ours = !
|
|
3246
|
+
let command = path10.join(os3.homedir(), ".local", "bin", "truecanvas");
|
|
3247
|
+
const ours = !fs8.existsSync(command) || fs8.readFileSync(command, "utf8").includes("# truecanvas wrapper");
|
|
3030
3248
|
if (ours) {
|
|
3031
|
-
|
|
3032
|
-
|
|
3249
|
+
fs8.mkdirSync(path10.dirname(command), { recursive: true });
|
|
3250
|
+
fs8.writeFileSync(command, `#!/bin/sh
|
|
3033
3251
|
# truecanvas wrapper (installed by \`truecanvas desktop\`)
|
|
3034
3252
|
exec ${JSON.stringify(process.execPath)} ${JSON.stringify(cli)} "$@"
|
|
3035
3253
|
`, { mode: 493 });
|
|
@@ -3037,19 +3255,19 @@ exec ${JSON.stringify(process.execPath)} ${JSON.stringify(cli)} "$@"
|
|
|
3037
3255
|
return { desktop, launcher, icon, command };
|
|
3038
3256
|
}
|
|
3039
3257
|
function launcherPath() {
|
|
3040
|
-
const p =
|
|
3041
|
-
return
|
|
3258
|
+
const p = path10.join(data(), "truecanvas", "launch.sh");
|
|
3259
|
+
return fs8.existsSync(p) ? p : null;
|
|
3042
3260
|
}
|
|
3043
3261
|
function uninstallDesktop() {
|
|
3044
3262
|
const removed = [];
|
|
3045
|
-
const cmd =
|
|
3046
|
-
if (
|
|
3047
|
-
|
|
3263
|
+
const cmd = path10.join(os3.homedir(), ".local", "bin", "truecanvas");
|
|
3264
|
+
if (fs8.existsSync(cmd) && fs8.readFileSync(cmd, "utf8").includes("# truecanvas wrapper")) {
|
|
3265
|
+
fs8.rmSync(cmd);
|
|
3048
3266
|
removed.push(cmd);
|
|
3049
3267
|
}
|
|
3050
|
-
for (const f of [
|
|
3051
|
-
if (
|
|
3052
|
-
|
|
3268
|
+
for (const f of [path10.join(data(), "applications", "truecanvas.desktop"), path10.join(data(), "truecanvas")]) {
|
|
3269
|
+
if (fs8.existsSync(f)) {
|
|
3270
|
+
fs8.rmSync(f, { recursive: true, force: true });
|
|
3053
3271
|
removed.push(f);
|
|
3054
3272
|
}
|
|
3055
3273
|
}
|
|
@@ -3062,8 +3280,8 @@ import { createInterface } from "readline/promises";
|
|
|
3062
3280
|
|
|
3063
3281
|
// src/cli/doctor.ts
|
|
3064
3282
|
import { execFile } from "child_process";
|
|
3065
|
-
import
|
|
3066
|
-
import
|
|
3283
|
+
import fs9 from "fs";
|
|
3284
|
+
import path11 from "path";
|
|
3067
3285
|
var major = (v) => v ? Number(/(\d+)(?:\.(\d+))?/.exec(v)?.[1] ?? 0) : 0;
|
|
3068
3286
|
function has(cmd, args, cwd) {
|
|
3069
3287
|
return new Promise((resolve) => execFile(cmd, args, { cwd, timeout: 8e3 }, (err, stdout, stderr) => resolve(err ? null : `${stdout}${stderr}`)));
|
|
@@ -3090,7 +3308,7 @@ async function doctor(root, cliVersion) {
|
|
|
3090
3308
|
const tw = depVersion(root, "tailwindcss");
|
|
3091
3309
|
add(tw ? { label: "Tailwind CSS", status: "ok", detail: tw } : { label: "Tailwind CSS", status: "warn", detail: "not installed", fix: "Optional, but the layout and style controls write Tailwind classes" });
|
|
3092
3310
|
const config = loadConfig(root);
|
|
3093
|
-
if (config.framework === "next") add(
|
|
3311
|
+
if (config.framework === "next") add(fs9.existsSync(path11.join(root, config.appDir)) ? { label: "App Router", status: "ok", detail: config.appDir } : { label: "App Router", status: "fail", detail: `no ${config.appDir}/`, fix: "Truecanvas renders through the App Router (app/ or src/app/)" });
|
|
3094
3312
|
const tc = installedIn(root);
|
|
3095
3313
|
if (!tc.declared) add({ label: "Truecanvas in the project", status: "fail", detail: "not a dependency", fix: "npx truecanvas (sets it up)" });
|
|
3096
3314
|
else if (!tc.installed) add({ label: "Truecanvas in the project", status: "fail", detail: "declared but not installed", fix: `${pm} install` });
|
|
@@ -3099,15 +3317,15 @@ async function doctor(root, cliVersion) {
|
|
|
3099
3317
|
const wrapped = configStatus(root, config.framework);
|
|
3100
3318
|
const configLabel = config.framework === "vite" ? "vite.config" : "next.config";
|
|
3101
3319
|
const uses = config.framework === "vite" ? "has the truecanvas() plugin" : "uses withTruecanvas()";
|
|
3102
|
-
add(wrapped.wrapped ? { label: configLabel, status: "ok", detail: `${
|
|
3320
|
+
add(wrapped.wrapped ? { label: configLabel, status: "ok", detail: `${path11.basename(wrapped.file)} ${uses}` } : { label: configLabel, status: "fail", detail: wrapped.file ? "Truecanvas not enabled" : "missing", fix: "npx truecanvas init" });
|
|
3103
3321
|
const scripts = pkg.scripts ?? {};
|
|
3104
3322
|
const script = Object.entries(scripts).find(([, v]) => v.includes("truecanvas"));
|
|
3105
3323
|
add(script ? { label: "Start script", status: "ok", detail: runScript(pm, script[0]) } : { label: "Start script", status: "warn", detail: "none", fix: 'npx truecanvas init (adds "canvas": "truecanvas dev")' });
|
|
3106
|
-
const ignore =
|
|
3324
|
+
const ignore = fs9.existsSync(path11.join(root, ".gitignore")) ? fs9.readFileSync(path11.join(root, ".gitignore"), "utf8") : "";
|
|
3107
3325
|
add(ignore.includes(config.routeDir) ? { label: "Generated route ignored", status: "ok" } : { label: "Generated route ignored", status: "warn", detail: `${config.routeDir}/ isn't in .gitignore`, fix: "npx truecanvas init" });
|
|
3108
3326
|
add(hasAgentConfig(root) ? { label: "Agents", status: "ok", detail: ".mcp.json lists Truecanvas" } : { label: "Agents", status: "warn", detail: "not connected for this project", fix: "npx truecanvas connect (then commit .mcp.json)" });
|
|
3109
3327
|
const chrome = findChromium();
|
|
3110
|
-
add(chrome ? { label: "Screenshots", status: "ok", detail:
|
|
3328
|
+
add(chrome ? { label: "Screenshots", status: "ok", detail: path11.basename(chrome) } : { label: "Screenshots", status: "warn", detail: "no Chromium found", fix: "npx playwright install chromium-headless-shell (or install Chrome)" });
|
|
3111
3329
|
const git = await has("git", ["rev-parse", "--is-inside-work-tree"], root);
|
|
3112
3330
|
add(git ? { label: "Git", status: "ok" } : { label: "Git", status: "warn", detail: "not a repository", fix: "git init (branches, history, comments and compare use git)" });
|
|
3113
3331
|
const gh = await has("gh", ["auth", "status"], root);
|
|
@@ -3134,7 +3352,8 @@ Usage
|
|
|
3134
3352
|
truecanvas desktop Add Truecanvas to your app launcher (Linux); "desktop remove" undoes it
|
|
3135
3353
|
truecanvas share [canvas] Share a canvas as a link for clients (needs truecanvas running).
|
|
3136
3354
|
--password <p>, --no-password, --revoke, --restore, --delete, --title <t>, --local (preview only),
|
|
3137
|
-
|
|
3355
|
+
The real site (your app built as static pages) is included, so clients click the actual pages;
|
|
3356
|
+
--frozen leaves it out, --live <url|dir> uses another one (where the app runs, or a static build)
|
|
3138
3357
|
--invite <emails>: invite people by email (they sign in with a link; new links are invited-only)
|
|
3139
3358
|
--access invited|password|public: who can open the link
|
|
3140
3359
|
--link-only: with --invite or --access, change the link without a new version
|
|
@@ -3235,6 +3454,7 @@ async function main() {
|
|
|
3235
3454
|
delete: { type: "boolean" },
|
|
3236
3455
|
token: { type: "string" },
|
|
3237
3456
|
live: { type: "string" },
|
|
3457
|
+
frozen: { type: "boolean" },
|
|
3238
3458
|
invite: { type: "string", multiple: true },
|
|
3239
3459
|
access: { type: "string" },
|
|
3240
3460
|
"link-only": { type: "boolean" },
|
|
@@ -3259,9 +3479,9 @@ async function main() {
|
|
|
3259
3479
|
}
|
|
3260
3480
|
if (!await ensureSetup(root2, pkgDir, version, yes)) return;
|
|
3261
3481
|
if (await openInWindow(root2, values.port ? Number(values.port) : hubPort())) return;
|
|
3262
|
-
const local =
|
|
3263
|
-
if (
|
|
3264
|
-
const child =
|
|
3482
|
+
const local = path12.join(root2, "node_modules", "truecanvas", "dist", "cli.js");
|
|
3483
|
+
if (fs10.existsSync(local) && fs10.realpathSync(local) !== fs10.realpathSync(cli)) {
|
|
3484
|
+
const child = spawn2(process.execPath, [local, "dev", ...process.argv.slice(2).filter((a) => a !== "start" && a !== "-y" && a !== "--yes")], { stdio: "inherit" });
|
|
3265
3485
|
for (const sig of ["SIGINT", "SIGTERM"]) process.on(sig, () => child.kill(sig));
|
|
3266
3486
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
3267
3487
|
return;
|
|
@@ -3342,7 +3562,7 @@ async function main() {
|
|
|
3342
3562
|
return;
|
|
3343
3563
|
}
|
|
3344
3564
|
const root = process.cwd();
|
|
3345
|
-
if (!
|
|
3565
|
+
if (!fs10.existsSync(path12.join(root, "package.json"))) {
|
|
3346
3566
|
console.error("Run truecanvas from your app's root (no package.json here).");
|
|
3347
3567
|
process.exit(1);
|
|
3348
3568
|
}
|
|
@@ -3352,7 +3572,7 @@ async function main() {
|
|
|
3352
3572
|
const report = initProject(config);
|
|
3353
3573
|
ws.ensureRoute();
|
|
3354
3574
|
console.log(`
|
|
3355
|
-
${c.accent("\u25C6")} ${c.bold("Truecanvas")} is set up in ${c.bold(
|
|
3575
|
+
${c.accent("\u25C6")} ${c.bold("Truecanvas")} is set up in ${c.bold(path12.basename(root))}
|
|
3356
3576
|
`);
|
|
3357
3577
|
for (const line of report.done) console.log(` ${c.green("\u2713")} ${line}`);
|
|
3358
3578
|
console.log(` ${c.green("\u2713")} Canvas folder ${c.bold(config.canvasDir)} and dev route ${c.bold(config.routeDir)}`);
|
|
@@ -3384,7 +3604,7 @@ async function main() {
|
|
|
3384
3604
|
console.error(site ? "Name the canvas: truecanvas share <canvas> --delete" : `No review site yet: ${c.accent("npx truecanvas share setup")}`);
|
|
3385
3605
|
process.exit(1);
|
|
3386
3606
|
}
|
|
3387
|
-
const done = await deleteShare(site,
|
|
3607
|
+
const done = await deleteShare(site, path12.basename(root), canvas);
|
|
3388
3608
|
console.log(done ? ` ${c.green("\u2713")} Deleted the link of ${canvas}, with its versions and comments.` : ` ${canvas} has no link.`);
|
|
3389
3609
|
return;
|
|
3390
3610
|
}
|
|
@@ -3395,7 +3615,7 @@ async function main() {
|
|
|
3395
3615
|
process.exit(1);
|
|
3396
3616
|
}
|
|
3397
3617
|
const change = { ...values.revoke ? { revoked: true } : values.restore ? { revoked: false } : {}, ...values["no-password"] ? { password: null } : {} };
|
|
3398
|
-
const url = await updateShare(site,
|
|
3618
|
+
const url = await updateShare(site, path12.basename(root), canvas, change);
|
|
3399
3619
|
console.log(` ${c.green("\u2713")} ${values.revoke ? "Link revoked" : values.restore ? "Link restored" : "Password removed"}: ${url}`);
|
|
3400
3620
|
return;
|
|
3401
3621
|
}
|
|
@@ -3418,7 +3638,7 @@ async function main() {
|
|
|
3418
3638
|
process.exit(1);
|
|
3419
3639
|
}
|
|
3420
3640
|
try {
|
|
3421
|
-
const done = await updateAccess(site,
|
|
3641
|
+
const done = await updateAccess(site, path12.basename(root), canvas, { access, password: values.password, invite });
|
|
3422
3642
|
console.log(` ${c.green("\u2713")} ${done.url} ${c.dim(`(${accessLabel(done.access)})`)}`);
|
|
3423
3643
|
printInvited(done.invited);
|
|
3424
3644
|
} catch (err) {
|
|
@@ -3440,8 +3660,8 @@ async function main() {
|
|
|
3440
3660
|
const publish = !values.local && !!reviewSite();
|
|
3441
3661
|
console.log(` ${c.dim(`Rendering ${canvas} through your app${publish ? " and publishing it" : ""}\u2026`)}`);
|
|
3442
3662
|
const password = values.password;
|
|
3443
|
-
const live = values.live ? /^https?:\/\//.test(values.live) ? { url: values.live } : { dir:
|
|
3444
|
-
if (live && "dir" in live && !
|
|
3663
|
+
const live = values.live ? /^https?:\/\//.test(values.live) ? { url: values.live } : { dir: path12.resolve(values.live) } : void 0;
|
|
3664
|
+
if (live && "dir" in live && !fs10.existsSync(path12.join(live.dir, "index.html"))) {
|
|
3445
3665
|
console.error(` ${c.red("\u2717")} No index.html in ${live.dir}. Pass a static build folder (Next \`out/\`, Vite \`dist/\`) or the URL where the app runs.`);
|
|
3446
3666
|
process.exit(1);
|
|
3447
3667
|
}
|
|
@@ -3452,7 +3672,7 @@ async function main() {
|
|
|
3452
3672
|
const res = await fetch(`${base}/api/share/snapshot`, {
|
|
3453
3673
|
method: "POST",
|
|
3454
3674
|
headers: { "content-type": "application/json", origin: base },
|
|
3455
|
-
body: JSON.stringify({ canvas, local: !publish, title: values.title, password, live, access, invite })
|
|
3675
|
+
body: JSON.stringify({ canvas, local: !publish, title: values.title, password, live, access, invite, realSite: !values.frozen })
|
|
3456
3676
|
});
|
|
3457
3677
|
const body = await res.json();
|
|
3458
3678
|
if (!res.ok) {
|
|
@@ -3460,14 +3680,16 @@ async function main() {
|
|
|
3460
3680
|
process.exit(1);
|
|
3461
3681
|
}
|
|
3462
3682
|
const { manifest, published } = body;
|
|
3463
|
-
console.log(` ${c.green("\u2713")} ${manifest.frames.length} frame${manifest.frames.length === 1 ? "" : "s"}
|
|
3683
|
+
console.log(` ${c.green("\u2713")} ${manifest.frames.length} frame${manifest.frames.length === 1 ? "" : "s"} rendered.`);
|
|
3464
3684
|
if (manifest.external.length) console.log(` ${c.yellow("!")} While rendering, the app called ${manifest.external.join(", ")}. Whatever it showed is in the snapshot: use sample data for client links.`);
|
|
3465
3685
|
if (manifest.missing.length) console.log(` ${c.yellow("!")} ${manifest.missing.length} asset${manifest.missing.length === 1 ? "" : "s"} couldn't be fetched.`);
|
|
3466
3686
|
if (published?.skipped.length) console.log(` ${c.yellow("!")} Left out (over 4.4 MB): ${published.skipped.join(", ")}`);
|
|
3467
3687
|
if (published) {
|
|
3468
3688
|
const who = published.access ? `, ${accessLabel(published.access)}` : published.password ? ", password protected" : "";
|
|
3469
3689
|
console.log(` ${c.green("\u2713")} Shared: ${c.bold(published.url)} ${c.dim(`(version ${published.versions}${who})`)}`);
|
|
3470
|
-
if (published.live) console.log(` ${c.green("\u2713")}
|
|
3690
|
+
if (published.live) console.log(` ${c.green("\u2713")} Real site included: clients scroll and click the actual pages.`);
|
|
3691
|
+
if (body.realSiteError) console.log(` ${c.yellow("!")} The real site couldn't be built, so this version has frozen frames only:
|
|
3692
|
+
${c.dim(body.realSiteError.replace(/^/gm, " "))}`);
|
|
3471
3693
|
printInvited(published.invited);
|
|
3472
3694
|
if (published.access === "invited" && !published.invited.length) console.log(` ${c.dim("Only invited people and your studio can open it. Invite someone:")} ${c.accent(`npx truecanvas share ${canvas} --invite name@client.com --link-only`)}`);
|
|
3473
3695
|
console.log("");
|
|
@@ -3571,12 +3793,12 @@ async function openWindow(cli, port) {
|
|
|
3571
3793
|
const url = `http://localhost:${port}`;
|
|
3572
3794
|
const launcher = launcherPath();
|
|
3573
3795
|
if (launcher) {
|
|
3574
|
-
|
|
3796
|
+
spawn2(launcher, [], { stdio: "ignore", detached: true }).unref();
|
|
3575
3797
|
return console.log(`${c.accent("\u25C6")} Opening Truecanvas\u2026`);
|
|
3576
3798
|
}
|
|
3577
3799
|
const up = await fetch(`${url}/api/hub/state`, { signal: AbortSignal.timeout(1500) }).then((r) => r.ok).catch(() => false);
|
|
3578
3800
|
if (!up) {
|
|
3579
|
-
|
|
3801
|
+
spawn2(process.execPath, [cli, "hub", "--port", String(port), "--no-open"], { stdio: "ignore", detached: true }).unref();
|
|
3580
3802
|
for (let i = 0; i < 40 && !await fetch(`${url}/api/hub/state`).then((r) => r.ok).catch(() => false); i++) await new Promise((r) => setTimeout(r, 250));
|
|
3581
3803
|
}
|
|
3582
3804
|
openBrowser(url);
|
|
@@ -3589,7 +3811,7 @@ async function openInWindow(root, port) {
|
|
|
3589
3811
|
const post = (route, body) => fetch(`${hub}${route}`, { method: "POST", headers: { "content-type": "application/json", origin: hub }, body: JSON.stringify(body) }).catch(() => null);
|
|
3590
3812
|
await post("/api/hub/add", { path: root });
|
|
3591
3813
|
await post("/api/hub/open", { path: root, focus: "1" });
|
|
3592
|
-
console.log(`${c.accent("\u25C6")} Opened ${c.bold(
|
|
3814
|
+
console.log(`${c.accent("\u25C6")} Opened ${c.bold(path12.basename(root))} in the Truecanvas window ${c.dim(`(${hub})`)}.`);
|
|
3593
3815
|
return true;
|
|
3594
3816
|
}
|
|
3595
3817
|
async function prompt(question) {
|
|
@@ -3613,7 +3835,7 @@ async function ensureSetup(root, pkgDir, version, yes, force = false) {
|
|
|
3613
3835
|
const tc = installedIn(root);
|
|
3614
3836
|
const wrapped = configStatus(root).wrapped;
|
|
3615
3837
|
if (tc.declared && tc.installed && wrapped && !force) return true;
|
|
3616
|
-
const name = readPackage(root)?.name ??
|
|
3838
|
+
const name = readPackage(root)?.name ?? path12.basename(root);
|
|
3617
3839
|
if (!yes) {
|
|
3618
3840
|
if (!process.stdin.isTTY) {
|
|
3619
3841
|
console.error(`Truecanvas isn't set up in ${name} yet. Run again with --yes to set it up.`);
|
|
@@ -3684,10 +3906,10 @@ function startApp(root, appUrl, framework) {
|
|
|
3684
3906
|
const port = new URL(appUrl).port || (framework === "vite" ? "5173" : "3000");
|
|
3685
3907
|
const devArgs = framework === "vite" ? ["--port", port, "--strictPort", "--host", "127.0.0.1"] : ["dev", "-p", port, "-H", "127.0.0.1"];
|
|
3686
3908
|
const pkg = framework === "vite" ? "vite" : "next";
|
|
3687
|
-
const
|
|
3688
|
-
const [cmd, args] =
|
|
3909
|
+
const bin2 = binScript(root, pkg);
|
|
3910
|
+
const [cmd, args] = bin2 ? [process.execPath, [bin2, ...devArgs]] : [process.platform === "win32" ? "npx.cmd" : "npx", [pkg, ...devArgs]];
|
|
3689
3911
|
const label = framework === "vite" ? "vite" : "next";
|
|
3690
|
-
const child =
|
|
3912
|
+
const child = spawn2(cmd, args, { cwd: root, env: toolEnv(process.env.NO_COLOR ? {} : { FORCE_COLOR: "1" }), stdio: ["ignore", "pipe", "pipe"], shell: !bin2 && process.platform === "win32" });
|
|
3691
3913
|
child.on("error", (err) => console.error(` ${c.dim(`${label} \u2502`)} couldn't start ${pkg}: ${err.message}`));
|
|
3692
3914
|
const prefix = c.dim(`${label} \u2502 `);
|
|
3693
3915
|
for (const stream of [child.stdout, child.stderr]) {
|
|
@@ -3709,24 +3931,24 @@ function startApp(root, appUrl, framework) {
|
|
|
3709
3931
|
}
|
|
3710
3932
|
function binScript(root, pkg) {
|
|
3711
3933
|
try {
|
|
3712
|
-
const req = createRequire(
|
|
3934
|
+
const req = createRequire(path12.join(root, "package.json"));
|
|
3713
3935
|
const manifest = req.resolve(`${pkg}/package.json`);
|
|
3714
|
-
const { bin } = JSON.parse(
|
|
3715
|
-
const rel = typeof
|
|
3716
|
-
return rel ?
|
|
3936
|
+
const { bin: bin2 } = JSON.parse(fs10.readFileSync(manifest, "utf8"));
|
|
3937
|
+
const rel = typeof bin2 === "string" ? bin2 : bin2?.[pkg];
|
|
3938
|
+
return rel ? path12.join(path12.dirname(manifest), rel) : null;
|
|
3717
3939
|
} catch {
|
|
3718
3940
|
return null;
|
|
3719
3941
|
}
|
|
3720
3942
|
}
|
|
3721
3943
|
function stopTree(child, signal) {
|
|
3722
|
-
if (process.platform === "win32" && child.pid)
|
|
3944
|
+
if (process.platform === "win32" && child.pid) spawn2("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" }).on("error", () => {
|
|
3723
3945
|
});
|
|
3724
3946
|
else child.kill(signal);
|
|
3725
3947
|
}
|
|
3726
3948
|
function openBrowser(url) {
|
|
3727
3949
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3728
3950
|
try {
|
|
3729
|
-
|
|
3951
|
+
spawn2(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).on("error", () => {
|
|
3730
3952
|
}).unref();
|
|
3731
3953
|
} catch {
|
|
3732
3954
|
}
|