truecanvas 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -181
- package/dist/catalog-worker.js +1 -1
- package/dist/chunk-C4SATRZT.js +196 -0
- package/dist/chunk-PBKX3SBC.js +97 -0
- package/dist/chunk-TSHD2S36.js +28 -0
- package/dist/{chunk-753N4QRO.js → chunk-WU7NBTJH.js} +39 -20
- package/dist/{chunk-WB35FXGP.js → chunk-XWQ5MS24.js} +1071 -489
- package/dist/cli.js +676 -429
- package/dist/core/index.d.ts +179 -11
- package/dist/core/index.js +40 -25
- package/dist/editor/assets/Menu-BFifVcgz.js +9 -0
- package/dist/editor/assets/Menu-DXPm0EXo.css +1 -0
- package/dist/editor/assets/hub-UhU8yp5F.js +2 -0
- package/dist/editor/assets/main-UtSaCfnK.js +26 -0
- package/dist/editor/hub.html +3 -3
- package/dist/editor/index.html +3 -3
- package/dist/icons-worker.js +7 -0
- package/dist/next/index.d.ts +6 -1
- package/dist/next/index.js +16 -3
- package/dist/runtime/host.js +3 -1
- package/dist/vite/index.d.ts +33 -0
- package/dist/vite/index.js +81 -0
- package/package.json +15 -6
- package/dist/editor/assets/controls-BQYSgX53.css +0 -1
- package/dist/editor/assets/controls-BYFjuGCq.js +0 -9
- package/dist/editor/assets/hub-C4_700Hv.js +0 -2
- package/dist/editor/assets/main-CfAJbDf1.js +0 -25
|
@@ -1,78 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectFramework,
|
|
3
|
+
idOf,
|
|
4
|
+
isComponentName,
|
|
5
|
+
jsxName,
|
|
6
|
+
parseModule,
|
|
7
|
+
rewriteImportSource,
|
|
8
|
+
walk
|
|
9
|
+
} from "./chunk-PBKX3SBC.js";
|
|
1
10
|
import {
|
|
2
11
|
Catalog,
|
|
3
|
-
importSpecifier
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const appDir = file.appDir ?? (fs.existsSync(path.join(root, "src/app")) ? "src/app" : "app");
|
|
14
|
-
const srcPrefix = appDir.startsWith("src/") ? "src/" : "";
|
|
15
|
-
let deps = {};
|
|
16
|
-
try {
|
|
17
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
18
|
-
deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
19
|
-
} catch {
|
|
20
|
-
deps = {};
|
|
21
|
-
}
|
|
22
|
-
const knownLibraries = ["@paper-design/shaders-react"].filter((l) => l in deps);
|
|
23
|
-
return {
|
|
24
|
-
root,
|
|
25
|
-
appUrl: "http://localhost:3000",
|
|
26
|
-
port: 4800,
|
|
27
|
-
canvasDir: `${srcPrefix}canvas`,
|
|
28
|
-
appDir,
|
|
29
|
-
components: [`${srcPrefix}components/**/*.tsx`],
|
|
30
|
-
darkMode: { strategy: "class", value: "dark" },
|
|
31
|
-
...file,
|
|
32
|
-
libraries: [.../* @__PURE__ */ new Set([...knownLibraries, ...file.libraries ?? []])],
|
|
33
|
-
...stripUndefined(overrides)
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function stripUndefined(o) {
|
|
37
|
-
return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// src/core/ast.ts
|
|
41
|
-
import { parse } from "@babel/parser";
|
|
42
|
-
function parseModule(source) {
|
|
43
|
-
return parse(source, {
|
|
44
|
-
sourceType: "module",
|
|
45
|
-
plugins: ["typescript", "jsx"],
|
|
46
|
-
errorRecovery: false
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
function idOf(node) {
|
|
50
|
-
const loc = node.loc.start;
|
|
51
|
-
return `${loc.line}:${loc.column}`;
|
|
52
|
-
}
|
|
53
|
-
function jsxName(name) {
|
|
54
|
-
if (name.type === "JSXIdentifier") return name.name;
|
|
55
|
-
if (name.type === "JSXNamespacedName") return `${name.namespace.name}:${name.name.name}`;
|
|
56
|
-
return `${jsxName(name.object)}.${name.property.name}`;
|
|
57
|
-
}
|
|
58
|
-
function walk(node, visit, parent = null) {
|
|
59
|
-
if (!node || typeof node !== "object") return;
|
|
60
|
-
if (Array.isArray(node)) {
|
|
61
|
-
for (const child of node) walk(child, visit, parent);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const n = node;
|
|
65
|
-
if (typeof n.type !== "string") return;
|
|
66
|
-
if (visit(n, parent) === false) return;
|
|
67
|
-
for (const key of Object.keys(n)) {
|
|
68
|
-
if (key === "loc" || key === "leadingComments" || key === "trailingComments" || key === "innerComments" || key === "extra") continue;
|
|
69
|
-
const value = n[key];
|
|
70
|
-
if (value && typeof value === "object") walk(value, visit, n);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
function isComponentName(name) {
|
|
74
|
-
return /^[A-Z]/.test(name) || name.includes(".");
|
|
75
|
-
}
|
|
12
|
+
importSpecifier,
|
|
13
|
+
posix,
|
|
14
|
+
resolveAliasPath
|
|
15
|
+
} from "./chunk-WU7NBTJH.js";
|
|
16
|
+
import {
|
|
17
|
+
ICON_LIBRARIES,
|
|
18
|
+
iconLibrary,
|
|
19
|
+
loadIcons,
|
|
20
|
+
searchIcons
|
|
21
|
+
} from "./chunk-C4SATRZT.js";
|
|
76
22
|
|
|
77
23
|
// src/core/parse.ts
|
|
78
24
|
function parseCanvas(name, file, source) {
|
|
@@ -102,8 +48,8 @@ function parseCanvas(name, file, source) {
|
|
|
102
48
|
let frameIndex = 0;
|
|
103
49
|
for (const child of canvasNode.children) {
|
|
104
50
|
if (child.kind !== "component" || child.name !== "Frame") continue;
|
|
105
|
-
const
|
|
106
|
-
reindex(child,
|
|
51
|
+
const path14 = [frameIndex++];
|
|
52
|
+
reindex(child, path14);
|
|
107
53
|
frames.push({
|
|
108
54
|
...child,
|
|
109
55
|
frameName: str(child.props.name) ?? `Frame ${frameIndex}`,
|
|
@@ -134,6 +80,7 @@ function readImports(ast) {
|
|
|
134
80
|
typeOnly,
|
|
135
81
|
// value bindings only: `import type` and `{ type X }` don't exist at runtime
|
|
136
82
|
names: typeOnly ? [] : [...named.filter((s) => s.importKind !== "type").map((s) => s.local.name), ...ns ? [ns.local.name] : []],
|
|
83
|
+
bindings: typeOnly ? [] : named.filter((s) => s.importKind !== "type").map((s) => ({ imported: s.imported.type === "Identifier" ? s.imported.name : s.imported.value, local: s.local.name })),
|
|
137
84
|
defaultName: def ? def.local.name : null,
|
|
138
85
|
start: stmt.start,
|
|
139
86
|
end: stmt.end,
|
|
@@ -153,9 +100,9 @@ function topLevelNames(ast) {
|
|
|
153
100
|
}
|
|
154
101
|
return out;
|
|
155
102
|
}
|
|
156
|
-
function reindex(node,
|
|
157
|
-
node.path =
|
|
158
|
-
node.children.forEach((c, i) => reindex(c, [...
|
|
103
|
+
function reindex(node, path14) {
|
|
104
|
+
node.path = path14;
|
|
105
|
+
node.children.forEach((c, i) => reindex(c, [...path14, i]));
|
|
159
106
|
}
|
|
160
107
|
function str(v) {
|
|
161
108
|
return v?.kind === "string" ? v.value : void 0;
|
|
@@ -163,10 +110,10 @@ function str(v) {
|
|
|
163
110
|
function num(v) {
|
|
164
111
|
return v?.kind === "number" ? v.value : void 0;
|
|
165
112
|
}
|
|
166
|
-
function buildNode(el, source,
|
|
113
|
+
function buildNode(el, source, path14) {
|
|
167
114
|
const children = [];
|
|
168
115
|
for (const child of el.children) {
|
|
169
|
-
const built = buildChild(child, source, [...
|
|
116
|
+
const built = buildChild(child, source, [...path14, children.length]);
|
|
170
117
|
if (built) children.push(built);
|
|
171
118
|
}
|
|
172
119
|
if (el.type === "JSXFragment") {
|
|
@@ -181,7 +128,7 @@ function buildNode(el, source, path12) {
|
|
|
181
128
|
end: el.end,
|
|
182
129
|
line: el.loc.start.line,
|
|
183
130
|
col: el.loc.start.column,
|
|
184
|
-
path:
|
|
131
|
+
path: path14,
|
|
185
132
|
attrs: [],
|
|
186
133
|
nameEnd: el.openingFragment.end - 1,
|
|
187
134
|
openEnd: el.openingFragment.end,
|
|
@@ -216,7 +163,7 @@ function buildNode(el, source, path12) {
|
|
|
216
163
|
end: el.end,
|
|
217
164
|
line: open.loc.start.line,
|
|
218
165
|
col: open.loc.start.column,
|
|
219
|
-
path:
|
|
166
|
+
path: path14,
|
|
220
167
|
attrs,
|
|
221
168
|
// after type arguments: `<Select<Opt> …>`
|
|
222
169
|
nameEnd: (open.typeParameters ?? open.name).end,
|
|
@@ -225,27 +172,27 @@ function buildNode(el, source, path12) {
|
|
|
225
172
|
closeStart: el.closingElement ? el.closingElement.start : open.end
|
|
226
173
|
};
|
|
227
174
|
}
|
|
228
|
-
function buildChild(child, source,
|
|
229
|
-
if (child.type === "JSXElement" || child.type === "JSXFragment") return buildNode(child, source,
|
|
175
|
+
function buildChild(child, source, path14) {
|
|
176
|
+
if (child.type === "JSXElement" || child.type === "JSXFragment") return buildNode(child, source, path14);
|
|
230
177
|
if (child.type === "JSXText") {
|
|
231
178
|
const text = child.value.replace(/\s+/g, " ").trim();
|
|
232
179
|
if (!text) return null;
|
|
233
|
-
return leaf("text", text, child,
|
|
180
|
+
return leaf("text", text, child, path14, { text });
|
|
234
181
|
}
|
|
235
182
|
if (child.type === "JSXExpressionContainer") {
|
|
236
183
|
const expr = child.expression;
|
|
237
184
|
if (expr.type === "JSXEmptyExpression") return null;
|
|
238
|
-
if (expr.type === "StringLiteral") return leaf("text", expr.value, child,
|
|
185
|
+
if (expr.type === "StringLiteral") return leaf("text", expr.value, child, path14, { text: expr.value });
|
|
239
186
|
if (expr.type === "TemplateLiteral" && expr.expressions.length === 0) {
|
|
240
187
|
const text = expr.quasis.map((q) => q.value.cooked ?? "").join("");
|
|
241
|
-
return leaf("text", text, child,
|
|
188
|
+
return leaf("text", text, child, path14, { text });
|
|
242
189
|
}
|
|
243
|
-
return leaf("expression", source.slice(expr.start, expr.end), child,
|
|
190
|
+
return leaf("expression", source.slice(expr.start, expr.end), child, path14, {});
|
|
244
191
|
}
|
|
245
|
-
if (child.type === "JSXSpreadChild") return leaf("expression", source.slice(child.start, child.end), child,
|
|
192
|
+
if (child.type === "JSXSpreadChild") return leaf("expression", source.slice(child.start, child.end), child, path14, {});
|
|
246
193
|
return null;
|
|
247
194
|
}
|
|
248
|
-
function leaf(kind, name, node,
|
|
195
|
+
function leaf(kind, name, node, path14, extra) {
|
|
249
196
|
return {
|
|
250
197
|
id: `${kind[0]}${idOf(node)}`,
|
|
251
198
|
kind,
|
|
@@ -258,7 +205,7 @@ function leaf(kind, name, node, path12, extra) {
|
|
|
258
205
|
end: node.end,
|
|
259
206
|
line: node.loc.start.line,
|
|
260
207
|
col: node.loc.start.column,
|
|
261
|
-
path:
|
|
208
|
+
path: path14,
|
|
262
209
|
attrs: [],
|
|
263
210
|
nameEnd: node.end,
|
|
264
211
|
openEnd: node.end,
|
|
@@ -322,10 +269,10 @@ function findNode(doc, id) {
|
|
|
322
269
|
function findFrame(doc, ref) {
|
|
323
270
|
return doc.frames.find((f) => f.id === ref || f.frameName === ref) ?? null;
|
|
324
271
|
}
|
|
325
|
-
function findByPath(doc,
|
|
326
|
-
if (!
|
|
327
|
-
let node = doc.frames[
|
|
328
|
-
for (const i of
|
|
272
|
+
function findByPath(doc, path14) {
|
|
273
|
+
if (!path14.length) return null;
|
|
274
|
+
let node = doc.frames[path14[0]];
|
|
275
|
+
for (const i of path14.slice(1)) node = node?.children[i];
|
|
329
276
|
return node ?? null;
|
|
330
277
|
}
|
|
331
278
|
|
|
@@ -418,7 +365,8 @@ var CanvasEditor = class {
|
|
|
418
365
|
} else if (node.openEnd === node.closeStart) {
|
|
419
366
|
this.s.appendLeft(node.openEnd, code);
|
|
420
367
|
} else {
|
|
421
|
-
this.
|
|
368
|
+
const comments = this.source.slice(node.openEnd, node.closeStart).match(/\{\s*\/\*[\s\S]*?\*\/\s*\}/g) ?? [];
|
|
369
|
+
this.s.overwrite(node.openEnd, node.closeStart, comments.join("") + code);
|
|
422
370
|
}
|
|
423
371
|
}
|
|
424
372
|
// ---------- indentation helpers: edits match the file's existing style ----------
|
|
@@ -454,13 +402,20 @@ var CanvasEditor = class {
|
|
|
454
402
|
}
|
|
455
403
|
return this.lineIndent(parent.start) + this.unit();
|
|
456
404
|
}
|
|
457
|
-
/**
|
|
405
|
+
/**
|
|
406
|
+
* Re-indents lines 2..n of a snippet so it sits at `indent`. Lines that start
|
|
407
|
+
* inside a multi-line string or template literal are content, not code: they
|
|
408
|
+
* are kept exactly as written.
|
|
409
|
+
*/
|
|
458
410
|
shift(code, indent) {
|
|
459
|
-
const
|
|
460
|
-
const
|
|
411
|
+
const trimmed = code.trim();
|
|
412
|
+
const lines = trimmed.split("\n");
|
|
413
|
+
const frozen = literalLines(trimmed);
|
|
414
|
+
const free = (i) => !frozen.has(i);
|
|
415
|
+
const rest = lines.slice(1).filter((l, i) => l.trim() && free(i + 1));
|
|
461
416
|
const base = rest.length ? Math.min(...rest.map((l) => /^[ \t]*/.exec(l)[0].length)) : 0;
|
|
462
|
-
const relative = lines.slice(1).map((l) => l.trim() ? l.slice(base) : "");
|
|
463
|
-
const steps = relative.map((l) => /^ */.exec(l)[0].length).filter((n) => n > 0);
|
|
417
|
+
const relative = lines.slice(1).map((l, i) => free(i + 1) ? l.trim() ? l.slice(base) : "" : l);
|
|
418
|
+
const steps = relative.filter((_, i) => free(i + 1)).map((l) => /^ */.exec(l)[0].length).filter((n) => n > 0);
|
|
464
419
|
const from = steps.length ? Math.min(...steps) : 0;
|
|
465
420
|
const unit = this.unit();
|
|
466
421
|
const convert = (l) => {
|
|
@@ -468,7 +423,7 @@ var CanvasEditor = class {
|
|
|
468
423
|
const n = /^ */.exec(l)[0].length;
|
|
469
424
|
return unit.repeat(Math.floor(n / from)) + " ".repeat(n % from) + l.slice(n);
|
|
470
425
|
};
|
|
471
|
-
return [lines[0], ...relative.map((l) => l ? indent + convert(l) : "")].join("\n");
|
|
426
|
+
return [lines[0], ...relative.map((l, i) => !free(i + 1) ? l : l ? indent + convert(l) : "")].join("\n");
|
|
472
427
|
}
|
|
473
428
|
openToClose(node, inner) {
|
|
474
429
|
let end = node.openEnd - 2;
|
|
@@ -532,16 +487,18 @@ ${pi}`);
|
|
|
532
487
|
this.placed.push({ x: right + 80, y: f.y, width: f.width, height: h });
|
|
533
488
|
code = rewriteFrameProps(code, { name, x: right + 80 });
|
|
534
489
|
}
|
|
490
|
+
const marked = withMarker(code.trim(), node);
|
|
535
491
|
this.s.appendLeft(this.contentEnd(node), `
|
|
536
|
-
${this.lineIndent(node.start)}${
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
492
|
+
${this.lineIndent(node.start)}${marked}`);
|
|
493
|
+
if (marked === code.trim()) {
|
|
494
|
+
const path14 = [...node.path];
|
|
495
|
+
path14[path14.length - 1] += 1;
|
|
496
|
+
this.selectPaths.push(path14);
|
|
497
|
+
}
|
|
540
498
|
}
|
|
541
499
|
move(node, target, index) {
|
|
542
500
|
if (isInside(target, node)) throw new EditError("Cannot move a node into itself.");
|
|
543
|
-
const
|
|
544
|
-
const code = node.kind === "component" || node.kind === "element" ? raw.replace(/^<([\w.:-]+)/, "<$1 __tcsel") : raw;
|
|
501
|
+
const code = withMarker(this.source.slice(this.contentStart(node), this.contentEnd(node)), node);
|
|
545
502
|
const kids = target.children.filter((c) => c.id !== node.id);
|
|
546
503
|
const i = index === void 0 || index < 0 || index > kids.length ? kids.length : index;
|
|
547
504
|
this.remove(node);
|
|
@@ -583,7 +540,7 @@ ${ind}${close}`);
|
|
|
583
540
|
this.remove(node);
|
|
584
541
|
return;
|
|
585
542
|
}
|
|
586
|
-
const inner = this.source.slice(
|
|
543
|
+
const inner = node.selfClosing ? "" : this.source.slice(node.openEnd, node.closeStart).trim();
|
|
587
544
|
this.s.overwrite(this.contentStart(node), this.contentEnd(node), this.shift(inner, this.lineIndent(this.contentStart(node))));
|
|
588
545
|
this.selectPaths.push(node.path);
|
|
589
546
|
}
|
|
@@ -601,8 +558,36 @@ ${ind}${close}`);
|
|
|
601
558
|
this.s.appendLeft(same.lastSpecifierEnd, `, ${name}`);
|
|
602
559
|
return;
|
|
603
560
|
}
|
|
561
|
+
this.addImportLine(`import { ${name} } from ${JSON.stringify(from)};`);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Imports `name` from `from` and returns the local name to write in JSX. When
|
|
565
|
+
* the name is already taken by something else (another import, a local
|
|
566
|
+
* component), it is imported under `alias` instead: `{ Search as SearchIcon }`.
|
|
567
|
+
*/
|
|
568
|
+
importAs(name, from, alias) {
|
|
569
|
+
const existing = this.doc.imports.find((i) => i.source === from && !i.typeOnly)?.bindings.find((b) => b.imported === name);
|
|
570
|
+
if (existing) return existing.local;
|
|
571
|
+
const key = `${from}\0${name}`;
|
|
572
|
+
const pending = this.importedAs.get(key);
|
|
573
|
+
if (pending) return pending;
|
|
574
|
+
const taken = (n) => this.doc.declared.includes(n) || this.added.has(n) || this.doc.imports.some((i) => i.names.includes(n) || i.defaultName === n);
|
|
575
|
+
let local = name;
|
|
576
|
+
if (taken(local)) {
|
|
577
|
+
local = alias;
|
|
578
|
+
for (let k = 2; taken(local); k++) local = `${alias}${k}`;
|
|
579
|
+
}
|
|
580
|
+
this.added.add(local);
|
|
581
|
+
this.importedAs.set(key, local);
|
|
582
|
+
const specifier = local === name ? name : `${name} as ${local}`;
|
|
583
|
+
const same = this.doc.imports.find((i) => i.source === from && !i.typeOnly && i.lastSpecifierEnd !== null);
|
|
584
|
+
if (same) this.s.appendLeft(same.lastSpecifierEnd, `, ${specifier}`);
|
|
585
|
+
else this.addImportLine(`import { ${specifier} } from ${JSON.stringify(from)};`);
|
|
586
|
+
return local;
|
|
587
|
+
}
|
|
588
|
+
importedAs = /* @__PURE__ */ new Map();
|
|
589
|
+
addImportLine(line) {
|
|
604
590
|
const last = this.doc.imports[this.doc.imports.length - 1];
|
|
605
|
-
const line = `import { ${name} } from ${JSON.stringify(from)};`;
|
|
606
591
|
if (last) this.s.appendLeft(last.end, `
|
|
607
592
|
${line}`);
|
|
608
593
|
else if (this.doc.prologueEnd) this.s.appendLeft(this.doc.prologueEnd, `
|
|
@@ -622,12 +607,7 @@ ${line}`);
|
|
|
622
607
|
const known = /* @__PURE__ */ new Set([...this.doc.imports.flatMap((i) => [...i.names, ...i.defaultName ? [i.defaultName] : []]), ...this.added]);
|
|
623
608
|
if (names.length && names.every((n) => known.has(n))) return;
|
|
624
609
|
for (const n of names) this.added.add(n);
|
|
625
|
-
|
|
626
|
-
if (last) this.s.appendLeft(last.end, `
|
|
627
|
-
${statement}`);
|
|
628
|
-
else if (this.doc.prologueEnd) this.s.appendLeft(this.doc.prologueEnd, `
|
|
629
|
-
${statement}`);
|
|
630
|
-
else this.prependImport(statement);
|
|
610
|
+
this.addImportLine(statement);
|
|
631
611
|
}
|
|
632
612
|
importsPrepended = false;
|
|
633
613
|
/** First imports of a file: in order, with a blank line before the code. */
|
|
@@ -683,6 +663,43 @@ function componentNamesIn(jsx) {
|
|
|
683
663
|
});
|
|
684
664
|
return [...names];
|
|
685
665
|
}
|
|
666
|
+
function withMarker(code, node) {
|
|
667
|
+
if (node.kind !== "component" && node.kind !== "element") return code;
|
|
668
|
+
const m = /^<[\w.:-]+/.exec(code);
|
|
669
|
+
if (!m) return code;
|
|
670
|
+
let i = m[0].length;
|
|
671
|
+
if (code[i] === "<") {
|
|
672
|
+
for (let depth = 0; i < code.length; i++) {
|
|
673
|
+
if (code[i] === "<") depth++;
|
|
674
|
+
else if (code[i] === ">" && --depth === 0) {
|
|
675
|
+
i++;
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return `${code.slice(0, i)} __tcsel${code.slice(i)}`;
|
|
681
|
+
}
|
|
682
|
+
function literalLines(code) {
|
|
683
|
+
const out = /* @__PURE__ */ new Set();
|
|
684
|
+
if (!code.includes("\n") || !/[`"']/.test(code)) return out;
|
|
685
|
+
let ast;
|
|
686
|
+
try {
|
|
687
|
+
ast = parseModule(`<>${code}</>`);
|
|
688
|
+
} catch {
|
|
689
|
+
return out;
|
|
690
|
+
}
|
|
691
|
+
const ranges = [];
|
|
692
|
+
walk(ast, (n) => {
|
|
693
|
+
if (n.type === "TemplateLiteral" || n.type === "StringLiteral") ranges.push([n.start - 2, n.end - 2]);
|
|
694
|
+
});
|
|
695
|
+
if (!ranges.length) return out;
|
|
696
|
+
let offset = 0;
|
|
697
|
+
code.split("\n").forEach((line, i) => {
|
|
698
|
+
if (i > 0 && ranges.some(([a, b]) => offset > a && offset < b)) out.add(i);
|
|
699
|
+
offset += line.length + 1;
|
|
700
|
+
});
|
|
701
|
+
return out;
|
|
702
|
+
}
|
|
686
703
|
function parseJsx(jsx) {
|
|
687
704
|
try {
|
|
688
705
|
return parseModule(`<>${jsx}</>`);
|
|
@@ -724,6 +741,237 @@ function findDevice(id) {
|
|
|
724
741
|
return id ? DEVICES.find((d) => d.id === id) : void 0;
|
|
725
742
|
}
|
|
726
743
|
|
|
744
|
+
// src/core/scaffold.ts
|
|
745
|
+
import fs from "fs";
|
|
746
|
+
import path from "path";
|
|
747
|
+
var CANVAS_SUFFIX = ".canvas.tsx";
|
|
748
|
+
function canvasFiles(config) {
|
|
749
|
+
const dir = path.join(config.root, config.canvasDir);
|
|
750
|
+
if (!fs.existsSync(dir)) return [];
|
|
751
|
+
return fs.readdirSync(dir).filter((f) => f.endsWith(CANVAS_SUFFIX)).sort((a, b) => canvasNameOf(a).localeCompare(canvasNameOf(b)));
|
|
752
|
+
}
|
|
753
|
+
function canvasNameOf(file) {
|
|
754
|
+
return path.basename(file).slice(0, -CANVAS_SUFFIX.length);
|
|
755
|
+
}
|
|
756
|
+
function assertCanvasName(name) {
|
|
757
|
+
if (!/^[a-z0-9][a-z0-9-_]*$/i.test(name)) throw new Error(`Invalid canvas name "${name}". Use letters, digits, - and _.`);
|
|
758
|
+
return name;
|
|
759
|
+
}
|
|
760
|
+
function canvasPath(config, name) {
|
|
761
|
+
assertCanvasName(name);
|
|
762
|
+
return path.join(config.root, config.canvasDir, `${name}${CANVAS_SUFFIX}`);
|
|
763
|
+
}
|
|
764
|
+
function writeIfChanged(file, content) {
|
|
765
|
+
if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === content) return false;
|
|
766
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
767
|
+
fs.writeFileSync(file, content);
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
function routeDirOf(config) {
|
|
771
|
+
return path.join(config.root, config.routeDir);
|
|
772
|
+
}
|
|
773
|
+
function syncRoute(config) {
|
|
774
|
+
const routeDir = routeDirOf(config);
|
|
775
|
+
const registryFile = path.join(routeDir, "canvases.tsx");
|
|
776
|
+
const canvasDirAbs = path.join(config.root, config.canvasDir);
|
|
777
|
+
let rel = posix(path.relative(routeDir, canvasDirAbs));
|
|
778
|
+
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
779
|
+
const compareDir = path.join(routeDir, "compare");
|
|
780
|
+
const compares = fs.existsSync(compareDir) ? fs.readdirSync(compareDir).filter((f) => f.endsWith(CANVAS_SUFFIX)) : [];
|
|
781
|
+
const entries = [
|
|
782
|
+
...canvasFiles(config).map((f) => ` ${JSON.stringify(canvasNameOf(f))}: lazy(() => import(${JSON.stringify(`${rel}/${f.replace(/\.tsx$/, "")}`)})),`),
|
|
783
|
+
// older versions rendered for Compare
|
|
784
|
+
...compares.map((f) => ` ${JSON.stringify(`__compare__${canvasNameOf(f)}`)}: lazy(() => import(${JSON.stringify(`./compare/${f.replace(/\.tsx$/, "")}`)})),`)
|
|
785
|
+
].join("\n");
|
|
786
|
+
const registry = `"use client";
|
|
787
|
+
/* eslint-disable */
|
|
788
|
+
// Generated by Truecanvas. Do not edit: it is regenerated when canvases are added or removed.
|
|
789
|
+
import { lazy } from "react";
|
|
790
|
+
import { TruecanvasHost, type HostProps } from "truecanvas/host";
|
|
791
|
+
import { components } from "./components";
|
|
792
|
+
|
|
793
|
+
const canvases = {
|
|
794
|
+
${entries}
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
export default function Canvases(props: Omit<HostProps, "canvases" | "components">) {
|
|
798
|
+
return <TruecanvasHost {...props} canvases={canvases} components={components} />;
|
|
799
|
+
}
|
|
800
|
+
`;
|
|
801
|
+
const dm = config.darkMode;
|
|
802
|
+
const componentsFile = path.join(routeDir, "components.tsx");
|
|
803
|
+
if (!fs.existsSync(componentsFile)) writeIfChanged(componentsFile, componentRegistry(config, []));
|
|
804
|
+
const a = writeIfChanged(registryFile, registry);
|
|
805
|
+
if (config.framework === "vite") return writeIfChanged(path.join(routeDir, "entry.tsx"), viteEntry(config)) || a;
|
|
806
|
+
const page = `/* eslint-disable */
|
|
807
|
+
// Generated by Truecanvas. Dev-only route that renders canvas frames.
|
|
808
|
+
import { notFound } from "next/navigation";
|
|
809
|
+
import Canvases from "../canvases";
|
|
810
|
+
|
|
811
|
+
export const dynamic = "force-dynamic";
|
|
812
|
+
|
|
813
|
+
export default async function TruecanvasFrame({
|
|
814
|
+
params,
|
|
815
|
+
searchParams,
|
|
816
|
+
}: {
|
|
817
|
+
params: Promise<{ canvas: string }>;
|
|
818
|
+
searchParams: Promise<Record<string, string | undefined>>;
|
|
819
|
+
}) {
|
|
820
|
+
if (process.env.NODE_ENV !== "development") notFound();
|
|
821
|
+
const { canvas } = await params;
|
|
822
|
+
const query = await searchParams;
|
|
823
|
+
return (
|
|
824
|
+
<Canvases
|
|
825
|
+
canvas={canvas}
|
|
826
|
+
frame={query.frame ?? null}
|
|
827
|
+
theme={(query.theme as "light" | "dark" | "system" | undefined) ?? "system"}
|
|
828
|
+
darkMode={${JSON.stringify(dm)}}
|
|
829
|
+
/>
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
`;
|
|
833
|
+
return writeIfChanged(path.join(routeDir, "[canvas]", "page.tsx"), page) || a;
|
|
834
|
+
}
|
|
835
|
+
function viteEntry(config) {
|
|
836
|
+
const css = (config.css ?? viteStylesheets(config.root)).map((f) => f.startsWith("/") ? f : f.startsWith("./") ? f.slice(1) : fs.existsSync(path.join(config.root, f)) ? `/${f}` : f);
|
|
837
|
+
return `/* eslint-disable */
|
|
838
|
+
// Generated by Truecanvas. Dev-only entry that renders canvas frames at /truecanvas/<canvas>.
|
|
839
|
+
${css.map((f) => `import ${JSON.stringify(f)};`).join("\n")}
|
|
840
|
+
import { createRoot } from "react-dom/client";
|
|
841
|
+
import Canvases from "./canvases";
|
|
842
|
+
|
|
843
|
+
const query = new URLSearchParams(location.search);
|
|
844
|
+
const canvas = decodeURIComponent(location.pathname.split("/")[2] ?? "");
|
|
845
|
+
|
|
846
|
+
createRoot(document.getElementById("tc-root")!).render(
|
|
847
|
+
<Canvases
|
|
848
|
+
canvas={canvas}
|
|
849
|
+
frame={query.get("frame")}
|
|
850
|
+
theme={(query.get("theme") as "light" | "dark" | "system" | null) ?? "system"}
|
|
851
|
+
darkMode={${JSON.stringify(config.darkMode)}}
|
|
852
|
+
/>,
|
|
853
|
+
);
|
|
854
|
+
`;
|
|
855
|
+
}
|
|
856
|
+
function viteStylesheets(root) {
|
|
857
|
+
let html = "";
|
|
858
|
+
try {
|
|
859
|
+
html = fs.readFileSync(path.join(root, "index.html"), "utf8");
|
|
860
|
+
} catch {
|
|
861
|
+
return [];
|
|
862
|
+
}
|
|
863
|
+
const src = /<script[^>]*type=["']module["'][^>]*src=["']([^"']+)["']/i.exec(html)?.[1] ?? /<script[^>]*src=["']([^"']+)["'][^>]*type=["']module["']/i.exec(html)?.[1];
|
|
864
|
+
if (!src || /^https?:/.test(src)) return [];
|
|
865
|
+
const entry = path.join(root, src.replace(/^\//, ""));
|
|
866
|
+
let code = "";
|
|
867
|
+
try {
|
|
868
|
+
code = fs.readFileSync(entry, "utf8");
|
|
869
|
+
} catch {
|
|
870
|
+
return [];
|
|
871
|
+
}
|
|
872
|
+
const out = [];
|
|
873
|
+
for (const m of code.matchAll(/^\s*import\s+["']([^"']+\.(?:css|scss|sass|less|styl|pcss))["']/gm)) {
|
|
874
|
+
const spec = m[1];
|
|
875
|
+
out.push(spec.startsWith(".") ? `/${posix(path.relative(root, path.resolve(path.dirname(entry), spec)))}` : spec);
|
|
876
|
+
}
|
|
877
|
+
return out;
|
|
878
|
+
}
|
|
879
|
+
var STARTER_CANVAS = `"use client";
|
|
880
|
+
import { Canvas, Frame } from "truecanvas";
|
|
881
|
+
|
|
882
|
+
/*
|
|
883
|
+
* A placeholder page. Replace the Welcome frame with your own components:
|
|
884
|
+
* drag them from Assets, draw frames with F, or ask your agent over MCP.
|
|
885
|
+
* Inline styles keep it readable in any app, with or without Tailwind.
|
|
886
|
+
*/
|
|
887
|
+
export default function StarterCanvas() {
|
|
888
|
+
return (
|
|
889
|
+
<Canvas>
|
|
890
|
+
<Frame name="Welcome" x={0} y={0} width={1200} height={760}>
|
|
891
|
+
<div
|
|
892
|
+
style={{
|
|
893
|
+
height: "100%",
|
|
894
|
+
display: "flex",
|
|
895
|
+
alignItems: "center",
|
|
896
|
+
justifyContent: "center",
|
|
897
|
+
fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif",
|
|
898
|
+
color: "light-dark(#1d1d1b, #ededea)",
|
|
899
|
+
backgroundColor: "light-dark(#f6f5f2, #121211)",
|
|
900
|
+
backgroundImage: "radial-gradient(light-dark(rgba(0,0,0,.07), rgba(255,255,255,.06)) 1px, transparent 1px), radial-gradient(circle at 50% 38%, rgba(255,138,92,.16), transparent 55%)",
|
|
901
|
+
backgroundSize: "22px 22px, 100% 100%",
|
|
902
|
+
}}
|
|
903
|
+
>
|
|
904
|
+
<div
|
|
905
|
+
style={{
|
|
906
|
+
width: 520,
|
|
907
|
+
padding: 40,
|
|
908
|
+
borderRadius: 24,
|
|
909
|
+
backgroundColor: "light-dark(#ffffff, #1b1b1a)",
|
|
910
|
+
border: "1px solid light-dark(#e9e8e4, #2a2a28)",
|
|
911
|
+
boxShadow: "0 1px 2px rgba(0,0,0,.04), 0 24px 60px -24px rgba(0,0,0,.25)",
|
|
912
|
+
}}
|
|
913
|
+
>
|
|
914
|
+
<svg width="44" height="44" viewBox="0 0 64 64" aria-hidden>
|
|
915
|
+
<defs>
|
|
916
|
+
<linearGradient id="tc-starter" x1="0" y1="0" x2="0" y2="1">
|
|
917
|
+
<stop offset="0" stopColor="#ff9466" />
|
|
918
|
+
<stop offset="1" stopColor="#ea6a3c" />
|
|
919
|
+
</linearGradient>
|
|
920
|
+
</defs>
|
|
921
|
+
<rect width="64" height="64" rx="15" fill="url(#tc-starter)" />
|
|
922
|
+
<path fill="#f6f5f1" fillRule="evenodd" d="M10.5 10.5h43v13.1h-43z M25.5 16.8h13v36.7h-13z" />
|
|
923
|
+
</svg>
|
|
924
|
+
<h1 style={{ margin: "22px 0 6px", fontSize: 26, lineHeight: "32px", fontWeight: 650, letterSpacing: "-0.02em" }}>Your canvas is ready</h1>
|
|
925
|
+
<p style={{ margin: 0, fontSize: 15, lineHeight: "23px", color: "light-dark(#6b6b66, #a3a39d)" }}>
|
|
926
|
+
Everything here is real code from your app. Replace this frame with your own components.
|
|
927
|
+
</p>
|
|
928
|
+
<div style={{ marginTop: 28, display: "flex", flexDirection: "column", gap: 14 }}>
|
|
929
|
+
<div style={{ display: "flex", alignItems: "center", gap: 14, fontSize: 14 }}>
|
|
930
|
+
<span style={{ minWidth: 58, textAlign: "center", padding: "3px 8px", borderRadius: 7, fontSize: 12, fontWeight: 600, backgroundColor: "light-dark(#f3f2ee, #262624)", border: "1px solid light-dark(#e3e1dc, #34342f)" }}>Assets</span>
|
|
931
|
+
<span>Drag a component from your design system onto a frame</span>
|
|
932
|
+
</div>
|
|
933
|
+
<div style={{ display: "flex", alignItems: "center", gap: 14, fontSize: 14 }}>
|
|
934
|
+
<span style={{ minWidth: 58, textAlign: "center", padding: "3px 8px", borderRadius: 7, fontSize: 12, fontWeight: 600, backgroundColor: "light-dark(#f3f2ee, #262624)", border: "1px solid light-dark(#e3e1dc, #34342f)" }}>F</span>
|
|
935
|
+
<span>Draw a frame, or pick an iPhone or desktop size</span>
|
|
936
|
+
</div>
|
|
937
|
+
<div style={{ display: "flex", alignItems: "center", gap: 14, fontSize: 14 }}>
|
|
938
|
+
<span style={{ minWidth: 58, textAlign: "center", padding: "3px 8px", borderRadius: 7, fontSize: 12, fontWeight: 600, color: "#fff", backgroundColor: "#ee7445", border: "1px solid #e0652f" }}>Agent</span>
|
|
939
|
+
<span>Ask: \u201CBuild a pricing page with our components\u201D</span>
|
|
940
|
+
</div>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
</div>
|
|
944
|
+
</Frame>
|
|
945
|
+
</Canvas>
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
`;
|
|
949
|
+
function componentRegistry(config, specs) {
|
|
950
|
+
const routeDir = routeDirOf(config);
|
|
951
|
+
const lines = specs.map((c) => {
|
|
952
|
+
let from = c.library;
|
|
953
|
+
if (!from) {
|
|
954
|
+
from = posix(path.relative(routeDir, path.join(config.root, c.file))).replace(/\.(tsx|ts|jsx|js)$/, "");
|
|
955
|
+
if (!from.startsWith(".")) from = `./${from}`;
|
|
956
|
+
}
|
|
957
|
+
return ` ${JSON.stringify(c.name)}: lazy(() => import(${JSON.stringify(from)}).then((m) => ({ default: m[${JSON.stringify(c.name)}] }))),`;
|
|
958
|
+
});
|
|
959
|
+
return `"use client";
|
|
960
|
+
/* eslint-disable */
|
|
961
|
+
// Generated by Truecanvas. Do not edit: it is regenerated when components change.
|
|
962
|
+
import { lazy, type ComponentType } from "react";
|
|
963
|
+
|
|
964
|
+
// any: components with required props are rendered with whatever the preview passes
|
|
965
|
+
export const components: Record<string, ComponentType<any>> = {
|
|
966
|
+
${lines.join("\n")}
|
|
967
|
+
};
|
|
968
|
+
`;
|
|
969
|
+
}
|
|
970
|
+
function syncComponentRegistry(config, specs) {
|
|
971
|
+
const file = path.join(routeDirOf(config), "components.tsx");
|
|
972
|
+
return writeIfChanged(file, componentRegistry(config, specs));
|
|
973
|
+
}
|
|
974
|
+
|
|
727
975
|
// src/core/comments.ts
|
|
728
976
|
import fs2 from "fs";
|
|
729
977
|
import path2 from "path";
|
|
@@ -734,7 +982,7 @@ var Comments = class {
|
|
|
734
982
|
}
|
|
735
983
|
config;
|
|
736
984
|
file(canvas) {
|
|
737
|
-
return path2.join(this.config.root, this.config.canvasDir, `${canvas}.comments.json`);
|
|
985
|
+
return path2.join(this.config.root, this.config.canvasDir, `${assertCanvasName(canvas)}.comments.json`);
|
|
738
986
|
}
|
|
739
987
|
list(canvas) {
|
|
740
988
|
try {
|
|
@@ -755,8 +1003,9 @@ var Comments = class {
|
|
|
755
1003
|
}
|
|
756
1004
|
update(canvas, id, fn) {
|
|
757
1005
|
const threads = this.list(canvas);
|
|
758
|
-
const
|
|
759
|
-
|
|
1006
|
+
const prefixed = id.length >= 4 ? threads.filter((x) => x.id.startsWith(id)) : [];
|
|
1007
|
+
const t = threads.find((x) => x.id === id) ?? (prefixed.length === 1 ? prefixed[0] : void 0);
|
|
1008
|
+
if (!t) throw new Error(prefixed.length > 1 ? `"${id}" matches several comment threads. Use the full id.` : `No comment thread "${id}".`);
|
|
760
1009
|
fn(t);
|
|
761
1010
|
this.save(canvas, threads);
|
|
762
1011
|
return t;
|
|
@@ -833,6 +1082,10 @@ function run(cmd, cwd, args, timeout, opts = {}) {
|
|
|
833
1082
|
if (opts.input !== void 0) child.stdin?.end(opts.input);
|
|
834
1083
|
});
|
|
835
1084
|
}
|
|
1085
|
+
function safeRef(ref) {
|
|
1086
|
+
if (!ref || ref.startsWith("-") || !/^[\w./~^@{}+-]+$/.test(ref) || ref.includes("..")) throw new GitError(`Invalid git ref "${ref}".`);
|
|
1087
|
+
return ref;
|
|
1088
|
+
}
|
|
836
1089
|
var Git = class {
|
|
837
1090
|
constructor(cwd) {
|
|
838
1091
|
this.cwd = cwd;
|
|
@@ -845,9 +1098,12 @@ var Git = class {
|
|
|
845
1098
|
} catch {
|
|
846
1099
|
return { repo: false, root: null, branch: null, unborn: false, upstream: null, ahead: 0, behind: 0, files: [], user: null, defaultBranch: null, remote: null };
|
|
847
1100
|
}
|
|
848
|
-
const out = await git(this.cwd, ["status", "--porcelain=v2", "--branch", "--untracked-files=all", "--", "."]);
|
|
1101
|
+
const out = await git(this.cwd, ["status", "--porcelain=v2", "-z", "--branch", "--untracked-files=all", "--", "."]);
|
|
1102
|
+
const prefix = await this.prefix();
|
|
849
1103
|
const s = { repo: true, root, branch: null, unborn: false, upstream: null, ahead: 0, behind: 0, files: [], user: null, defaultBranch: null, remote: null };
|
|
850
|
-
|
|
1104
|
+
const entries = out.split("\0");
|
|
1105
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1106
|
+
const line = entries[i];
|
|
851
1107
|
if (line.startsWith("# branch.head ")) s.branch = line.slice(14) === "(detached)" ? null : line.slice(14);
|
|
852
1108
|
else if (line.startsWith("# branch.oid ")) s.unborn = line.slice(13) === "(initial)";
|
|
853
1109
|
else if (line.startsWith("# branch.upstream ")) s.upstream = line.slice(18);
|
|
@@ -857,10 +1113,13 @@ var Git = class {
|
|
|
857
1113
|
} else if (line.startsWith("1 ") || line.startsWith("2 ")) {
|
|
858
1114
|
const parts = line.split(" ");
|
|
859
1115
|
const xy = parts[1];
|
|
860
|
-
const file = line.startsWith("2 ") ?
|
|
1116
|
+
const file = parts.slice(line.startsWith("2 ") ? 9 : 8).join(" ");
|
|
1117
|
+
if (line.startsWith("2 ")) i++;
|
|
861
1118
|
const code = xy.replace(".", "")[0] ?? "M";
|
|
862
|
-
s.files.push({ path: this.rel(
|
|
863
|
-
} else if (line.startsWith("
|
|
1119
|
+
s.files.push({ path: this.rel(prefix, file), status: code === "A" ? "added" : code === "D" ? "deleted" : code === "R" ? "renamed" : "modified", staged: xy[0] !== "." });
|
|
1120
|
+
} else if (line.startsWith("u ")) {
|
|
1121
|
+
s.files.push({ path: this.rel(prefix, line.split(" ").slice(10).join(" ")), status: "conflicted", staged: false });
|
|
1122
|
+
} else if (line.startsWith("? ")) s.files.push({ path: this.rel(prefix, line.slice(2)), status: "untracked", staged: false });
|
|
864
1123
|
}
|
|
865
1124
|
s.user = (await git(this.cwd, ["config", "user.name"]).catch(() => "")).trim() || null;
|
|
866
1125
|
s.remote = (await git(this.cwd, ["remote", "get-url", "origin"]).catch(() => "")).trim() || null;
|
|
@@ -881,11 +1140,12 @@ var Git = class {
|
|
|
881
1140
|
}
|
|
882
1141
|
/** When a commit was made (ms). */
|
|
883
1142
|
async commitAt(ref) {
|
|
884
|
-
const out = await git(this.cwd, ["log", "-1", "--format=%ct", ref]).catch(() => "");
|
|
1143
|
+
const out = await git(this.cwd, ["log", "-1", "--format=%ct", safeRef(ref), "--"]).catch(() => "");
|
|
885
1144
|
return Number(out.trim() || 0) * 1e3;
|
|
886
1145
|
}
|
|
887
1146
|
/** Commit where this branch left `base`. */
|
|
888
1147
|
async mergeBase(base) {
|
|
1148
|
+
safeRef(base);
|
|
889
1149
|
for (const ref of [`origin/${base}`, base]) {
|
|
890
1150
|
const out = await git(this.cwd, ["merge-base", ref, "HEAD"]).catch(() => "");
|
|
891
1151
|
if (out.trim()) return out.trim();
|
|
@@ -894,7 +1154,7 @@ var Git = class {
|
|
|
894
1154
|
}
|
|
895
1155
|
/** Files changed between a commit and HEAD, from the project root. */
|
|
896
1156
|
async changedSince(ref) {
|
|
897
|
-
const out = await git(this.cwd, ["diff", "--name-only", "--relative", `${ref}...HEAD
|
|
1157
|
+
const out = await git(this.cwd, ["diff", "--name-only", "--relative", `${safeRef(ref)}...HEAD`, "--"]).catch(() => "");
|
|
898
1158
|
return out.split("\n").filter(Boolean);
|
|
899
1159
|
}
|
|
900
1160
|
/** Unified diff of these files against HEAD, including new files. */
|
|
@@ -911,9 +1171,10 @@ var Git = class {
|
|
|
911
1171
|
* (used for PR screenshots), and pushes it. Returns the branch tip.
|
|
912
1172
|
*/
|
|
913
1173
|
async commitToSideBranch(branch, files, message) {
|
|
1174
|
+
safeRef(branch);
|
|
914
1175
|
const tip = (await git(this.cwd, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]).catch(() => "")).trim() || (await git(this.cwd, ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${branch}`]).catch(() => "")).trim();
|
|
915
1176
|
const os = await import("os");
|
|
916
|
-
const
|
|
1177
|
+
const fs13 = await import("fs");
|
|
917
1178
|
const index = path3.join(os.tmpdir(), `truecanvas-index-${process.pid}-${Date.now()}`);
|
|
918
1179
|
const env = { GIT_INDEX_FILE: index };
|
|
919
1180
|
try {
|
|
@@ -928,7 +1189,7 @@ var Git = class {
|
|
|
928
1189
|
await git(this.cwd, ["update-ref", `refs/heads/${branch}`, commit]);
|
|
929
1190
|
await git(this.cwd, ["push", "origin", `refs/heads/${branch}:refs/heads/${branch}`], 12e4);
|
|
930
1191
|
} finally {
|
|
931
|
-
|
|
1192
|
+
fs13.rmSync(index, { force: true });
|
|
932
1193
|
}
|
|
933
1194
|
}
|
|
934
1195
|
// ---------- GitHub (gh CLI) ----------
|
|
@@ -952,7 +1213,7 @@ var Git = class {
|
|
|
952
1213
|
return { number: pr.number, url: pr.url, title: pr.title, state: pr.state, draft: pr.isDraft, review: pr.reviewDecision || null, checks };
|
|
953
1214
|
}
|
|
954
1215
|
async createPullRequest(opts) {
|
|
955
|
-
const out = await run("gh", this.cwd, ["pr", "create", "--title", opts.title, "--body-file", "-", "--base", opts.base, ...opts.draft ? ["--draft"] : []], 12e4, { input: opts.body });
|
|
1216
|
+
const out = await run("gh", this.cwd, ["pr", "create", "--title", opts.title, "--body-file", "-", "--base", safeRef(opts.base), ...opts.draft ? ["--draft"] : []], 12e4, { input: opts.body });
|
|
956
1217
|
return out.trim().split("\n").pop() ?? "";
|
|
957
1218
|
}
|
|
958
1219
|
/** Creates a GitHub repo for this project and pushes it (gh repo create). */
|
|
@@ -962,8 +1223,16 @@ var Git = class {
|
|
|
962
1223
|
return (await git(this.cwd, ["remote", "get-url", "origin"])).trim();
|
|
963
1224
|
}
|
|
964
1225
|
/** git reports paths from the repo root; we show them from the project root. */
|
|
965
|
-
rel(
|
|
966
|
-
return
|
|
1226
|
+
rel(prefix, file) {
|
|
1227
|
+
return file.startsWith(prefix) ? file.slice(prefix.length) : posix(path3.posix.relative(prefix, file));
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* The project folder inside the repo ("apps/web/", or "" at the root), as git
|
|
1231
|
+
* itself sees it. Asking git instead of comparing file system paths holds up
|
|
1232
|
+
* through symlinks (macOS /var → /private/var), junctions and Windows short names.
|
|
1233
|
+
*/
|
|
1234
|
+
async prefix() {
|
|
1235
|
+
return (await git(this.cwd, ["rev-parse", "--show-prefix"]).catch(() => "")).trim();
|
|
967
1236
|
}
|
|
968
1237
|
async branches() {
|
|
969
1238
|
const out = await git(this.cwd, ["branch", "-a", "--format=%(refname)%09%(HEAD)"]).catch(() => "");
|
|
@@ -991,9 +1260,8 @@ var Git = class {
|
|
|
991
1260
|
}
|
|
992
1261
|
/** A file's content at a ref, or null if it doesn't exist there. */
|
|
993
1262
|
async show(ref, file) {
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
return git(this.cwd, ["show", `${ref}:${inRepo}`]).catch(() => null);
|
|
1263
|
+
const inRepo = path3.posix.normalize(`${await this.prefix()}${posix(file)}`);
|
|
1264
|
+
return git(this.cwd, ["show", `${safeRef(ref)}:${inRepo}`, "--"]).catch(() => null);
|
|
997
1265
|
}
|
|
998
1266
|
async commit(message, files) {
|
|
999
1267
|
if (!message.trim()) throw new GitError("Write a commit message.");
|
|
@@ -1015,14 +1283,14 @@ var Git = class {
|
|
|
1015
1283
|
});
|
|
1016
1284
|
}
|
|
1017
1285
|
async switch(branch, create = false) {
|
|
1018
|
-
|
|
1286
|
+
safeRef(branch);
|
|
1019
1287
|
await git(this.cwd, create ? ["switch", "-c", branch] : ["switch", branch]);
|
|
1020
1288
|
}
|
|
1021
1289
|
};
|
|
1022
1290
|
|
|
1023
1291
|
// src/core/workspace.ts
|
|
1024
|
-
import
|
|
1025
|
-
import
|
|
1292
|
+
import fs7 from "fs";
|
|
1293
|
+
import path8 from "path";
|
|
1026
1294
|
import { createRequire } from "module";
|
|
1027
1295
|
|
|
1028
1296
|
// src/core/link.ts
|
|
@@ -1243,6 +1511,7 @@ function parseTargetDoc(file, source) {
|
|
|
1243
1511
|
var PAGE = /^page\.(tsx|jsx|ts|js)$/;
|
|
1244
1512
|
var LAYOUT = ["layout.tsx", "layout.jsx", "layout.js", "layout.ts"];
|
|
1245
1513
|
function listRoutes(config) {
|
|
1514
|
+
if (config.framework !== "next") return [];
|
|
1246
1515
|
const appDir = path4.join(config.root, config.appDir);
|
|
1247
1516
|
const out = [];
|
|
1248
1517
|
const walkDir = (dir, segments) => {
|
|
@@ -1255,7 +1524,7 @@ function listRoutes(config) {
|
|
|
1255
1524
|
for (const e of entries) {
|
|
1256
1525
|
if (e.isFile() && PAGE.test(e.name)) {
|
|
1257
1526
|
const visible = segments.filter((s) => !/^\(.*\)$/.test(s) && !s.startsWith("@"));
|
|
1258
|
-
out.push({ route: `/${visible.join("/")}`, file: path4.relative(config.root, path4.join(dir, e.name)), dynamic: segments.some((s) => s.startsWith("[")) });
|
|
1527
|
+
out.push({ route: `/${visible.join("/")}`, file: posix(path4.relative(config.root, path4.join(dir, e.name))), dynamic: segments.some((s) => s.startsWith("[")) });
|
|
1259
1528
|
}
|
|
1260
1529
|
if (e.isDirectory() && !e.name.startsWith("_") && e.name !== "truecanvas" && e.name !== "api" && e.name !== "node_modules") walkDir(path4.join(dir, e.name), [...segments, e.name]);
|
|
1261
1530
|
}
|
|
@@ -1274,7 +1543,7 @@ function extract(file, canvasFile, config, layout) {
|
|
|
1274
1543
|
if (/\.(css|scss|sass|less)$/.test(src)) continue;
|
|
1275
1544
|
const from = repoint(config, src, file, canvasFile);
|
|
1276
1545
|
const text = source.slice(stmt.start, stmt.end);
|
|
1277
|
-
imports.push(from === src ? text : text
|
|
1546
|
+
imports.push(from === src ? text : rewriteImportSource(text, src, from));
|
|
1278
1547
|
}
|
|
1279
1548
|
return { jsx: source.slice(info.node.start, info.node.end), imports };
|
|
1280
1549
|
}
|
|
@@ -1287,7 +1556,7 @@ function layoutFiles(config, pageFile) {
|
|
|
1287
1556
|
const out = [];
|
|
1288
1557
|
for (let dir = path4.dirname(path4.resolve(config.root, pageFile)); dir.startsWith(appDir) && dir !== appDir; dir = path4.dirname(dir)) {
|
|
1289
1558
|
const file = LAYOUT.map((f) => path4.join(dir, f)).find((f) => fs3.existsSync(f));
|
|
1290
|
-
if (file) out.push(path4.relative(config.root, file));
|
|
1559
|
+
if (file) out.push(posix(path4.relative(config.root, file)));
|
|
1291
1560
|
}
|
|
1292
1561
|
return out;
|
|
1293
1562
|
}
|
|
@@ -1319,7 +1588,8 @@ function routeFrame(config, pageFile, canvasFile) {
|
|
|
1319
1588
|
const layout = extract(file, canvasAbs, config, true);
|
|
1320
1589
|
const holes = layout ? (layout.jsx.match(/\{\s*children\s*\}/g) ?? []).length : 0;
|
|
1321
1590
|
if (layout && holes === 1) {
|
|
1322
|
-
|
|
1591
|
+
const inner = jsx.startsWith("<>") ? jsx.slice(2, -3) : jsx;
|
|
1592
|
+
jsx = layout.jsx.replace(/\{\s*children\s*\}/, () => inner);
|
|
1323
1593
|
imports.push(...layout.imports);
|
|
1324
1594
|
} else {
|
|
1325
1595
|
const local = `RouteLayout${i++ || ""}`;
|
|
@@ -1349,6 +1619,18 @@ function readSource(config, rel) {
|
|
|
1349
1619
|
return null;
|
|
1350
1620
|
}
|
|
1351
1621
|
}
|
|
1622
|
+
var parseMemo = /* @__PURE__ */ new Map();
|
|
1623
|
+
function memoParse(key, source, parse) {
|
|
1624
|
+
let hit = parseMemo.get(key);
|
|
1625
|
+
if (!hit || hit.source !== source) {
|
|
1626
|
+
hit = { source, doc: parse() };
|
|
1627
|
+
parseMemo.set(key, hit);
|
|
1628
|
+
}
|
|
1629
|
+
return hit.doc && { ...hit.doc, frames: hit.doc.frames.map(cloneTree) };
|
|
1630
|
+
}
|
|
1631
|
+
function cloneTree(n) {
|
|
1632
|
+
return { ...n, children: n.children.map(cloneTree) };
|
|
1633
|
+
}
|
|
1352
1634
|
function resolveLinks(config, doc) {
|
|
1353
1635
|
if (!doc.frames.some((f) => f.page || f.component)) return doc;
|
|
1354
1636
|
return { ...doc, frames: doc.frames.map((f) => f.component ? linkComponent(config, f) : f.page ? linkFrame(config, f) : f) };
|
|
@@ -1361,7 +1643,7 @@ function linkComponent(config, original) {
|
|
|
1361
1643
|
const frame = { ...original };
|
|
1362
1644
|
const { file, name } = splitComponentRef(frame.component);
|
|
1363
1645
|
const source = readSource(config, file);
|
|
1364
|
-
const doc = source === null ? null : parseModuleDoc(file, source);
|
|
1646
|
+
const doc = source === null ? null : memoParse(`module:${file}`, source, () => parseModuleDoc(file, source));
|
|
1365
1647
|
const own = doc?.frames.find((f) => f.frameName === name);
|
|
1366
1648
|
if (!own) {
|
|
1367
1649
|
frame.link = { kind: "component", page: file, route: name, files: [], editable: false, reason: source === null ? `${file} doesn't exist anymore.` : `${file} has no exported component ${name} returning JSX.` };
|
|
@@ -1384,7 +1666,7 @@ function linkFrame(config, original) {
|
|
|
1384
1666
|
const chain = wrapperChain(original);
|
|
1385
1667
|
const files = [];
|
|
1386
1668
|
let current;
|
|
1387
|
-
const pageDoc = parseSourceDoc(page, pageSource, false);
|
|
1669
|
+
const pageDoc = memoParse(`page:${page}`, pageSource, () => parseSourceDoc(page, pageSource, false));
|
|
1388
1670
|
if (pageDoc) {
|
|
1389
1671
|
current = flatten(pageDoc.frames[0].children[0]);
|
|
1390
1672
|
files.push(page);
|
|
@@ -1394,7 +1676,7 @@ function linkFrame(config, original) {
|
|
|
1394
1676
|
const layouts = layoutFiles(config, page);
|
|
1395
1677
|
layouts.forEach((rel, i) => {
|
|
1396
1678
|
const src = readSource(config, rel);
|
|
1397
|
-
const ldoc = src === null ? null : parseSourceDoc(rel, src, true);
|
|
1679
|
+
const ldoc = src === null ? null : memoParse(`layout:${rel}`, src, () => parseSourceDoc(rel, src, true));
|
|
1398
1680
|
const root = ldoc?.frames[0].children[0];
|
|
1399
1681
|
const slot = root && findSlot(root);
|
|
1400
1682
|
if (root && slot) {
|
|
@@ -1479,7 +1761,7 @@ function explorationBody(config, frame, canvasRel) {
|
|
|
1479
1761
|
const src = stmt.source.value;
|
|
1480
1762
|
const from = repoint(config, src, pageAbs, path5.join(config.root, canvasRel));
|
|
1481
1763
|
const text = source.slice(stmt.start, stmt.end);
|
|
1482
|
-
imports.push(from === src ? text : text
|
|
1764
|
+
imports.push(from === src ? text : rewriteImportSource(text, src, from));
|
|
1483
1765
|
}
|
|
1484
1766
|
jsx = unwrapFragment(source.slice(info.node.start, info.node.end));
|
|
1485
1767
|
} else {
|
|
@@ -1778,8 +2060,8 @@ export function TextAnimate({ effect = "words", stagger = 0.06, delay = 0, durat
|
|
|
1778
2060
|
function motionFiles(config) {
|
|
1779
2061
|
const glob = config.components[0] ?? "components/**/*.tsx";
|
|
1780
2062
|
const base = glob.split("/").filter((p) => !p.includes("*")).join("/") || "components";
|
|
1781
|
-
const dir =
|
|
1782
|
-
return { reveal:
|
|
2063
|
+
const dir = `${base}/motion`;
|
|
2064
|
+
return { reveal: `${dir}/reveal.tsx`, text: `${dir}/text-animate.tsx` };
|
|
1783
2065
|
}
|
|
1784
2066
|
function ensureMotionComponents(config) {
|
|
1785
2067
|
const files = motionFiles(config);
|
|
@@ -1891,193 +2173,11 @@ ${indentBlock(indentBlock(jsx.trim()))}
|
|
|
1891
2173
|
`;
|
|
1892
2174
|
}
|
|
1893
2175
|
function newComponentPath(config, name) {
|
|
1894
|
-
const rel =
|
|
2176
|
+
const rel = `${componentsDir(config)}/${kebab(name)}.tsx`;
|
|
1895
2177
|
if (fs6.existsSync(path7.join(config.root, rel))) throw new EditError(`${rel} already exists. Pick another name.`);
|
|
1896
2178
|
return rel;
|
|
1897
2179
|
}
|
|
1898
2180
|
|
|
1899
|
-
// src/core/scaffold.ts
|
|
1900
|
-
import fs7 from "fs";
|
|
1901
|
-
import path8 from "path";
|
|
1902
|
-
var CANVAS_SUFFIX = ".canvas.tsx";
|
|
1903
|
-
var ROUTE = "truecanvas";
|
|
1904
|
-
function canvasFiles(config) {
|
|
1905
|
-
const dir = path8.join(config.root, config.canvasDir);
|
|
1906
|
-
if (!fs7.existsSync(dir)) return [];
|
|
1907
|
-
return fs7.readdirSync(dir).filter((f) => f.endsWith(CANVAS_SUFFIX)).sort((a, b) => canvasNameOf(a).localeCompare(canvasNameOf(b)));
|
|
1908
|
-
}
|
|
1909
|
-
function canvasNameOf(file) {
|
|
1910
|
-
return path8.basename(file).slice(0, -CANVAS_SUFFIX.length);
|
|
1911
|
-
}
|
|
1912
|
-
function canvasPath(config, name) {
|
|
1913
|
-
if (!/^[a-z0-9][a-z0-9-_]*$/i.test(name)) throw new Error(`Invalid canvas name "${name}". Use letters, digits, - and _.`);
|
|
1914
|
-
return path8.join(config.root, config.canvasDir, `${name}${CANVAS_SUFFIX}`);
|
|
1915
|
-
}
|
|
1916
|
-
function writeIfChanged(file, content) {
|
|
1917
|
-
if (fs7.existsSync(file) && fs7.readFileSync(file, "utf8") === content) return false;
|
|
1918
|
-
fs7.mkdirSync(path8.dirname(file), { recursive: true });
|
|
1919
|
-
fs7.writeFileSync(file, content);
|
|
1920
|
-
return true;
|
|
1921
|
-
}
|
|
1922
|
-
function syncRoute(config) {
|
|
1923
|
-
const routeDir = path8.join(config.root, config.appDir, ROUTE);
|
|
1924
|
-
const pageFile = path8.join(routeDir, "[canvas]", "page.tsx");
|
|
1925
|
-
const registryFile = path8.join(routeDir, "canvases.tsx");
|
|
1926
|
-
const canvasDirAbs = path8.join(config.root, config.canvasDir);
|
|
1927
|
-
let rel = path8.relative(routeDir, canvasDirAbs);
|
|
1928
|
-
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
1929
|
-
const compareDir = path8.join(routeDir, "compare");
|
|
1930
|
-
const compares = fs7.existsSync(compareDir) ? fs7.readdirSync(compareDir).filter((f) => f.endsWith(CANVAS_SUFFIX)) : [];
|
|
1931
|
-
const entries = [
|
|
1932
|
-
...canvasFiles(config).map((f) => ` ${JSON.stringify(canvasNameOf(f))}: lazy(() => import(${JSON.stringify(`${rel}/${f.replace(/\.tsx$/, "")}`)})),`),
|
|
1933
|
-
// older versions rendered for Compare
|
|
1934
|
-
...compares.map((f) => ` ${JSON.stringify(`__compare__${canvasNameOf(f)}`)}: lazy(() => import(${JSON.stringify(`./compare/${f.replace(/\.tsx$/, "")}`)})),`)
|
|
1935
|
-
].join("\n");
|
|
1936
|
-
const registry = `"use client";
|
|
1937
|
-
/* eslint-disable */
|
|
1938
|
-
// Generated by Truecanvas. Do not edit: it is regenerated when canvases are added or removed.
|
|
1939
|
-
import { lazy } from "react";
|
|
1940
|
-
import { TruecanvasHost, type HostProps } from "truecanvas/host";
|
|
1941
|
-
import { components } from "./components";
|
|
1942
|
-
|
|
1943
|
-
const canvases = {
|
|
1944
|
-
${entries}
|
|
1945
|
-
};
|
|
1946
|
-
|
|
1947
|
-
export default function Canvases(props: Omit<HostProps, "canvases" | "components">) {
|
|
1948
|
-
return <TruecanvasHost {...props} canvases={canvases} components={components} />;
|
|
1949
|
-
}
|
|
1950
|
-
`;
|
|
1951
|
-
const dm = config.darkMode;
|
|
1952
|
-
const page = `/* eslint-disable */
|
|
1953
|
-
// Generated by Truecanvas. Dev-only route that renders canvas frames.
|
|
1954
|
-
import { notFound } from "next/navigation";
|
|
1955
|
-
import Canvases from "../canvases";
|
|
1956
|
-
|
|
1957
|
-
export const dynamic = "force-dynamic";
|
|
1958
|
-
|
|
1959
|
-
export default async function TruecanvasFrame({
|
|
1960
|
-
params,
|
|
1961
|
-
searchParams,
|
|
1962
|
-
}: {
|
|
1963
|
-
params: Promise<{ canvas: string }>;
|
|
1964
|
-
searchParams: Promise<Record<string, string | undefined>>;
|
|
1965
|
-
}) {
|
|
1966
|
-
if (process.env.NODE_ENV !== "development") notFound();
|
|
1967
|
-
const { canvas } = await params;
|
|
1968
|
-
const query = await searchParams;
|
|
1969
|
-
return (
|
|
1970
|
-
<Canvases
|
|
1971
|
-
canvas={canvas}
|
|
1972
|
-
frame={query.frame ?? null}
|
|
1973
|
-
theme={(query.theme as "light" | "dark" | "system" | undefined) ?? "system"}
|
|
1974
|
-
darkMode={${JSON.stringify(dm)}}
|
|
1975
|
-
/>
|
|
1976
|
-
);
|
|
1977
|
-
}
|
|
1978
|
-
`;
|
|
1979
|
-
const componentsFile = path8.join(routeDir, "components.tsx");
|
|
1980
|
-
if (!fs7.existsSync(componentsFile)) writeIfChanged(componentsFile, componentRegistry(config, []));
|
|
1981
|
-
const a = writeIfChanged(registryFile, registry);
|
|
1982
|
-
const b = writeIfChanged(pageFile, page);
|
|
1983
|
-
return a || b;
|
|
1984
|
-
}
|
|
1985
|
-
var STARTER_CANVAS = `"use client";
|
|
1986
|
-
import { Canvas, Frame } from "truecanvas";
|
|
1987
|
-
|
|
1988
|
-
/*
|
|
1989
|
-
* A placeholder page. Replace the Welcome frame with your own components:
|
|
1990
|
-
* drag them from Assets, draw frames with F, or ask your agent over MCP.
|
|
1991
|
-
* Inline styles keep it readable in any app, with or without Tailwind.
|
|
1992
|
-
*/
|
|
1993
|
-
export default function StarterCanvas() {
|
|
1994
|
-
return (
|
|
1995
|
-
<Canvas>
|
|
1996
|
-
<Frame name="Welcome" x={0} y={0} width={1200} height={760}>
|
|
1997
|
-
<div
|
|
1998
|
-
style={{
|
|
1999
|
-
height: "100%",
|
|
2000
|
-
display: "flex",
|
|
2001
|
-
alignItems: "center",
|
|
2002
|
-
justifyContent: "center",
|
|
2003
|
-
fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif",
|
|
2004
|
-
color: "light-dark(#1d1d1b, #ededea)",
|
|
2005
|
-
backgroundColor: "light-dark(#f6f5f2, #121211)",
|
|
2006
|
-
backgroundImage: "radial-gradient(light-dark(rgba(0,0,0,.07), rgba(255,255,255,.06)) 1px, transparent 1px), radial-gradient(circle at 50% 38%, rgba(255,138,92,.16), transparent 55%)",
|
|
2007
|
-
backgroundSize: "22px 22px, 100% 100%",
|
|
2008
|
-
}}
|
|
2009
|
-
>
|
|
2010
|
-
<div
|
|
2011
|
-
style={{
|
|
2012
|
-
width: 520,
|
|
2013
|
-
padding: 40,
|
|
2014
|
-
borderRadius: 24,
|
|
2015
|
-
backgroundColor: "light-dark(#ffffff, #1b1b1a)",
|
|
2016
|
-
border: "1px solid light-dark(#e9e8e4, #2a2a28)",
|
|
2017
|
-
boxShadow: "0 1px 2px rgba(0,0,0,.04), 0 24px 60px -24px rgba(0,0,0,.25)",
|
|
2018
|
-
}}
|
|
2019
|
-
>
|
|
2020
|
-
<svg width="44" height="44" viewBox="0 0 64 64" aria-hidden>
|
|
2021
|
-
<defs>
|
|
2022
|
-
<linearGradient id="tc-starter" x1="0" y1="0" x2="0" y2="1">
|
|
2023
|
-
<stop offset="0" stopColor="#ff9466" />
|
|
2024
|
-
<stop offset="1" stopColor="#ea6a3c" />
|
|
2025
|
-
</linearGradient>
|
|
2026
|
-
</defs>
|
|
2027
|
-
<rect width="64" height="64" rx="15" fill="url(#tc-starter)" />
|
|
2028
|
-
<path fill="#f6f5f1" fillRule="evenodd" d="M10.5 10.5h43v13.1h-43z M25.5 16.8h13v36.7h-13z" />
|
|
2029
|
-
</svg>
|
|
2030
|
-
<h1 style={{ margin: "22px 0 6px", fontSize: 26, lineHeight: "32px", fontWeight: 650, letterSpacing: "-0.02em" }}>Your canvas is ready</h1>
|
|
2031
|
-
<p style={{ margin: 0, fontSize: 15, lineHeight: "23px", color: "light-dark(#6b6b66, #a3a39d)" }}>
|
|
2032
|
-
Everything here is real code from your app. Replace this frame with your own components.
|
|
2033
|
-
</p>
|
|
2034
|
-
<div style={{ marginTop: 28, display: "flex", flexDirection: "column", gap: 14 }}>
|
|
2035
|
-
<div style={{ display: "flex", alignItems: "center", gap: 14, fontSize: 14 }}>
|
|
2036
|
-
<span style={{ minWidth: 58, textAlign: "center", padding: "3px 8px", borderRadius: 7, fontSize: 12, fontWeight: 600, backgroundColor: "light-dark(#f3f2ee, #262624)", border: "1px solid light-dark(#e3e1dc, #34342f)" }}>Assets</span>
|
|
2037
|
-
<span>Drag a component from your design system onto a frame</span>
|
|
2038
|
-
</div>
|
|
2039
|
-
<div style={{ display: "flex", alignItems: "center", gap: 14, fontSize: 14 }}>
|
|
2040
|
-
<span style={{ minWidth: 58, textAlign: "center", padding: "3px 8px", borderRadius: 7, fontSize: 12, fontWeight: 600, backgroundColor: "light-dark(#f3f2ee, #262624)", border: "1px solid light-dark(#e3e1dc, #34342f)" }}>F</span>
|
|
2041
|
-
<span>Draw a frame, or pick an iPhone or desktop size</span>
|
|
2042
|
-
</div>
|
|
2043
|
-
<div style={{ display: "flex", alignItems: "center", gap: 14, fontSize: 14 }}>
|
|
2044
|
-
<span style={{ minWidth: 58, textAlign: "center", padding: "3px 8px", borderRadius: 7, fontSize: 12, fontWeight: 600, color: "#fff", backgroundColor: "#ee7445", border: "1px solid #e0652f" }}>Agent</span>
|
|
2045
|
-
<span>Ask: \u201CBuild a pricing page with our components\u201D</span>
|
|
2046
|
-
</div>
|
|
2047
|
-
</div>
|
|
2048
|
-
</div>
|
|
2049
|
-
</div>
|
|
2050
|
-
</Frame>
|
|
2051
|
-
</Canvas>
|
|
2052
|
-
);
|
|
2053
|
-
}
|
|
2054
|
-
`;
|
|
2055
|
-
function componentRegistry(config, specs) {
|
|
2056
|
-
const routeDir = path8.join(config.root, config.appDir, ROUTE);
|
|
2057
|
-
const lines = specs.map((c) => {
|
|
2058
|
-
let from = c.library;
|
|
2059
|
-
if (!from) {
|
|
2060
|
-
from = path8.relative(routeDir, path8.join(config.root, c.file)).replace(/\.(tsx|ts|jsx|js)$/, "");
|
|
2061
|
-
if (!from.startsWith(".")) from = `./${from}`;
|
|
2062
|
-
}
|
|
2063
|
-
return ` ${JSON.stringify(c.name)}: lazy(() => import(${JSON.stringify(from)}).then((m) => ({ default: m[${JSON.stringify(c.name)}] }))),`;
|
|
2064
|
-
});
|
|
2065
|
-
return `"use client";
|
|
2066
|
-
/* eslint-disable */
|
|
2067
|
-
// Generated by Truecanvas. Do not edit: it is regenerated when components change.
|
|
2068
|
-
import { lazy, type ComponentType } from "react";
|
|
2069
|
-
|
|
2070
|
-
// any: components with required props are rendered with whatever the preview passes
|
|
2071
|
-
export const components: Record<string, ComponentType<any>> = {
|
|
2072
|
-
${lines.join("\n")}
|
|
2073
|
-
};
|
|
2074
|
-
`;
|
|
2075
|
-
}
|
|
2076
|
-
function syncComponentRegistry(config, specs) {
|
|
2077
|
-
const file = path8.join(config.root, config.appDir, ROUTE, "components.tsx");
|
|
2078
|
-
return writeIfChanged(file, componentRegistry(config, specs));
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
2181
|
// src/core/workspace.ts
|
|
2082
2182
|
var require2 = createRequire(import.meta.url);
|
|
2083
2183
|
var MARK = "__tcsel";
|
|
@@ -2094,13 +2194,14 @@ var Workspace = class {
|
|
|
2094
2194
|
this.catalog = new Catalog(config.root, config.components, config.libraries);
|
|
2095
2195
|
this.comments = new Comments(config);
|
|
2096
2196
|
this.git = new Git(config.root);
|
|
2097
|
-
for (const c of this.canvases()) this.sources.set(c,
|
|
2197
|
+
for (const c of this.canvases()) this.sources.set(c, fs7.readFileSync(this.file(c), "utf8"));
|
|
2098
2198
|
}
|
|
2099
2199
|
config;
|
|
2100
2200
|
catalog;
|
|
2101
2201
|
sources = /* @__PURE__ */ new Map();
|
|
2102
2202
|
undoStacks = /* @__PURE__ */ new Map();
|
|
2103
2203
|
redoStacks = /* @__PURE__ */ new Map();
|
|
2204
|
+
/** Activity across canvases: labels only, never file copies. */
|
|
2104
2205
|
feed = [];
|
|
2105
2206
|
seq = 0;
|
|
2106
2207
|
queue = Promise.resolve();
|
|
@@ -2124,29 +2225,52 @@ var Workspace = class {
|
|
|
2124
2225
|
return canvasPath(this.config, canvas);
|
|
2125
2226
|
}
|
|
2126
2227
|
relFile(canvas) {
|
|
2127
|
-
return
|
|
2228
|
+
return posix(path8.relative(this.config.root, this.file(canvas)));
|
|
2128
2229
|
}
|
|
2129
2230
|
read(canvas) {
|
|
2130
2231
|
const file = this.file(canvas);
|
|
2131
|
-
if (!
|
|
2132
|
-
const src =
|
|
2232
|
+
if (!fs7.existsSync(file)) throw new EditError(`Canvas "${canvas}" does not exist. Canvases: ${this.canvases().join(", ") || "none"}`);
|
|
2233
|
+
const src = fs7.readFileSync(file, "utf8");
|
|
2133
2234
|
if (!this.sources.has(canvas)) this.sources.set(canvas, src);
|
|
2134
2235
|
return src;
|
|
2135
2236
|
}
|
|
2136
2237
|
doc(canvas) {
|
|
2137
2238
|
return this.docFrom(canvas, this.read(canvas));
|
|
2138
2239
|
}
|
|
2139
|
-
/**
|
|
2240
|
+
/**
|
|
2241
|
+
* A canvas doc with linked frames resolved to their page's layers. Resolving
|
|
2242
|
+
* reads and parses every linked page and layout, so the result is reused
|
|
2243
|
+
* until the canvas source or one of those files changes (checked with stat).
|
|
2244
|
+
*/
|
|
2140
2245
|
docFrom(canvas, source) {
|
|
2141
|
-
|
|
2246
|
+
const hit = this.docCache.get(canvas);
|
|
2247
|
+
if (hit && hit.source === source && hit.stamps.every(([rel, stamp]) => this.stamp(rel) === stamp)) return { ...hit.doc, rev: ++this.revision };
|
|
2248
|
+
const doc = resolveLinks(this.config, parseCanvas(canvas, this.relFile(canvas), source));
|
|
2249
|
+
const files = new Set(doc.frames.flatMap((f) => [...f.link?.files ?? [], ...f.page ? [f.page] : []]));
|
|
2250
|
+
this.docCache.set(canvas, { source, doc, stamps: [...files].map((rel) => [rel, this.stamp(rel)]) });
|
|
2251
|
+
return { ...doc, rev: ++this.revision };
|
|
2252
|
+
}
|
|
2253
|
+
/** a doc computed later reflects the files at least as recently: its rev is higher */
|
|
2254
|
+
revision = 0;
|
|
2255
|
+
docCache = /* @__PURE__ */ new Map();
|
|
2256
|
+
stamp(rel) {
|
|
2257
|
+
try {
|
|
2258
|
+
const st = fs7.statSync(path8.join(this.config.root, rel));
|
|
2259
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
2260
|
+
} catch {
|
|
2261
|
+
return "missing";
|
|
2262
|
+
}
|
|
2142
2263
|
}
|
|
2143
2264
|
/** Where a node's code lives: the canvas, or a linked page/layout file. */
|
|
2144
2265
|
nodeSource(canvas, id) {
|
|
2145
|
-
const
|
|
2266
|
+
const doc = this.doc(canvas);
|
|
2267
|
+
const hit = findNode(doc, id);
|
|
2146
2268
|
if (!hit) return null;
|
|
2147
|
-
const
|
|
2148
|
-
|
|
2149
|
-
|
|
2269
|
+
const linked = fileOfId(id);
|
|
2270
|
+
if (linked) assertShown(doc, linked);
|
|
2271
|
+
const rel = linked ?? this.relFile(canvas);
|
|
2272
|
+
const abs = path8.join(this.config.root, rel);
|
|
2273
|
+
return { node: hit.node, rel, abs, source: fs7.readFileSync(abs, "utf8") };
|
|
2150
2274
|
}
|
|
2151
2275
|
/** Page and layout files shown by linked frames, across all canvases (the Git panel counts them as design files). */
|
|
2152
2276
|
linkedFiles() {
|
|
@@ -2165,7 +2289,7 @@ var Workspace = class {
|
|
|
2165
2289
|
*/
|
|
2166
2290
|
designFiles() {
|
|
2167
2291
|
const m = motionFiles(this.config);
|
|
2168
|
-
return [...this.linkedFiles(), ...[m.reveal, m.text].filter((f) =>
|
|
2292
|
+
return [...this.linkedFiles(), ...[m.reveal, m.text].filter((f) => fs7.existsSync(path8.join(this.config.root, f)))];
|
|
2169
2293
|
}
|
|
2170
2294
|
/** Canvases with a linked frame showing this page or layout file. */
|
|
2171
2295
|
canvasesLinking(rel) {
|
|
@@ -2181,20 +2305,21 @@ var Workspace = class {
|
|
|
2181
2305
|
linkedFileChanged(rel) {
|
|
2182
2306
|
let next;
|
|
2183
2307
|
try {
|
|
2184
|
-
next =
|
|
2308
|
+
next = fs7.readFileSync(path8.join(this.config.root, rel), "utf8");
|
|
2185
2309
|
} catch {
|
|
2186
2310
|
next = "";
|
|
2187
2311
|
}
|
|
2188
2312
|
const key = `file:${rel}`;
|
|
2189
2313
|
if (this.sources.get(key) === next) return;
|
|
2190
2314
|
this.sources.set(key, next);
|
|
2315
|
+
this.docCache.clear();
|
|
2191
2316
|
for (const c of this.canvasesLinking(rel)) this.emit({ type: "doc", canvas: c, doc: this.doc(c) });
|
|
2192
2317
|
}
|
|
2193
2318
|
createCanvas(name, actor, content = STARTER_CANVAS) {
|
|
2194
2319
|
const file = canvasPath(this.config, name);
|
|
2195
|
-
if (
|
|
2196
|
-
|
|
2197
|
-
|
|
2320
|
+
if (fs7.existsSync(file)) throw new EditError(`Canvas "${name}" already exists.`);
|
|
2321
|
+
fs7.mkdirSync(path8.dirname(file), { recursive: true });
|
|
2322
|
+
fs7.writeFileSync(file, content);
|
|
2198
2323
|
this.sources.set(name, content);
|
|
2199
2324
|
this.canvasesChanged();
|
|
2200
2325
|
this.record(name, `Created canvas ${name}`, actor, "", content, []);
|
|
@@ -2229,10 +2354,10 @@ var Workspace = class {
|
|
|
2229
2354
|
}
|
|
2230
2355
|
renameCanvas(from, to) {
|
|
2231
2356
|
const src = this.file(from);
|
|
2232
|
-
if (!
|
|
2357
|
+
if (!fs7.existsSync(src)) throw new EditError(`Canvas "${from}" does not exist.`);
|
|
2233
2358
|
const dest = canvasPath(this.config, to);
|
|
2234
|
-
if (
|
|
2235
|
-
|
|
2359
|
+
if (fs7.existsSync(dest)) throw new EditError(`A canvas named "${to}" already exists.`);
|
|
2360
|
+
fs7.renameSync(src, dest);
|
|
2236
2361
|
this.comments.renameCanvas(from, to);
|
|
2237
2362
|
for (const map of [this.sources, this.undoStacks, this.redoStacks]) {
|
|
2238
2363
|
if (map.has(from)) {
|
|
@@ -2249,7 +2374,7 @@ var Workspace = class {
|
|
|
2249
2374
|
duplicateCanvas(name) {
|
|
2250
2375
|
const source = this.read(name);
|
|
2251
2376
|
const copy = this.uniqueCanvasName(`${name}-copy`);
|
|
2252
|
-
|
|
2377
|
+
fs7.writeFileSync(canvasPath(this.config, copy), source);
|
|
2253
2378
|
this.sources.set(copy, source);
|
|
2254
2379
|
this.canvasesChanged();
|
|
2255
2380
|
return copy;
|
|
@@ -2258,17 +2383,19 @@ var Workspace = class {
|
|
|
2258
2383
|
trash = /* @__PURE__ */ new Map();
|
|
2259
2384
|
deleteCanvas(name) {
|
|
2260
2385
|
const file = this.file(name);
|
|
2261
|
-
if (!
|
|
2262
|
-
this.trash.set(name,
|
|
2263
|
-
|
|
2386
|
+
if (!fs7.existsSync(file)) throw new EditError(`Canvas "${name}" does not exist.`);
|
|
2387
|
+
this.trash.set(name, fs7.readFileSync(file, "utf8"));
|
|
2388
|
+
fs7.unlinkSync(file);
|
|
2264
2389
|
this.sources.delete(name);
|
|
2390
|
+
this.undoStacks.delete(name);
|
|
2391
|
+
this.redoStacks.delete(name);
|
|
2265
2392
|
this.canvasesChanged();
|
|
2266
2393
|
}
|
|
2267
2394
|
restoreCanvas(name) {
|
|
2268
2395
|
const source = this.trash.get(name);
|
|
2269
2396
|
if (source === void 0) throw new EditError(`Nothing to restore for "${name}".`);
|
|
2270
2397
|
const target = this.uniqueCanvasName(name);
|
|
2271
|
-
|
|
2398
|
+
fs7.writeFileSync(canvasPath(this.config, target), source);
|
|
2272
2399
|
this.sources.set(target, source);
|
|
2273
2400
|
this.trash.delete(name);
|
|
2274
2401
|
this.canvasesChanged();
|
|
@@ -2279,14 +2406,14 @@ var Workspace = class {
|
|
|
2279
2406
|
if (!fileName.endsWith(CANVAS_SUFFIX)) return;
|
|
2280
2407
|
const canvas = canvasNameOf(fileName);
|
|
2281
2408
|
const file = this.file(canvas);
|
|
2282
|
-
if (!
|
|
2409
|
+
if (!fs7.existsSync(file)) {
|
|
2283
2410
|
this.sources.delete(canvas);
|
|
2284
2411
|
syncRoute(this.config);
|
|
2285
2412
|
this.emit({ type: "canvases", canvases: this.canvases() });
|
|
2286
2413
|
return;
|
|
2287
2414
|
}
|
|
2288
2415
|
const prev = this.sources.get(canvas);
|
|
2289
|
-
const next =
|
|
2416
|
+
const next = fs7.readFileSync(file, "utf8");
|
|
2290
2417
|
if (prev === next) return;
|
|
2291
2418
|
this.sources.set(canvas, next);
|
|
2292
2419
|
if (prev === void 0) {
|
|
@@ -2305,7 +2432,8 @@ var Workspace = class {
|
|
|
2305
2432
|
} catch (err) {
|
|
2306
2433
|
if (!(err instanceof ConflictError)) throw err;
|
|
2307
2434
|
if (err.file) this.linkedFileChanged(err.file);
|
|
2308
|
-
else this.externalChange(
|
|
2435
|
+
else this.externalChange(path8.basename(this.file(cmd.canvas)));
|
|
2436
|
+
if (nodeRefs(cmd).length || "frame" in cmd && cmd.frame) throw new EditError(`${path8.basename(err.file ?? this.relFile(cmd.canvas))} changed while editing. Re-read the canvas and try again.`);
|
|
2309
2437
|
return this.exec(cmd, actor);
|
|
2310
2438
|
}
|
|
2311
2439
|
});
|
|
@@ -2314,32 +2442,55 @@ var Workspace = class {
|
|
|
2314
2442
|
}
|
|
2315
2443
|
async exec(cmd, actor) {
|
|
2316
2444
|
cmd = { ...cmd };
|
|
2317
|
-
|
|
2445
|
+
let prepared = null;
|
|
2446
|
+
if (cmd.op === "add_component_frame" && !fs7.existsSync(this.file(cmd.canvas))) {
|
|
2447
|
+
await this.catalog.load();
|
|
2448
|
+
const spec = this.catalog.get(cmd.component);
|
|
2449
|
+
if (!spec) throw new EditError(`Unknown component "${cmd.component}". Use list_components.`);
|
|
2450
|
+
if (spec.library) throw new EditError(`${spec.name} comes from ${spec.library}: it can't be edited here.`);
|
|
2451
|
+
this.createCanvas(cmd.canvas, actor, COMPONENTS_CANVAS);
|
|
2452
|
+
}
|
|
2318
2453
|
const canvasSource = this.read(cmd.canvas);
|
|
2319
2454
|
const canvasDoc = parseCanvas(cmd.canvas, this.relFile(cmd.canvas), canvasSource);
|
|
2320
2455
|
if (canvasDoc.error) throw new EditError(`The canvas file does not parse: ${canvasDoc.error}`);
|
|
2321
2456
|
const view = resolveLinks(this.config, canvasDoc);
|
|
2322
2457
|
cmd = this.routeLinked(cmd, view);
|
|
2323
2458
|
let target = this.targetOf(cmd);
|
|
2459
|
+
if (target) assertShown(view, target);
|
|
2324
2460
|
if (cmd.op === "explore_copy") {
|
|
2325
2461
|
const frame = findFrame(canvasDoc, cmd.frame);
|
|
2326
2462
|
if (!frame) throw new EditError(`Frame "${cmd.frame}" not found.`);
|
|
2327
2463
|
const body = explorationBody(this.config, frame, this.relFile(cmd.canvas));
|
|
2328
|
-
|
|
2464
|
+
prepared = { kind: "explore", frame, imports: body.imports, jsx: await this.formatSnippet(body.jsx) };
|
|
2329
2465
|
}
|
|
2330
2466
|
if (cmd.op === "apply_to_page") {
|
|
2331
2467
|
const frame = findFrame(canvasDoc, cmd.frame);
|
|
2332
2468
|
if (!frame) throw new EditError(`Frame "${cmd.frame}" not found.`);
|
|
2333
2469
|
const plan = applyPlan(this.config, canvasSource, canvasDoc, frame, this.relFile(cmd.canvas));
|
|
2334
2470
|
target = plan.page;
|
|
2335
|
-
|
|
2471
|
+
prepared = { kind: "apply", frame, ...plan };
|
|
2336
2472
|
}
|
|
2337
2473
|
const rel = target ?? this.relFile(cmd.canvas);
|
|
2338
|
-
const file =
|
|
2339
|
-
const
|
|
2340
|
-
const
|
|
2341
|
-
const
|
|
2474
|
+
const file = path8.join(this.config.root, rel);
|
|
2475
|
+
const original = target ? fs7.readFileSync(file, "utf8") : canvasSource;
|
|
2476
|
+
const crlf = original.includes("\r\n");
|
|
2477
|
+
const before = crlf ? original.replace(/\r\n/g, "\n") : original;
|
|
2478
|
+
const parse = (src) => target ? parseTargetDoc(target, src) : parseCanvas(cmd.canvas, rel, src);
|
|
2479
|
+
const doc = parse(before);
|
|
2342
2480
|
if (!doc) throw new EditError(`${rel} has data or logic in its component now, so it can't be edited from the canvas. Explore a copy instead.`);
|
|
2481
|
+
if (cmd.op === "insert_icon") {
|
|
2482
|
+
const icon = cmd;
|
|
2483
|
+
const lib = iconLibrary(icon.library);
|
|
2484
|
+
if (!lib) throw new EditError(`Unknown icon library "${icon.library}". Known: ${ICON_LIBRARIES.map((l) => l.id).join(", ")}.`);
|
|
2485
|
+
const icons = await loadIcons(this.config.root, lib.id);
|
|
2486
|
+
const wanted = icon.name.toLowerCase();
|
|
2487
|
+
const exact = icons.find((i) => i.name === icon.name) ?? icons.find((i) => i.name.toLowerCase() === wanted);
|
|
2488
|
+
if (!exact) {
|
|
2489
|
+
const near = searchIcons(icons, icon.name, 5).icons.map((i) => i.name);
|
|
2490
|
+
throw new EditError(`No icon "${icon.name}" in ${lib.label}.${near.length ? ` Did you mean ${near.join(", ")}?` : " Use search_icons."}`);
|
|
2491
|
+
}
|
|
2492
|
+
cmd = { ...icon, library: lib.id, name: exact.name };
|
|
2493
|
+
}
|
|
2343
2494
|
if ((cmd.op === "insert_jsx" || cmd.op === "replace" || cmd.op === "create_frame") && cmd.jsx) {
|
|
2344
2495
|
cmd = { ...cmd, jsx: await this.formatSnippet(cmd.jsx) };
|
|
2345
2496
|
}
|
|
@@ -2349,16 +2500,16 @@ var Workspace = class {
|
|
|
2349
2500
|
if (!r) throw new EditError(`No page "${cmd.route}". Pages: ${routes.map((x) => x.route).join(", ") || "none"}`);
|
|
2350
2501
|
if (cmd.copy) {
|
|
2351
2502
|
const built = routeFrame(this.config, r.file, this.relFile(cmd.canvas));
|
|
2352
|
-
|
|
2503
|
+
prepared = { kind: "route", route: r.route, page: null, imports: built.imports, mode: built.mode, jsx: await this.formatSnippet(built.jsx) };
|
|
2353
2504
|
} else {
|
|
2354
2505
|
const built = linkedFrameBody(this.config, canvasDoc, this.relFile(cmd.canvas), r.file);
|
|
2355
|
-
|
|
2506
|
+
prepared = { kind: "route", route: r.route, page: r.file, imports: built.imports, mode: "linked", jsx: built.jsx };
|
|
2356
2507
|
}
|
|
2357
2508
|
}
|
|
2358
2509
|
if (cmd.op === "add_animation") {
|
|
2359
2510
|
const files = ensureMotionComponents(this.config);
|
|
2360
2511
|
if (files.created) this.catalog.invalidate();
|
|
2361
|
-
|
|
2512
|
+
prepared = { kind: "motion", files };
|
|
2362
2513
|
}
|
|
2363
2514
|
let newComponent = null;
|
|
2364
2515
|
let movedNames = /* @__PURE__ */ new Set();
|
|
@@ -2379,15 +2530,15 @@ var Workspace = class {
|
|
|
2379
2530
|
imports = importsFor(this.config, jsx, before, doc, rel, fileRel);
|
|
2380
2531
|
movedNames = new Set(componentNamesIn(jsx));
|
|
2381
2532
|
}
|
|
2382
|
-
const abs =
|
|
2533
|
+
const abs = path8.join(this.config.root, fileRel);
|
|
2383
2534
|
newComponent = { rel: fileRel, content: await this.format(componentFile(name, jsx, imports), abs).catch(() => componentFile(name, jsx, imports)) };
|
|
2384
|
-
|
|
2535
|
+
prepared = { kind: "component", name, file: fileRel };
|
|
2385
2536
|
}
|
|
2386
2537
|
await this.catalog.load().catch(() => []);
|
|
2387
2538
|
const ed = new CanvasEditor(before, doc);
|
|
2388
2539
|
const renamedFrame = cmd.op === "update_frame" && cmd.name !== void 0 ? findFrame(doc, cmd.frame) : null;
|
|
2389
2540
|
const usedBefore = cmd.op === "apply_to_page" ? new Set(componentNamesIn(before.slice(doc.frames[0].children[0].start, doc.frames[0].children[0].end))) : null;
|
|
2390
|
-
const label = this.apply(ed, cmd, rel);
|
|
2541
|
+
const label = this.apply(ed, cmd, rel, prepared);
|
|
2391
2542
|
const pretty = await this.isPrettierClean(before, file);
|
|
2392
2543
|
const finish = async (code) => pretty ? this.format(code, file) : validate(code);
|
|
2393
2544
|
let result = ed.result();
|
|
@@ -2397,8 +2548,8 @@ var Workspace = class {
|
|
|
2397
2548
|
let after = await finish(result);
|
|
2398
2549
|
let ids = [];
|
|
2399
2550
|
const reparse = (src) => {
|
|
2400
|
-
const d =
|
|
2401
|
-
if (!d) throw new EditError(`That edit would put logic into ${
|
|
2551
|
+
const d = parse(src);
|
|
2552
|
+
if (!d) throw new EditError(`That edit would put logic into ${path8.basename(rel)}. Only plain JSX can be edited from the canvas.`);
|
|
2402
2553
|
return d;
|
|
2403
2554
|
};
|
|
2404
2555
|
if (after.includes(MARK)) {
|
|
@@ -2421,19 +2572,25 @@ var Workspace = class {
|
|
|
2421
2572
|
ids = paths.map((p) => pathId(clean, p)).filter(Boolean);
|
|
2422
2573
|
}
|
|
2423
2574
|
if (cmd.op === "apply_to_page") ids = [findFrame(canvasDoc, cmd.frame).id];
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
fs8.writeFileSync(path9.join(this.config.root, newComponent.rel), newComponent.content, { flag: "wx" });
|
|
2575
|
+
const writeNewComponent = () => {
|
|
2576
|
+
if (!newComponent) return;
|
|
2577
|
+
fs7.mkdirSync(path8.dirname(path8.join(this.config.root, newComponent.rel)), { recursive: true });
|
|
2578
|
+
fs7.writeFileSync(path8.join(this.config.root, newComponent.rel), newComponent.content, { flag: "wx" });
|
|
2429
2579
|
this.catalog.invalidate();
|
|
2430
2580
|
this.emit({ type: "catalog" });
|
|
2581
|
+
};
|
|
2582
|
+
if (crlf) after = after.replace(/\n/g, "\r\n");
|
|
2583
|
+
if (after === original) {
|
|
2584
|
+
writeNewComponent();
|
|
2585
|
+
return { doc: target ? this.doc(cmd.canvas) : view, ids, label };
|
|
2431
2586
|
}
|
|
2587
|
+
if (fs7.readFileSync(file, "utf8") !== original) throw new ConflictError(`${path8.basename(rel)} changed during the edit.`, target ?? void 0);
|
|
2588
|
+
writeNewComponent();
|
|
2432
2589
|
if (target) this.sources.set(`file:${target}`, after);
|
|
2433
2590
|
else this.sources.set(cmd.canvas, after);
|
|
2434
|
-
|
|
2591
|
+
fs7.writeFileSync(file, after);
|
|
2435
2592
|
const newDoc = this.doc(cmd.canvas);
|
|
2436
|
-
this.record(cmd.canvas, label, actor,
|
|
2593
|
+
this.record(cmd.canvas, label, actor, original, after, ids, target ?? void 0, touchedFrames(cmd, ids, view, newDoc));
|
|
2437
2594
|
if (renamedFrame) {
|
|
2438
2595
|
const now = newDoc.frames[renamedFrame.path[0]];
|
|
2439
2596
|
if (now && now.frameName !== renamedFrame.frameName) {
|
|
@@ -2457,7 +2614,7 @@ var Workspace = class {
|
|
|
2457
2614
|
const f = findFrame(view, cmd.parent);
|
|
2458
2615
|
if (f?.link) {
|
|
2459
2616
|
if (!f.link.editable) throw viewOnly(f);
|
|
2460
|
-
const src =
|
|
2617
|
+
const src = fs7.readFileSync(path8.join(this.config.root, f.link.page), "utf8");
|
|
2461
2618
|
if (f.link.kind === "component") {
|
|
2462
2619
|
const own = parseModuleDoc(f.link.page, src)?.frames.find((x) => x.frameName === f.link.route);
|
|
2463
2620
|
if (!own) throw viewOnly(f);
|
|
@@ -2482,21 +2639,60 @@ var Workspace = class {
|
|
|
2482
2639
|
const refs = nodeRefs(cmd);
|
|
2483
2640
|
const files = new Set(refs.map(fileOfId));
|
|
2484
2641
|
if (files.size > 1) {
|
|
2485
|
-
const names = [...files].map((f) => f ?
|
|
2642
|
+
const names = [...files].map((f) => f ? path8.basename(f) : "the canvas");
|
|
2486
2643
|
throw new EditError(`These layers live in different files (${names.join(", ")}). Edit them one file at a time.`);
|
|
2487
2644
|
}
|
|
2488
2645
|
return [...files][0] ?? null;
|
|
2489
2646
|
}
|
|
2490
|
-
|
|
2647
|
+
/** Applies a command to the file being edited and returns its history label. */
|
|
2648
|
+
apply(ed, cmd, rel, prepared) {
|
|
2649
|
+
switch (cmd.op) {
|
|
2650
|
+
case "set_props":
|
|
2651
|
+
case "set_text":
|
|
2652
|
+
case "set_class":
|
|
2653
|
+
case "insert_jsx":
|
|
2654
|
+
case "insert_component":
|
|
2655
|
+
case "insert_icon":
|
|
2656
|
+
case "replace":
|
|
2657
|
+
case "duplicate":
|
|
2658
|
+
case "delete":
|
|
2659
|
+
case "move":
|
|
2660
|
+
case "reorder":
|
|
2661
|
+
case "wrap":
|
|
2662
|
+
return this.applyLayerEdit(ed, cmd, rel, prepared);
|
|
2663
|
+
case "create_frame":
|
|
2664
|
+
case "update_frame":
|
|
2665
|
+
case "add_background":
|
|
2666
|
+
case "apply_preset":
|
|
2667
|
+
case "create_variants":
|
|
2668
|
+
case "add_component_frame":
|
|
2669
|
+
return this.applyFrameEdit(ed, cmd, rel, prepared);
|
|
2670
|
+
case "import_route":
|
|
2671
|
+
case "explore_copy":
|
|
2672
|
+
case "apply_to_page":
|
|
2673
|
+
return this.applyPageEdit(ed, cmd, rel, prepared);
|
|
2674
|
+
case "add_animation":
|
|
2675
|
+
case "remove_animation":
|
|
2676
|
+
return this.applyMotionEdit(ed, cmd, rel, prepared);
|
|
2677
|
+
case "create_component":
|
|
2678
|
+
return this.applyComponentEdit(ed, cmd, rel, prepared);
|
|
2679
|
+
default: {
|
|
2680
|
+
const unknown = cmd;
|
|
2681
|
+
throw new EditError(`Unknown command ${unknown.op}.`);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
/** Edits to layers: props, text, classes, inserting, moving, wrapping. */
|
|
2686
|
+
applyLayerEdit(ed, cmd, rel, prepared) {
|
|
2491
2687
|
const doc = ed.doc;
|
|
2492
2688
|
const guard = (node, verb) => {
|
|
2493
2689
|
if (doc.canvasNode) return;
|
|
2494
|
-
if (node.path.length <= 2) throw new EditError(`Can't ${verb} the root element of ${
|
|
2495
|
-
if (node.kind === "expression" && node.name.trim() === "children" && /^layout\.[jt]sx?$/.test(
|
|
2690
|
+
if (node.path.length <= 2) throw new EditError(`Can't ${verb} the root element of ${path8.basename(rel)}.`);
|
|
2691
|
+
if (node.kind === "expression" && node.name.trim() === "children" && /^layout\.[jt]sx?$/.test(path8.basename(rel))) throw new EditError(`Can't ${verb} the {children} slot of ${path8.basename(rel)}: that's where the page renders.`);
|
|
2496
2692
|
};
|
|
2497
2693
|
const literalOnly = (node, key) => {
|
|
2498
2694
|
const v = node.props[key];
|
|
2499
|
-
if (!doc.canvasNode && v?.kind === "expression") throw new EditError(`${key} on ${node.name} is computed in code ({${short(v.code, 50)}}). Edit it in ${
|
|
2695
|
+
if (!doc.canvasNode && v?.kind === "expression") throw new EditError(`${key} on ${node.name} is computed in code ({${short(v.code, 50)}}). Edit it in ${path8.basename(rel)}, or ask your agent.`);
|
|
2500
2696
|
};
|
|
2501
2697
|
switch (cmd.op) {
|
|
2502
2698
|
case "set_props": {
|
|
@@ -2545,6 +2741,14 @@ var Workspace = class {
|
|
|
2545
2741
|
ed.insertChild(parent, cmd.index, jsx);
|
|
2546
2742
|
return `Inserted ${spec.name}`;
|
|
2547
2743
|
}
|
|
2744
|
+
case "insert_icon": {
|
|
2745
|
+
const parent = ed.container(cmd.parent);
|
|
2746
|
+
const lib = iconLibrary(cmd.library);
|
|
2747
|
+
const local = ed.importAs(cmd.name, lib.from, `${cmd.name.replace(/Icon$/, "")}Icon`);
|
|
2748
|
+
const className = cmd.className?.trim() ?? "size-4";
|
|
2749
|
+
ed.insertChild(parent, cmd.index, `<${local}${className ? ` className=${JSON.stringify(className)}` : ""} />`);
|
|
2750
|
+
return `Inserted ${cmd.name} icon`;
|
|
2751
|
+
}
|
|
2548
2752
|
case "replace": {
|
|
2549
2753
|
const { node } = ed.node(cmd.id);
|
|
2550
2754
|
parseJsx(cmd.jsx);
|
|
@@ -2553,7 +2757,8 @@ var Workspace = class {
|
|
|
2553
2757
|
return `Replaced ${node.name}`;
|
|
2554
2758
|
}
|
|
2555
2759
|
case "duplicate": {
|
|
2556
|
-
const
|
|
2760
|
+
const all = cmd.ids.map((id) => ed.node(id));
|
|
2761
|
+
const nodes = all.filter(({ node: n }) => !all.some(({ node: o }) => o !== n && n.start >= o.start && n.end <= o.end));
|
|
2557
2762
|
for (const { node } of nodes) guard(node, "duplicate");
|
|
2558
2763
|
for (const { node } of nodes) ed.duplicate(node);
|
|
2559
2764
|
return nodes.length === 1 ? `Duplicated ${nodes[0].node.kind === "component" && "frameName" in nodes[0].node ? "frame" : nodes[0].node.name}` : `Duplicated ${nodes.length} layers`;
|
|
@@ -2590,11 +2795,19 @@ var Workspace = class {
|
|
|
2590
2795
|
ed.wrap(nodes, `<div className="${cls}">`, "</div>");
|
|
2591
2796
|
return nodes.length === 1 ? `Wrapped ${nodes[0].name} in auto layout` : `Wrapped ${nodes.length} layers in auto layout`;
|
|
2592
2797
|
}
|
|
2798
|
+
default:
|
|
2799
|
+
throw new Error(`Internal: ${cmd.op} isn't handled by applyLayerEdit.`);
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
/** Frames: creating, updating, backgrounds, presets, variant grids, main component frames. */
|
|
2803
|
+
applyFrameEdit(ed, cmd, rel, prepared) {
|
|
2804
|
+
const doc = ed.doc;
|
|
2805
|
+
switch (cmd.op) {
|
|
2593
2806
|
case "create_frame": {
|
|
2594
2807
|
const device = findDevice(cmd.device);
|
|
2595
2808
|
if (cmd.device && !device) throw new EditError(`Unknown device "${cmd.device}". Devices: ${DEVICES.map((d) => d.id).join(", ")}`);
|
|
2596
2809
|
const name = uniqueFrameName(doc, cmd.name ?? device?.name ?? "Frame");
|
|
2597
|
-
const right = doc
|
|
2810
|
+
const right = rightEdge(doc);
|
|
2598
2811
|
const top = doc.frames.length ? Math.min(...doc.frames.map((f) => f.y)) : 0;
|
|
2599
2812
|
if (cmd.jsx) {
|
|
2600
2813
|
parseJsx(cmd.jsx);
|
|
@@ -2684,19 +2897,68 @@ var Workspace = class {
|
|
|
2684
2897
|
ed.selectPaths.push(node.path);
|
|
2685
2898
|
return `Applied ${preset.name} preset to ${spec.name}`;
|
|
2686
2899
|
}
|
|
2900
|
+
case "create_variants": {
|
|
2901
|
+
const spec = this.catalog.get(cmd.component);
|
|
2902
|
+
if (!spec) throw new EditError(`Unknown component "${cmd.component}".`);
|
|
2903
|
+
const prop = spec.props.find((p) => p.name === cmd.prop);
|
|
2904
|
+
if (!prop) throw new EditError(`${spec.name} has no prop "${cmd.prop}".`);
|
|
2905
|
+
const values = prop.type === "enum" ? prop.options : prop.type === "boolean" ? [false, true] : [];
|
|
2906
|
+
if (!values.length) throw new EditError(`Prop "${cmd.prop}" is ${prop.typeText}; variants need a union of string literals or a boolean.`);
|
|
2907
|
+
ed.ensureImport(spec.name, importSpecifier(this.config.root, rel, spec.file));
|
|
2908
|
+
const label = (v) => typeof v === "string" ? v[0].toUpperCase() + v.slice(1) : `${prop.name}: ${v}`;
|
|
2909
|
+
const rows = indentBlock(values.map((v) => componentJsx(spec, { ...cmd.props, [prop.name]: v }, spec.acceptsChildren ? label(v) : void 0)).join("\n"));
|
|
2910
|
+
const right = rightEdge(doc);
|
|
2911
|
+
const name = uniqueFrameName(doc, `${spec.name} \xB7 ${prop.name}`);
|
|
2912
|
+
ed.insertFrame(frameCode({ name, x: cmd.x ?? right + 80, y: cmd.y ?? 0, width: 360, body: `<div className="flex flex-col gap-3 p-4">
|
|
2913
|
+
${rows}
|
|
2914
|
+
</div>` }));
|
|
2915
|
+
return `Created ${spec.name} ${prop.name} variants`;
|
|
2916
|
+
}
|
|
2917
|
+
case "add_component_frame": {
|
|
2918
|
+
const spec = this.catalog.get(cmd.component);
|
|
2919
|
+
if (!spec) throw new EditError(`Unknown component "${cmd.component}". Use list_components.`);
|
|
2920
|
+
if (spec.library) throw new EditError(`${spec.name} comes from ${spec.library}: it can't be edited here.`);
|
|
2921
|
+
const existing = doc.frames.find((f) => f.component === `${spec.file}#${spec.name}`);
|
|
2922
|
+
if (existing) {
|
|
2923
|
+
ed.selectPaths.push(existing.path);
|
|
2924
|
+
return `Opened ${spec.name}`;
|
|
2925
|
+
}
|
|
2926
|
+
ed.ensureImport(spec.name, importSpecifier(this.config.root, rel, spec.file));
|
|
2927
|
+
const right = rightEdge(doc);
|
|
2928
|
+
const top = doc.frames.length ? Math.min(...doc.frames.map((f) => f.y)) : 0;
|
|
2929
|
+
ed.insertFrame(
|
|
2930
|
+
frameCode({
|
|
2931
|
+
name: uniqueFrameName(doc, spec.name),
|
|
2932
|
+
x: cmd.x ?? right + 80,
|
|
2933
|
+
y: cmd.y ?? top,
|
|
2934
|
+
width: cmd.width ?? 640,
|
|
2935
|
+
component: `${spec.file}#${spec.name}`,
|
|
2936
|
+
body: `<div className="p-10">
|
|
2937
|
+
${componentJsx(spec)}
|
|
2938
|
+
</div>`
|
|
2939
|
+
})
|
|
2940
|
+
);
|
|
2941
|
+
return `Opened main component ${spec.name}`;
|
|
2942
|
+
}
|
|
2943
|
+
default:
|
|
2944
|
+
throw new Error(`Internal: ${cmd.op} isn't handled by applyFrameEdit.`);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
/** Linked pages: adding a page, exploring a copy, applying it back. */
|
|
2948
|
+
applyPageEdit(ed, cmd, rel, prepared) {
|
|
2949
|
+
const doc = ed.doc;
|
|
2950
|
+
switch (cmd.op) {
|
|
2687
2951
|
case "import_route": {
|
|
2688
|
-
const built =
|
|
2689
|
-
this.pending = null;
|
|
2952
|
+
const built = take(prepared, "route");
|
|
2690
2953
|
for (const stmt of built.imports) ed.addImportStatement(stmt);
|
|
2691
|
-
const right = doc
|
|
2954
|
+
const right = rightEdge(doc);
|
|
2692
2955
|
const top = doc.frames.length ? Math.min(...doc.frames.map((f) => f.y)) : 0;
|
|
2693
2956
|
const name = uniqueFrameName(doc, cmd.name ?? (built.route === "/" ? "Home" : built.route.split("/").filter(Boolean).map((p) => p.replace(/^./, (c) => c.toUpperCase())).join(" / ")));
|
|
2694
2957
|
ed.insertFrame(frameCode({ name, x: cmd.x ?? right + 80, y: cmd.y ?? top, width: cmd.width ?? 1440, height: null, page: built.page ?? void 0, body: built.jsx }));
|
|
2695
2958
|
return built.mode === "linked" ? `Linked ${built.route}` : built.mode === "inline" ? `Copied ${built.route} as layers` : `Copied ${built.route}`;
|
|
2696
2959
|
}
|
|
2697
2960
|
case "explore_copy": {
|
|
2698
|
-
const built =
|
|
2699
|
-
this.pending = null;
|
|
2961
|
+
const built = take(prepared, "explore");
|
|
2700
2962
|
const f = built.frame;
|
|
2701
2963
|
for (const stmt of built.imports) ed.addImportStatement(stmt);
|
|
2702
2964
|
const h = (fr) => fr.height ?? 2e3;
|
|
@@ -2707,17 +2969,23 @@ var Workspace = class {
|
|
|
2707
2969
|
return `Explored a copy of ${f.frameName}`;
|
|
2708
2970
|
}
|
|
2709
2971
|
case "apply_to_page": {
|
|
2710
|
-
const plan =
|
|
2711
|
-
this.pending = null;
|
|
2972
|
+
const plan = take(prepared, "apply");
|
|
2712
2973
|
for (const stmt of plan.imports) ed.addImportStatement(stmt);
|
|
2713
2974
|
ed.replace(doc.frames[0].children[0], plan.jsx);
|
|
2714
2975
|
return `Applied ${plan.frame.frameName} to ${plan.page}`;
|
|
2715
2976
|
}
|
|
2977
|
+
default:
|
|
2978
|
+
throw new Error(`Internal: ${cmd.op} isn't handled by applyPageEdit.`);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
/** Scroll reveals and text animations. */
|
|
2982
|
+
applyMotionEdit(ed, cmd, rel, prepared) {
|
|
2983
|
+
switch (cmd.op) {
|
|
2716
2984
|
case "add_animation": {
|
|
2717
2985
|
const { node, parent } = ed.node(cmd.id);
|
|
2718
2986
|
if ("frameName" in node) throw new EditError("Animate a layer inside the frame, not the frame itself.");
|
|
2719
2987
|
if (node.kind === "expression") throw new EditError("Expressions can't be animated; pick the element around it.");
|
|
2720
|
-
const files =
|
|
2988
|
+
const files = prepared?.kind === "motion" ? prepared.files : motionFiles(this.config);
|
|
2721
2989
|
const props = {};
|
|
2722
2990
|
if (cmd.effect !== void 0) props.effect = cmd.effect;
|
|
2723
2991
|
for (const k of ["delay", "duration", "stagger"]) if (cmd[k] !== void 0 && !(cmd.kind === "reveal" && k === "stagger")) props[k] = Math.round(cmd[k] * 100) / 100;
|
|
@@ -2765,10 +3033,16 @@ var Workspace = class {
|
|
|
2765
3033
|
ed.selectPaths.push(wrapper === kids[0] ? node.path : wrapper.path);
|
|
2766
3034
|
return cmd.kind === "reveal" ? "Removed the reveal" : "Removed the text animation";
|
|
2767
3035
|
}
|
|
3036
|
+
default:
|
|
3037
|
+
throw new Error(`Internal: ${cmd.op} isn't handled by applyMotionEdit.`);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
/** New components, from scratch or from selected layers. */
|
|
3041
|
+
applyComponentEdit(ed, cmd, rel, prepared) {
|
|
3042
|
+
switch (cmd.op) {
|
|
2768
3043
|
case "create_component": {
|
|
2769
|
-
const made =
|
|
2770
|
-
this.
|
|
2771
|
-
ed.ensureImport(made.name, importSpecifier(this.config.root, rel, made.file));
|
|
3044
|
+
const made = take(prepared, "component");
|
|
3045
|
+
if (cmd.from || cmd.parent) ed.ensureImport(made.name, importSpecifier(this.config.root, rel, made.file));
|
|
2772
3046
|
if (cmd.from) {
|
|
2773
3047
|
const { node } = ed.node(cmd.from);
|
|
2774
3048
|
ed.replace(node, `<${made.name} />`);
|
|
@@ -2778,49 +3052,8 @@ var Workspace = class {
|
|
|
2778
3052
|
ed.insertChild(ed.container(cmd.parent), cmd.index, `<${made.name} />`);
|
|
2779
3053
|
return `Created component ${made.name}`;
|
|
2780
3054
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
if (!spec) throw new EditError(`Unknown component "${cmd.component}". Use list_components.`);
|
|
2784
|
-
if (spec.library) throw new EditError(`${spec.name} comes from ${spec.library}: it can't be edited here.`);
|
|
2785
|
-
const existing = doc.frames.find((f) => f.component === `${spec.file}#${spec.name}`);
|
|
2786
|
-
if (existing) {
|
|
2787
|
-
ed.selectPaths.push(existing.path);
|
|
2788
|
-
return `Opened ${spec.name}`;
|
|
2789
|
-
}
|
|
2790
|
-
ed.ensureImport(spec.name, importSpecifier(this.config.root, rel, spec.file));
|
|
2791
|
-
const right = doc.frames.reduce((m, f) => Math.max(m, f.x + f.width), -80);
|
|
2792
|
-
const top = doc.frames.length ? Math.min(...doc.frames.map((f) => f.y)) : 0;
|
|
2793
|
-
ed.insertFrame(
|
|
2794
|
-
frameCode({
|
|
2795
|
-
name: uniqueFrameName(doc, spec.name),
|
|
2796
|
-
x: cmd.x ?? right + 80,
|
|
2797
|
-
y: cmd.y ?? top,
|
|
2798
|
-
width: cmd.width ?? 640,
|
|
2799
|
-
component: `${spec.file}#${spec.name}`,
|
|
2800
|
-
body: `<div className="p-10">
|
|
2801
|
-
${componentJsx(spec)}
|
|
2802
|
-
</div>`
|
|
2803
|
-
})
|
|
2804
|
-
);
|
|
2805
|
-
return `Opened main component ${spec.name}`;
|
|
2806
|
-
}
|
|
2807
|
-
case "create_variants": {
|
|
2808
|
-
const spec = this.catalog.get(cmd.component);
|
|
2809
|
-
if (!spec) throw new EditError(`Unknown component "${cmd.component}".`);
|
|
2810
|
-
const prop = spec.props.find((p) => p.name === cmd.prop);
|
|
2811
|
-
if (!prop) throw new EditError(`${spec.name} has no prop "${cmd.prop}".`);
|
|
2812
|
-
const values = prop.type === "enum" ? prop.options : prop.type === "boolean" ? [false, true] : [];
|
|
2813
|
-
if (!values.length) throw new EditError(`Prop "${cmd.prop}" is ${prop.typeText}; variants need a union of string literals or a boolean.`);
|
|
2814
|
-
ed.ensureImport(spec.name, importSpecifier(this.config.root, rel, spec.file));
|
|
2815
|
-
const label = (v) => typeof v === "string" ? v[0].toUpperCase() + v.slice(1) : `${prop.name}: ${v}`;
|
|
2816
|
-
const rows = indentBlock(values.map((v) => componentJsx(spec, { ...cmd.props, [prop.name]: v }, spec.acceptsChildren ? label(v) : void 0)).join("\n"));
|
|
2817
|
-
const right = doc.frames.reduce((m, f) => Math.max(m, f.x + f.width), -80);
|
|
2818
|
-
const name = uniqueFrameName(doc, `${spec.name} \xB7 ${prop.name}`);
|
|
2819
|
-
ed.insertFrame(frameCode({ name, x: cmd.x ?? right + 80, y: cmd.y ?? 0, width: 360, body: `<div className="flex flex-col gap-3 p-4">
|
|
2820
|
-
${rows}
|
|
2821
|
-
</div>` }));
|
|
2822
|
-
return `Created ${spec.name} ${prop.name} variants`;
|
|
2823
|
-
}
|
|
3055
|
+
default:
|
|
3056
|
+
throw new Error(`Internal: ${cmd.op} isn't handled by applyComponentEdit.`);
|
|
2824
3057
|
}
|
|
2825
3058
|
}
|
|
2826
3059
|
autoImport(ed, rel, jsx) {
|
|
@@ -2833,9 +3066,6 @@ ${rows}
|
|
|
2833
3066
|
ed.ensureImport(name, importSpecifier(this.config.root, rel, spec.file));
|
|
2834
3067
|
}
|
|
2835
3068
|
}
|
|
2836
|
-
motion = null;
|
|
2837
|
-
/** work resolved (async) in exec before the synchronous apply */
|
|
2838
|
-
pending = null;
|
|
2839
3069
|
cleanCache = /* @__PURE__ */ new Map();
|
|
2840
3070
|
async isPrettierClean(source, file) {
|
|
2841
3071
|
const hit = this.cleanCache.get(source);
|
|
@@ -2880,11 +3110,29 @@ ${rows}
|
|
|
2880
3110
|
if (stack.length > 200) stack.shift();
|
|
2881
3111
|
this.undoStacks.set(canvas, stack);
|
|
2882
3112
|
this.redoStacks.set(canvas, []);
|
|
2883
|
-
this.feed.push(entry);
|
|
3113
|
+
this.feed.push(publicEntry(entry));
|
|
2884
3114
|
if (this.feed.length > 300) this.feed.shift();
|
|
3115
|
+
this.trimHistory();
|
|
2885
3116
|
this.emit({ type: "change", entry: publicEntry(entry), ids });
|
|
2886
3117
|
this.emitHistory(canvas);
|
|
2887
3118
|
}
|
|
3119
|
+
/**
|
|
3120
|
+
* Undo entries hold whole files (before and after). Keep their total under a
|
|
3121
|
+
* budget by dropping the oldest entries across canvases, so long sessions on
|
|
3122
|
+
* big pages don't grow without limit.
|
|
3123
|
+
*/
|
|
3124
|
+
trimHistory(budget = 48 * 1024 * 1024) {
|
|
3125
|
+
const all = [...this.undoStacks.values(), ...this.redoStacks.values()];
|
|
3126
|
+
let total = 0;
|
|
3127
|
+
for (const stack of all) for (const e of stack) total += e.before.length + e.after.length;
|
|
3128
|
+
while (total > budget) {
|
|
3129
|
+
let oldest = null;
|
|
3130
|
+
for (const stack of all) if (stack.length && (!oldest || stack[0].at < oldest[0].at)) oldest = stack;
|
|
3131
|
+
if (!oldest) break;
|
|
3132
|
+
const e = oldest.shift();
|
|
3133
|
+
total -= e.before.length + e.after.length;
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
2888
3136
|
/** Changes still in effect (not undone) on a canvas since a time, oldest first. */
|
|
2889
3137
|
changesSince(canvas, since) {
|
|
2890
3138
|
return (this.undoStacks.get(canvas) ?? []).filter((e) => e.at > since).map(publicEntry);
|
|
@@ -2909,17 +3157,20 @@ ${rows}
|
|
|
2909
3157
|
const entry = stack[stack.length - 1];
|
|
2910
3158
|
if (!entry) throw new EditError(`Nothing to ${dir}.`);
|
|
2911
3159
|
if (dir === "undo" && entry.before === "") throw new EditError("Cannot undo creating the canvas; delete the file instead.");
|
|
2912
|
-
|
|
2913
|
-
const
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
throw new EditError(`Cannot ${dir} "${entry.label}": ${
|
|
3160
|
+
const file = entry.file ? path8.join(this.config.root, entry.file) : this.file(canvas);
|
|
3161
|
+
const name = entry.file ? path8.basename(entry.file) : "the file";
|
|
3162
|
+
let current;
|
|
3163
|
+
try {
|
|
3164
|
+
current = entry.file ? fs7.readFileSync(file, "utf8") : this.read(canvas);
|
|
3165
|
+
} catch {
|
|
3166
|
+
throw new EditError(`Cannot ${dir} "${entry.label}": ${name} can't be read right now.`);
|
|
2919
3167
|
}
|
|
3168
|
+
const expected = dir === "undo" ? entry.after : entry.before;
|
|
3169
|
+
if (current !== expected) throw new EditError(`Cannot ${dir} "${entry.label}": ${name} changed since.`);
|
|
3170
|
+
stack.pop();
|
|
2920
3171
|
const next = dir === "undo" ? entry.before : entry.after;
|
|
2921
3172
|
this.sources.set(entry.file ? `file:${entry.file}` : canvas, next);
|
|
2922
|
-
|
|
3173
|
+
fs7.writeFileSync(file, next);
|
|
2923
3174
|
const list = to.get(canvas) ?? [];
|
|
2924
3175
|
list.push(entry);
|
|
2925
3176
|
to.set(canvas, list);
|
|
@@ -2929,9 +3180,10 @@ ${rows}
|
|
|
2929
3180
|
for (const c of this.canvasesLinking(entry.file)) if (c !== canvas) this.emit({ type: "doc", canvas: c, doc: this.doc(c) });
|
|
2930
3181
|
}
|
|
2931
3182
|
const label = `${dir === "undo" ? "Undid" : "Redid"} \u201C${entry.label}\u201D`;
|
|
2932
|
-
const marker = { ...entry, id: ++this.seq, label, actor, at: Date.now() };
|
|
3183
|
+
const marker = publicEntry({ ...entry, id: ++this.seq, label, actor, at: Date.now() });
|
|
2933
3184
|
this.feed.push(marker);
|
|
2934
|
-
this.
|
|
3185
|
+
if (this.feed.length > 300) this.feed.shift();
|
|
3186
|
+
this.emit({ type: "change", entry: marker, ids: [] });
|
|
2935
3187
|
this.emitHistory(canvas);
|
|
2936
3188
|
return { doc, label };
|
|
2937
3189
|
});
|
|
@@ -2939,7 +3191,7 @@ ${rows}
|
|
|
2939
3191
|
return job;
|
|
2940
3192
|
}
|
|
2941
3193
|
publicFeed() {
|
|
2942
|
-
return this.feed
|
|
3194
|
+
return this.feed;
|
|
2943
3195
|
}
|
|
2944
3196
|
// ---------- agents ----------
|
|
2945
3197
|
touchAgent(session, name) {
|
|
@@ -2959,17 +3211,17 @@ ${rows}
|
|
|
2959
3211
|
if (this.agents.delete(session)) this.emit({ type: "agents", agents: [...this.agents.values()] });
|
|
2960
3212
|
}
|
|
2961
3213
|
ensureRoute() {
|
|
2962
|
-
if (!
|
|
2963
|
-
|
|
3214
|
+
if (!fs7.existsSync(path8.join(this.config.root, this.config.canvasDir))) {
|
|
3215
|
+
fs7.mkdirSync(path8.join(this.config.root, this.config.canvasDir), { recursive: true });
|
|
2964
3216
|
}
|
|
2965
3217
|
if (!this.canvases().length) {
|
|
2966
3218
|
const file = canvasPath(this.config, "home");
|
|
2967
3219
|
const content = this.homeCanvas();
|
|
2968
|
-
|
|
3220
|
+
fs7.writeFileSync(file, content);
|
|
2969
3221
|
void this.format(content, file).then((pretty) => {
|
|
2970
|
-
if (
|
|
3222
|
+
if (fs7.readFileSync(file, "utf8") !== content) return;
|
|
2971
3223
|
this.sources.set("home", pretty);
|
|
2972
|
-
|
|
3224
|
+
fs7.writeFileSync(file, pretty);
|
|
2973
3225
|
}).catch(() => {
|
|
2974
3226
|
});
|
|
2975
3227
|
}
|
|
@@ -2980,7 +3232,7 @@ ${rows}
|
|
|
2980
3232
|
try {
|
|
2981
3233
|
const home = listRoutes(this.config).find((r) => r.route === "/");
|
|
2982
3234
|
if (!home) return STARTER_CANVAS;
|
|
2983
|
-
const rel =
|
|
3235
|
+
const rel = path8.relative(this.config.root, canvasPath(this.config, "home"));
|
|
2984
3236
|
const empty = parseCanvas("home", rel, `import { Canvas, Frame } from "truecanvas";
|
|
2985
3237
|
export default function C() { return <Canvas></Canvas>; }
|
|
2986
3238
|
`);
|
|
@@ -3020,6 +3272,10 @@ export default function ComponentsCanvas() {
|
|
|
3020
3272
|
return <Canvas></Canvas>;
|
|
3021
3273
|
}
|
|
3022
3274
|
`;
|
|
3275
|
+
function take(prepared, kind) {
|
|
3276
|
+
if (prepared?.kind !== kind) throw new Error(`Internal: the ${kind} step wasn't prepared.`);
|
|
3277
|
+
return prepared;
|
|
3278
|
+
}
|
|
3023
3279
|
function validate(code) {
|
|
3024
3280
|
try {
|
|
3025
3281
|
parseModule(code);
|
|
@@ -3032,6 +3288,13 @@ function publicEntry(e) {
|
|
|
3032
3288
|
const { before: _b, after: _a, ...rest } = e;
|
|
3033
3289
|
return rest;
|
|
3034
3290
|
}
|
|
3291
|
+
function assertShown(view, rel) {
|
|
3292
|
+
const shown = view.frames.some((f) => f.link?.files.includes(rel));
|
|
3293
|
+
if (!shown || path8.isAbsolute(rel) || rel.split(/[\\/]/).includes("..")) throw new EditError(`${rel} isn't shown on this canvas. Re-read the canvas for current ids.`);
|
|
3294
|
+
}
|
|
3295
|
+
function rightEdge(doc) {
|
|
3296
|
+
return doc.frames.reduce((m, f) => Math.max(m, f.x + f.width), -80);
|
|
3297
|
+
}
|
|
3035
3298
|
function nodeRefs(cmd) {
|
|
3036
3299
|
const refs = [];
|
|
3037
3300
|
if ("id" in cmd && cmd.id) refs.push(cmd.id);
|
|
@@ -3097,6 +3360,130 @@ function componentJsx(spec, props = {}, text) {
|
|
|
3097
3360
|
return childText !== void 0 ? `${open}>${textCode(childText)}</${spec.name}>` : `${open} />`;
|
|
3098
3361
|
}
|
|
3099
3362
|
|
|
3363
|
+
// src/core/pm.ts
|
|
3364
|
+
import { spawn } from "child_process";
|
|
3365
|
+
import fs8 from "fs";
|
|
3366
|
+
import path9 from "path";
|
|
3367
|
+
function detectPm(dir) {
|
|
3368
|
+
for (let d = dir; ; d = path9.dirname(d)) {
|
|
3369
|
+
if (fs8.existsSync(path9.join(d, "pnpm-lock.yaml")) || fs8.existsSync(path9.join(d, "pnpm-workspace.yaml"))) return "pnpm";
|
|
3370
|
+
if (fs8.existsSync(path9.join(d, "yarn.lock"))) return "yarn";
|
|
3371
|
+
if (fs8.existsSync(path9.join(d, "bun.lock")) || fs8.existsSync(path9.join(d, "bun.lockb"))) return "bun";
|
|
3372
|
+
if (fs8.existsSync(path9.join(d, "package-lock.json"))) return "npm";
|
|
3373
|
+
if (path9.dirname(d) === d || fs8.existsSync(path9.join(d, ".git"))) break;
|
|
3374
|
+
}
|
|
3375
|
+
const agent = process.env.npm_config_user_agent ?? "";
|
|
3376
|
+
if (agent.startsWith("pnpm")) return "pnpm";
|
|
3377
|
+
if (agent.startsWith("yarn")) return "yarn";
|
|
3378
|
+
if (agent.startsWith("bun")) return "bun";
|
|
3379
|
+
return "npm";
|
|
3380
|
+
}
|
|
3381
|
+
function addArgs(pm, specs, dev = false) {
|
|
3382
|
+
if (pm === "npm") return ["install", ...dev ? ["-D"] : [], ...specs];
|
|
3383
|
+
if (pm === "bun") return ["add", ...dev ? ["-d"] : [], ...specs];
|
|
3384
|
+
return ["add", ...dev ? ["-D"] : [], ...specs];
|
|
3385
|
+
}
|
|
3386
|
+
function dlx(pm, pkg, args) {
|
|
3387
|
+
if (pm === "pnpm") return ["pnpm", ["dlx", pkg, ...args]];
|
|
3388
|
+
if (pm === "yarn") return ["yarn", ["dlx", pkg, ...args]];
|
|
3389
|
+
if (pm === "bun") return ["bunx", [pkg, ...args]];
|
|
3390
|
+
return ["npx", ["--yes", pkg, ...args]];
|
|
3391
|
+
}
|
|
3392
|
+
function toolEnv(extra = {}) {
|
|
3393
|
+
return { ...process.env, PATH: `${path9.dirname(process.execPath)}${path9.delimiter}${process.env.PATH ?? ""}`, ...extra };
|
|
3394
|
+
}
|
|
3395
|
+
function shellSafe(cmd, args) {
|
|
3396
|
+
if (process.platform !== "win32") return { cmd, args, shell: false };
|
|
3397
|
+
const quote = (a) => /[\s"&|<>^()%!]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a;
|
|
3398
|
+
return { cmd: quote(cmd), args: args.map(quote), shell: true };
|
|
3399
|
+
}
|
|
3400
|
+
function run2(cmd, args, cwd, timeoutMs = 3e5) {
|
|
3401
|
+
return new Promise((resolve) => {
|
|
3402
|
+
let out = "";
|
|
3403
|
+
let done = false;
|
|
3404
|
+
const finish = (ok, extra = "") => {
|
|
3405
|
+
if (done) return;
|
|
3406
|
+
done = true;
|
|
3407
|
+
clearTimeout(timer);
|
|
3408
|
+
resolve({ ok, out: (out + extra).slice(-2e4) });
|
|
3409
|
+
};
|
|
3410
|
+
const safe = shellSafe(cmd, args);
|
|
3411
|
+
const p = spawn(safe.cmd, safe.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: toolEnv({ CI: "1", FORCE_COLOR: "0", NO_COLOR: "1" }), shell: safe.shell });
|
|
3412
|
+
const timer = setTimeout(() => {
|
|
3413
|
+
p.kill();
|
|
3414
|
+
finish(false, `
|
|
3415
|
+
${cmd} timed out after ${Math.round(timeoutMs / 1e3)}s`);
|
|
3416
|
+
}, timeoutMs);
|
|
3417
|
+
p.stdout.on("data", (d) => out += d);
|
|
3418
|
+
p.stderr.on("data", (d) => out += d);
|
|
3419
|
+
p.on("error", (err) => finish(false, `
|
|
3420
|
+
${err.message}`));
|
|
3421
|
+
p.on("close", (code) => finish(code === 0));
|
|
3422
|
+
});
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
// src/core/shadcn.ts
|
|
3426
|
+
import fs9 from "fs";
|
|
3427
|
+
import path10 from "path";
|
|
3428
|
+
function shadcnStatus(root) {
|
|
3429
|
+
const file = path10.join(root, "components.json");
|
|
3430
|
+
if (!fs9.existsSync(file)) return { initialized: false, uiDir: null, installed: [] };
|
|
3431
|
+
let ui = "@/components/ui";
|
|
3432
|
+
try {
|
|
3433
|
+
ui = JSON.parse(fs9.readFileSync(file, "utf8"))?.aliases?.ui ?? ui;
|
|
3434
|
+
} catch {
|
|
3435
|
+
}
|
|
3436
|
+
const dir = resolveAliasPath(root, ui);
|
|
3437
|
+
let installed = [];
|
|
3438
|
+
try {
|
|
3439
|
+
installed = fs9.readdirSync(dir).filter((f) => /\.(tsx|jsx)$/.test(f)).map((f) => f.replace(/\.(tsx|jsx)$/, "")).sort();
|
|
3440
|
+
} catch {
|
|
3441
|
+
installed = [];
|
|
3442
|
+
}
|
|
3443
|
+
return { initialized: true, uiDir: posix(path10.relative(root, dir)), installed };
|
|
3444
|
+
}
|
|
3445
|
+
var FALLBACK = "accordion alert alert-dialog aspect-ratio avatar badge breadcrumb button button-group calendar card carousel chart checkbox collapsible combobox command context-menu dialog drawer dropdown-menu empty field form hover-card input input-group input-otp item kbd label menubar navigation-menu pagination popover progress radio-group resizable scroll-area select separator sheet sidebar skeleton slider sonner spinner switch table tabs textarea toggle toggle-group tooltip".split(" ");
|
|
3446
|
+
var registryCache = null;
|
|
3447
|
+
async function shadcnRegistry() {
|
|
3448
|
+
if (registryCache && Date.now() - registryCache.at < 36e5) return { names: registryCache.names, offline: false };
|
|
3449
|
+
try {
|
|
3450
|
+
const res = await fetch("https://ui.shadcn.com/r/index.json", { signal: AbortSignal.timeout(8e3) });
|
|
3451
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
3452
|
+
const items = await res.json();
|
|
3453
|
+
const names = items.filter((i) => i.type === "registry:ui" && VALID.test(i.name)).map((i) => i.name).sort();
|
|
3454
|
+
if (!names.length) throw new Error("empty registry");
|
|
3455
|
+
registryCache = { at: Date.now(), names };
|
|
3456
|
+
return { names, offline: false };
|
|
3457
|
+
} catch {
|
|
3458
|
+
return { names: FALLBACK, offline: true };
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
var VALID = /^[a-z0-9][a-z0-9-]*$/;
|
|
3462
|
+
async function shadcnInit(root) {
|
|
3463
|
+
const [cmd, args] = dlx(detectPm(root), "shadcn@latest", ["init", "--defaults", "--yes", "--no-monorepo"]);
|
|
3464
|
+
return run2(cmd, args, root);
|
|
3465
|
+
}
|
|
3466
|
+
async function shadcnAdd(root, names) {
|
|
3467
|
+
const bad = names.filter((n) => !VALID.test(n));
|
|
3468
|
+
if (bad.length) return { ok: false, out: `Not a component name: ${bad.join(", ")}`, added: [] };
|
|
3469
|
+
const before = new Set(shadcnStatus(root).installed);
|
|
3470
|
+
let log = "";
|
|
3471
|
+
if (!shadcnStatus(root).initialized) {
|
|
3472
|
+
const init = await shadcnInit(root);
|
|
3473
|
+
log += init.out;
|
|
3474
|
+
if (!init.ok) return { ok: false, out: log, added: [] };
|
|
3475
|
+
}
|
|
3476
|
+
const wanted = names.filter((n) => !before.has(n));
|
|
3477
|
+
if (wanted.length) {
|
|
3478
|
+
const [cmd, args] = dlx(detectPm(root), "shadcn@latest", ["add", ...wanted, "--yes"]);
|
|
3479
|
+
const res = await run2(cmd, args, root);
|
|
3480
|
+
log += res.out;
|
|
3481
|
+
if (!res.ok) return { ok: false, out: log, added: [] };
|
|
3482
|
+
}
|
|
3483
|
+
const added = shadcnStatus(root).installed.filter((n) => !before.has(n));
|
|
3484
|
+
return { ok: true, out: log, added };
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3100
3487
|
// src/core/outline.ts
|
|
3101
3488
|
function propText(name, v) {
|
|
3102
3489
|
if (v.kind === "boolean") return v.value ? name : `${name}={false}`;
|
|
@@ -3185,8 +3572,8 @@ function describeComponent(c, verbose = false) {
|
|
|
3185
3572
|
}
|
|
3186
3573
|
|
|
3187
3574
|
// src/core/tokens.ts
|
|
3188
|
-
import
|
|
3189
|
-
import
|
|
3575
|
+
import fs10 from "fs";
|
|
3576
|
+
import path11 from "path";
|
|
3190
3577
|
import { globSync } from "tinyglobby";
|
|
3191
3578
|
var GROUPS = [
|
|
3192
3579
|
["--color-", "Colors", "bg-{name} text-{name} border-{name}"],
|
|
@@ -3203,9 +3590,9 @@ function readDesignTokens(root) {
|
|
|
3203
3590
|
const sources = [];
|
|
3204
3591
|
let darkVariant = null;
|
|
3205
3592
|
for (const file of files) {
|
|
3206
|
-
const css =
|
|
3593
|
+
const css = fs10.readFileSync(file, "utf8");
|
|
3207
3594
|
if (!css.includes("@theme") && !css.includes("@custom-variant")) continue;
|
|
3208
|
-
sources.push(
|
|
3595
|
+
sources.push(posix(path11.relative(root, file)));
|
|
3209
3596
|
const dv = /@custom-variant\s+dark\s*\(([^;]+)\);/.exec(css);
|
|
3210
3597
|
if (dv) darkVariant = dv[1].trim();
|
|
3211
3598
|
for (const block of css.matchAll(/@theme[^{]*\{([\s\S]*?)\n\}/g)) {
|
|
@@ -3244,7 +3631,7 @@ function readTokenData(root) {
|
|
|
3244
3631
|
for (const m of body.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)) into.set(m[1], m[2].trim());
|
|
3245
3632
|
};
|
|
3246
3633
|
for (const file of files) {
|
|
3247
|
-
const css =
|
|
3634
|
+
const css = fs10.readFileSync(file, "utf8");
|
|
3248
3635
|
for (const block of css.matchAll(/@theme[^{]*\{([\s\S]*?)\n\}/g)) decls(block[1], theme);
|
|
3249
3636
|
for (const block of css.matchAll(/(?:^|\n)\s*:root\s*\{([\s\S]*?)\n\}/g)) decls(block[1], lightVars);
|
|
3250
3637
|
for (const block of css.matchAll(/(?:^|\n)\s*(?:\.dark|\[data-theme=["']?dark["']?\]|:root\.dark)\s*\{([\s\S]*?)\n\}/g)) decls(block[1], darkVars);
|
|
@@ -3276,30 +3663,30 @@ function readTokenData(root) {
|
|
|
3276
3663
|
}
|
|
3277
3664
|
|
|
3278
3665
|
// src/core/compare.ts
|
|
3279
|
-
import
|
|
3280
|
-
import
|
|
3666
|
+
import fs11 from "fs";
|
|
3667
|
+
import path12 from "path";
|
|
3281
3668
|
var COMPARE_PREFIX = "__compare__";
|
|
3282
3669
|
function writeCompare(config, canvas, source, files = {}, as = canvas) {
|
|
3283
|
-
const routeDir =
|
|
3284
|
-
const dir =
|
|
3285
|
-
|
|
3286
|
-
const canvasDir =
|
|
3287
|
-
const canvasRel =
|
|
3670
|
+
const routeDir = routeDirOf(config);
|
|
3671
|
+
const dir = path12.join(routeDir, "compare");
|
|
3672
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
3673
|
+
const canvasDir = path12.join(config.root, config.canvasDir);
|
|
3674
|
+
const canvasRel = `${config.canvasDir}/${canvas}.canvas.tsx`;
|
|
3288
3675
|
const swapped = /* @__PURE__ */ new Map();
|
|
3289
3676
|
for (const [rel, content] of Object.entries(files)) {
|
|
3290
|
-
const ext =
|
|
3677
|
+
const ext = path12.extname(rel);
|
|
3291
3678
|
const name = `${as}--${rel.slice(0, -ext.length).replace(/[^\w.-]+/g, "_")}${ext}`;
|
|
3292
|
-
const original =
|
|
3293
|
-
|
|
3679
|
+
const original = path12.join(config.root, rel);
|
|
3680
|
+
fs11.writeFileSync(path12.join(dir, name), rewriteImports(content, (spec) => spec.startsWith(".") ? relativeSpec(dir, path12.resolve(path12.dirname(original), spec)) : spec));
|
|
3294
3681
|
swapped.set(importSpecifier(config.root, canvasRel, rel), `./${name.slice(0, -ext.length)}`);
|
|
3295
3682
|
}
|
|
3296
|
-
const out = rewriteImports(source, (spec) => swapped.get(spec) ?? (spec.startsWith(".") ? relativeSpec(dir,
|
|
3297
|
-
|
|
3683
|
+
const out = rewriteImports(source, (spec) => swapped.get(spec) ?? (spec.startsWith(".") ? relativeSpec(dir, path12.resolve(canvasDir, spec)) : spec));
|
|
3684
|
+
fs11.writeFileSync(path12.join(dir, `${as}.canvas.tsx`), out);
|
|
3298
3685
|
syncRoute(config);
|
|
3299
3686
|
return `${COMPARE_PREFIX}${as}`;
|
|
3300
3687
|
}
|
|
3301
3688
|
function relativeSpec(fromDir, target) {
|
|
3302
|
-
const next =
|
|
3689
|
+
const next = posix(path12.relative(fromDir, target));
|
|
3303
3690
|
return next.startsWith(".") ? next : `./${next}`;
|
|
3304
3691
|
}
|
|
3305
3692
|
function rewriteImports(source, map) {
|
|
@@ -3308,15 +3695,16 @@ function rewriteImports(source, map) {
|
|
|
3308
3695
|
for (const imp of [...doc.imports].sort((a, b) => b.start - a.start)) {
|
|
3309
3696
|
const next = map(imp.source);
|
|
3310
3697
|
if (next === imp.source) continue;
|
|
3311
|
-
const stmt = out.slice(imp.start, imp.end)
|
|
3698
|
+
const stmt = rewriteImportSource(out.slice(imp.start, imp.end), imp.source, next);
|
|
3312
3699
|
out = out.slice(0, imp.start) + stmt + out.slice(imp.end);
|
|
3313
3700
|
}
|
|
3314
3701
|
return out;
|
|
3315
3702
|
}
|
|
3316
3703
|
function clearCompare(config, canvas) {
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3704
|
+
if (canvas) assertCanvasName(canvas);
|
|
3705
|
+
const dir = path12.join(routeDirOf(config), "compare");
|
|
3706
|
+
if (!fs11.existsSync(dir)) return;
|
|
3707
|
+
for (const f of fs11.readdirSync(dir)) if (!canvas || f === `${canvas}.canvas.tsx` || f.startsWith(`${canvas}--`)) fs11.rmSync(path12.join(dir, f));
|
|
3320
3708
|
syncRoute(config);
|
|
3321
3709
|
}
|
|
3322
3710
|
function frameChanges(before, after) {
|
|
@@ -3332,11 +3720,189 @@ function frameChanges(before, after) {
|
|
|
3332
3720
|
return out;
|
|
3333
3721
|
}
|
|
3334
3722
|
|
|
3723
|
+
// src/core/init.ts
|
|
3724
|
+
import fs12 from "fs";
|
|
3725
|
+
import MagicString2 from "magic-string";
|
|
3726
|
+
import path13 from "path";
|
|
3727
|
+
var CONFIG_FILES = {
|
|
3728
|
+
next: ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.mts"],
|
|
3729
|
+
vite: ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"]
|
|
3730
|
+
};
|
|
3731
|
+
function projectFramework(root) {
|
|
3732
|
+
try {
|
|
3733
|
+
const pkg = JSON.parse(fs12.readFileSync(path13.join(root, "package.json"), "utf8"));
|
|
3734
|
+
return detectFramework({ ...pkg.dependencies, ...pkg.devDependencies });
|
|
3735
|
+
} catch {
|
|
3736
|
+
return "next";
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
function configStatus(root, framework = projectFramework(root)) {
|
|
3740
|
+
const marker = framework === "vite" ? "truecanvas/vite" : "withTruecanvas";
|
|
3741
|
+
for (const name of CONFIG_FILES[framework]) {
|
|
3742
|
+
const file = path13.join(root, name);
|
|
3743
|
+
if (fs12.existsSync(file)) return { file, wrapped: fs12.readFileSync(file, "utf8").includes(marker), framework };
|
|
3744
|
+
}
|
|
3745
|
+
return { file: null, wrapped: false, framework };
|
|
3746
|
+
}
|
|
3747
|
+
function initProject(config) {
|
|
3748
|
+
const { root } = config;
|
|
3749
|
+
const report = { done: [], todo: [] };
|
|
3750
|
+
const vite = config.framework === "vite";
|
|
3751
|
+
const status = configStatus(root, config.framework);
|
|
3752
|
+
if (vite) {
|
|
3753
|
+
const name = status.file ? path13.basename(status.file) : "vite.config.ts";
|
|
3754
|
+
if (status.wrapped) report.done.push(`${name} already uses the truecanvas() plugin`);
|
|
3755
|
+
else {
|
|
3756
|
+
const src = status.file ? fs12.readFileSync(status.file, "utf8") : null;
|
|
3757
|
+
const next = src && addVitePlugin(src);
|
|
3758
|
+
if (next) {
|
|
3759
|
+
fs12.writeFileSync(status.file, next);
|
|
3760
|
+
report.done.push(`Added the truecanvas() plugin to ${name}`);
|
|
3761
|
+
} else report.todo.push(`Add truecanvas() from "truecanvas/vite" to the plugins in ${name}`);
|
|
3762
|
+
}
|
|
3763
|
+
} else if (!status.file) {
|
|
3764
|
+
const file = path13.join(root, "next.config.ts");
|
|
3765
|
+
fs12.writeFileSync(file, `import type { NextConfig } from "next";
|
|
3766
|
+
import { withTruecanvas } from "truecanvas/next";
|
|
3767
|
+
|
|
3768
|
+
const nextConfig: NextConfig = {};
|
|
3769
|
+
|
|
3770
|
+
export default withTruecanvas(nextConfig);
|
|
3771
|
+
`);
|
|
3772
|
+
report.done.push("Created next.config.ts with withTruecanvas()");
|
|
3773
|
+
} else if (status.wrapped) {
|
|
3774
|
+
report.done.push(`${path13.basename(status.file)} already uses withTruecanvas()`);
|
|
3775
|
+
} else {
|
|
3776
|
+
const src = fs12.readFileSync(status.file, "utf8");
|
|
3777
|
+
const next = wrapConfigExport(src);
|
|
3778
|
+
if (next && next !== src && next.includes("withTruecanvas(")) {
|
|
3779
|
+
fs12.writeFileSync(status.file, next);
|
|
3780
|
+
report.done.push(`Wrapped ${path13.basename(status.file)} with withTruecanvas()`);
|
|
3781
|
+
} else {
|
|
3782
|
+
report.todo.push(`Wrap your ${path13.basename(status.file)} export with withTruecanvas() from "truecanvas/next"`);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
const pkgFile = path13.join(root, "package.json");
|
|
3786
|
+
try {
|
|
3787
|
+
const raw = fs12.readFileSync(pkgFile, "utf8");
|
|
3788
|
+
const pkg = JSON.parse(raw);
|
|
3789
|
+
pkg.scripts ??= {};
|
|
3790
|
+
if (!Object.values(pkg.scripts).some((v) => typeof v === "string" && v.includes("truecanvas"))) {
|
|
3791
|
+
pkg.scripts.canvas = "truecanvas dev";
|
|
3792
|
+
const indent = /\n(\s+)"/.exec(raw)?.[1] ?? " ";
|
|
3793
|
+
fs12.writeFileSync(pkgFile, `${JSON.stringify(pkg, null, indent)}
|
|
3794
|
+
`);
|
|
3795
|
+
report.done.push(`Added the "canvas" script (npm run canvas)`);
|
|
3796
|
+
}
|
|
3797
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3798
|
+
if (!("tailwindcss" in deps)) report.todo.push("Tailwind CSS isn't installed: layout and style controls write Tailwind classes");
|
|
3799
|
+
if (!("next" in deps) && !("vite" in deps)) report.todo.push("Neither Next.js nor Vite is in package.json: Truecanvas needs a Next.js (App Router) or Vite + React app");
|
|
3800
|
+
} catch {
|
|
3801
|
+
report.todo.push("Couldn't read package.json");
|
|
3802
|
+
}
|
|
3803
|
+
const ignoreFile = path13.join(root, ".gitignore");
|
|
3804
|
+
const entry = `/${config.routeDir}/`;
|
|
3805
|
+
const ignore = fs12.existsSync(ignoreFile) ? fs12.readFileSync(ignoreFile, "utf8") : "";
|
|
3806
|
+
if (!ignore.split("\n").some((l) => l.trim() === entry || l.trim() === entry.slice(1))) {
|
|
3807
|
+
fs12.writeFileSync(ignoreFile, `${ignore}${ignore && !ignore.endsWith("\n") ? "\n" : ""}
|
|
3808
|
+
# Truecanvas (generated dev-only route)
|
|
3809
|
+
${entry}
|
|
3810
|
+
`);
|
|
3811
|
+
report.done.push(`Ignored ${entry} in .gitignore`);
|
|
3812
|
+
}
|
|
3813
|
+
for (const name of ["tailwind.config.ts", "tailwind.config.js", "tailwind.config.mjs", "tailwind.config.cjs"]) {
|
|
3814
|
+
const file = path13.join(root, name);
|
|
3815
|
+
if (!fs12.existsSync(file)) continue;
|
|
3816
|
+
const src = fs12.readFileSync(file, "utf8");
|
|
3817
|
+
const glob = `./${config.canvasDir}/**/*.{ts,tsx}`;
|
|
3818
|
+
if (src.includes(config.canvasDir)) break;
|
|
3819
|
+
const m = /content\s*:\s*\[([ \t]*\n?)([ \t]*)/.exec(src);
|
|
3820
|
+
if (m) {
|
|
3821
|
+
const indent = m[2] || " ";
|
|
3822
|
+
const at = m.index + m[0].length;
|
|
3823
|
+
const entry2 = m[1].includes("\n") ? `${JSON.stringify(glob)},
|
|
3824
|
+
${indent}` : `${JSON.stringify(glob)}, `;
|
|
3825
|
+
fs12.writeFileSync(file, src.slice(0, at) + entry2 + src.slice(at));
|
|
3826
|
+
report.done.push(`Added ${config.canvasDir} to Tailwind's content in ${name}`);
|
|
3827
|
+
} else report.todo.push(`Add "${glob}" to the content list in ${name} so Tailwind styles your canvases`);
|
|
3828
|
+
break;
|
|
3829
|
+
}
|
|
3830
|
+
if (!vite && !fs12.existsSync(path13.join(root, config.appDir))) report.todo.push(`No ${config.appDir}/ folder found: Truecanvas needs the Next.js App Router`);
|
|
3831
|
+
return report;
|
|
3832
|
+
}
|
|
3833
|
+
function addImport(src, line) {
|
|
3834
|
+
const lines = src.split("\n");
|
|
3835
|
+
let at = 0;
|
|
3836
|
+
lines.forEach((l, i) => {
|
|
3837
|
+
if (/^\s*import\s/.test(l) || /^\s*["']use (client|server)["']/.test(l)) at = i + 1;
|
|
3838
|
+
});
|
|
3839
|
+
while (at > 0 && at < lines.length && !/;\s*$|from\s+["'][^"']+["']\s*;?\s*$/.test(lines[at - 1])) at++;
|
|
3840
|
+
lines.splice(at, 0, line);
|
|
3841
|
+
return lines.join("\n");
|
|
3842
|
+
}
|
|
3843
|
+
function wrapConfigExport(src) {
|
|
3844
|
+
let ast;
|
|
3845
|
+
try {
|
|
3846
|
+
ast = parseModule(src);
|
|
3847
|
+
} catch {
|
|
3848
|
+
return null;
|
|
3849
|
+
}
|
|
3850
|
+
const s = new MagicString2(src);
|
|
3851
|
+
for (const stmt of ast.program.body) {
|
|
3852
|
+
if (stmt.type === "ExportDefaultDeclaration") {
|
|
3853
|
+
const d = stmt.declaration;
|
|
3854
|
+
if (d.type === "FunctionDeclaration" && d.id) {
|
|
3855
|
+
s.remove(stmt.start, d.start);
|
|
3856
|
+
s.appendLeft(stmt.end, `
|
|
3857
|
+
|
|
3858
|
+
export default withTruecanvas(${d.id.name});`);
|
|
3859
|
+
} else if (d.type === "FunctionDeclaration" || d.type === "ArrowFunctionExpression" || d.type.endsWith("Expression") || d.type === "Identifier") {
|
|
3860
|
+
s.appendLeft(d.start, "withTruecanvas(");
|
|
3861
|
+
s.appendRight(d.end, ")");
|
|
3862
|
+
} else return null;
|
|
3863
|
+
return addImport(s.toString(), `import { withTruecanvas } from "truecanvas/next";`);
|
|
3864
|
+
}
|
|
3865
|
+
if (stmt.type === "ExpressionStatement" && stmt.expression.type === "AssignmentExpression") {
|
|
3866
|
+
const { left, right } = stmt.expression;
|
|
3867
|
+
const isModuleExports = left.type === "MemberExpression" && left.object.type === "Identifier" && left.object.name === "module" && left.property.type === "Identifier" && left.property.name === "exports";
|
|
3868
|
+
if (!isModuleExports) continue;
|
|
3869
|
+
s.appendLeft(right.start, "withTruecanvas(");
|
|
3870
|
+
s.appendRight(right.end, ")");
|
|
3871
|
+
return `const { withTruecanvas } = require("truecanvas/next");
|
|
3872
|
+
${s.toString()}`;
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
return null;
|
|
3876
|
+
}
|
|
3877
|
+
function addVitePlugin(src) {
|
|
3878
|
+
let ast;
|
|
3879
|
+
try {
|
|
3880
|
+
ast = parseModule(src);
|
|
3881
|
+
} catch {
|
|
3882
|
+
return null;
|
|
3883
|
+
}
|
|
3884
|
+
let plugins = null;
|
|
3885
|
+
walk(ast, (node) => {
|
|
3886
|
+
if (plugins) return false;
|
|
3887
|
+
if (node.type !== "ObjectProperty" || node.value.type !== "ArrayExpression") return;
|
|
3888
|
+
const key = node.key.type === "Identifier" ? node.key.name : node.key.type === "StringLiteral" ? node.key.value : null;
|
|
3889
|
+
if (key !== "plugins") return;
|
|
3890
|
+
plugins = { start: node.value.start, end: node.value.end, elements: node.value.elements.filter((e) => e !== null).map((e) => ({ start: e.start, end: e.end })) };
|
|
3891
|
+
return false;
|
|
3892
|
+
});
|
|
3893
|
+
if (!plugins) return null;
|
|
3894
|
+
const { start, end, elements } = plugins;
|
|
3895
|
+
const s = new MagicString2(src);
|
|
3896
|
+
if (!elements.length) s.overwrite(start, end, "[truecanvas()]");
|
|
3897
|
+
else {
|
|
3898
|
+
const last = elements[elements.length - 1];
|
|
3899
|
+
const before = src.slice(start + 1, elements[0].start);
|
|
3900
|
+
s.appendLeft(last.end, before.includes("\n") ? `,${before.replace(/^[^\n]*/, "")}truecanvas()` : ", truecanvas()");
|
|
3901
|
+
}
|
|
3902
|
+
return addImport(s.toString(), `import { truecanvas } from "truecanvas/vite";`);
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3335
3905
|
export {
|
|
3336
|
-
loadConfig,
|
|
3337
|
-
parseModule,
|
|
3338
|
-
idOf,
|
|
3339
|
-
walk,
|
|
3340
3906
|
parseCanvas,
|
|
3341
3907
|
allNodes,
|
|
3342
3908
|
findNode,
|
|
@@ -3351,16 +3917,32 @@ export {
|
|
|
3351
3917
|
componentsDir,
|
|
3352
3918
|
DEVICES,
|
|
3353
3919
|
findDevice,
|
|
3920
|
+
assertCanvasName,
|
|
3921
|
+
syncRoute,
|
|
3922
|
+
viteStylesheets,
|
|
3923
|
+
syncComponentRegistry,
|
|
3354
3924
|
Comments,
|
|
3355
3925
|
Git,
|
|
3356
|
-
syncComponentRegistry,
|
|
3357
3926
|
Workspace,
|
|
3358
3927
|
componentJsx,
|
|
3928
|
+
detectPm,
|
|
3929
|
+
addArgs,
|
|
3930
|
+
toolEnv,
|
|
3931
|
+
shellSafe,
|
|
3932
|
+
run2 as run,
|
|
3933
|
+
shadcnStatus,
|
|
3934
|
+
shadcnRegistry,
|
|
3935
|
+
shadcnInit,
|
|
3936
|
+
shadcnAdd,
|
|
3359
3937
|
outlineDoc,
|
|
3360
3938
|
describeComponent,
|
|
3361
3939
|
readDesignTokens,
|
|
3362
3940
|
readTokenData,
|
|
3363
3941
|
writeCompare,
|
|
3364
3942
|
clearCompare,
|
|
3365
|
-
frameChanges
|
|
3943
|
+
frameChanges,
|
|
3944
|
+
configStatus,
|
|
3945
|
+
initProject,
|
|
3946
|
+
wrapConfigExport,
|
|
3947
|
+
addVitePlugin
|
|
3366
3948
|
};
|