truecanvas 0.1.0 → 0.2.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 +3 -1
- package/dist/catalog-worker.js +1 -1
- package/dist/chunk-C4SATRZT.js +196 -0
- package/dist/{chunk-753N4QRO.js → chunk-T2TCBSTG.js} +18 -6
- package/dist/{chunk-WB35FXGP.js → chunk-ZPARY6OD.js} +994 -439
- package/dist/cli.js +592 -391
- package/dist/core/index.d.ts +143 -8
- package/dist/core/index.js +26 -2
- package/dist/editor/assets/Menu-BFifVcgz.js +9 -0
- package/dist/editor/assets/Menu-DXPm0EXo.css +1 -0
- package/dist/editor/assets/hub-BnbEAnt6.js +2 -0
- package/dist/editor/assets/main-DlADROMw.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 +13 -1
- package/dist/runtime/host.js +3 -1
- package/package.json +1 -1
- 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,7 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Catalog,
|
|
3
|
-
importSpecifier
|
|
4
|
-
|
|
3
|
+
importSpecifier,
|
|
4
|
+
resolveAliasPath
|
|
5
|
+
} from "./chunk-T2TCBSTG.js";
|
|
6
|
+
import {
|
|
7
|
+
ICON_LIBRARIES,
|
|
8
|
+
iconLibrary,
|
|
9
|
+
loadIcons,
|
|
10
|
+
searchIcons
|
|
11
|
+
} from "./chunk-C4SATRZT.js";
|
|
5
12
|
|
|
6
13
|
// src/core/config.ts
|
|
7
14
|
import fs from "fs";
|
|
@@ -73,6 +80,15 @@ function walk(node, visit, parent = null) {
|
|
|
73
80
|
function isComponentName(name) {
|
|
74
81
|
return /^[A-Z]/.test(name) || name.includes(".");
|
|
75
82
|
}
|
|
83
|
+
function rewriteImportSource(statement, from, to) {
|
|
84
|
+
if (from === to) return statement;
|
|
85
|
+
for (const q of ['"', "'"]) {
|
|
86
|
+
const needle = `${q}${from}${q}`;
|
|
87
|
+
const i = statement.indexOf(needle);
|
|
88
|
+
if (i >= 0) return `${statement.slice(0, i)}${q}${to}${q}${statement.slice(i + needle.length)}`;
|
|
89
|
+
}
|
|
90
|
+
return statement;
|
|
91
|
+
}
|
|
76
92
|
|
|
77
93
|
// src/core/parse.ts
|
|
78
94
|
function parseCanvas(name, file, source) {
|
|
@@ -102,8 +118,8 @@ function parseCanvas(name, file, source) {
|
|
|
102
118
|
let frameIndex = 0;
|
|
103
119
|
for (const child of canvasNode.children) {
|
|
104
120
|
if (child.kind !== "component" || child.name !== "Frame") continue;
|
|
105
|
-
const
|
|
106
|
-
reindex(child,
|
|
121
|
+
const path15 = [frameIndex++];
|
|
122
|
+
reindex(child, path15);
|
|
107
123
|
frames.push({
|
|
108
124
|
...child,
|
|
109
125
|
frameName: str(child.props.name) ?? `Frame ${frameIndex}`,
|
|
@@ -134,6 +150,7 @@ function readImports(ast) {
|
|
|
134
150
|
typeOnly,
|
|
135
151
|
// value bindings only: `import type` and `{ type X }` don't exist at runtime
|
|
136
152
|
names: typeOnly ? [] : [...named.filter((s) => s.importKind !== "type").map((s) => s.local.name), ...ns ? [ns.local.name] : []],
|
|
153
|
+
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
154
|
defaultName: def ? def.local.name : null,
|
|
138
155
|
start: stmt.start,
|
|
139
156
|
end: stmt.end,
|
|
@@ -153,9 +170,9 @@ function topLevelNames(ast) {
|
|
|
153
170
|
}
|
|
154
171
|
return out;
|
|
155
172
|
}
|
|
156
|
-
function reindex(node,
|
|
157
|
-
node.path =
|
|
158
|
-
node.children.forEach((c, i) => reindex(c, [...
|
|
173
|
+
function reindex(node, path15) {
|
|
174
|
+
node.path = path15;
|
|
175
|
+
node.children.forEach((c, i) => reindex(c, [...path15, i]));
|
|
159
176
|
}
|
|
160
177
|
function str(v) {
|
|
161
178
|
return v?.kind === "string" ? v.value : void 0;
|
|
@@ -163,10 +180,10 @@ function str(v) {
|
|
|
163
180
|
function num(v) {
|
|
164
181
|
return v?.kind === "number" ? v.value : void 0;
|
|
165
182
|
}
|
|
166
|
-
function buildNode(el, source,
|
|
183
|
+
function buildNode(el, source, path15) {
|
|
167
184
|
const children = [];
|
|
168
185
|
for (const child of el.children) {
|
|
169
|
-
const built = buildChild(child, source, [...
|
|
186
|
+
const built = buildChild(child, source, [...path15, children.length]);
|
|
170
187
|
if (built) children.push(built);
|
|
171
188
|
}
|
|
172
189
|
if (el.type === "JSXFragment") {
|
|
@@ -181,7 +198,7 @@ function buildNode(el, source, path12) {
|
|
|
181
198
|
end: el.end,
|
|
182
199
|
line: el.loc.start.line,
|
|
183
200
|
col: el.loc.start.column,
|
|
184
|
-
path:
|
|
201
|
+
path: path15,
|
|
185
202
|
attrs: [],
|
|
186
203
|
nameEnd: el.openingFragment.end - 1,
|
|
187
204
|
openEnd: el.openingFragment.end,
|
|
@@ -216,7 +233,7 @@ function buildNode(el, source, path12) {
|
|
|
216
233
|
end: el.end,
|
|
217
234
|
line: open.loc.start.line,
|
|
218
235
|
col: open.loc.start.column,
|
|
219
|
-
path:
|
|
236
|
+
path: path15,
|
|
220
237
|
attrs,
|
|
221
238
|
// after type arguments: `<Select<Opt> …>`
|
|
222
239
|
nameEnd: (open.typeParameters ?? open.name).end,
|
|
@@ -225,27 +242,27 @@ function buildNode(el, source, path12) {
|
|
|
225
242
|
closeStart: el.closingElement ? el.closingElement.start : open.end
|
|
226
243
|
};
|
|
227
244
|
}
|
|
228
|
-
function buildChild(child, source,
|
|
229
|
-
if (child.type === "JSXElement" || child.type === "JSXFragment") return buildNode(child, source,
|
|
245
|
+
function buildChild(child, source, path15) {
|
|
246
|
+
if (child.type === "JSXElement" || child.type === "JSXFragment") return buildNode(child, source, path15);
|
|
230
247
|
if (child.type === "JSXText") {
|
|
231
248
|
const text = child.value.replace(/\s+/g, " ").trim();
|
|
232
249
|
if (!text) return null;
|
|
233
|
-
return leaf("text", text, child,
|
|
250
|
+
return leaf("text", text, child, path15, { text });
|
|
234
251
|
}
|
|
235
252
|
if (child.type === "JSXExpressionContainer") {
|
|
236
253
|
const expr = child.expression;
|
|
237
254
|
if (expr.type === "JSXEmptyExpression") return null;
|
|
238
|
-
if (expr.type === "StringLiteral") return leaf("text", expr.value, child,
|
|
255
|
+
if (expr.type === "StringLiteral") return leaf("text", expr.value, child, path15, { text: expr.value });
|
|
239
256
|
if (expr.type === "TemplateLiteral" && expr.expressions.length === 0) {
|
|
240
257
|
const text = expr.quasis.map((q) => q.value.cooked ?? "").join("");
|
|
241
|
-
return leaf("text", text, child,
|
|
258
|
+
return leaf("text", text, child, path15, { text });
|
|
242
259
|
}
|
|
243
|
-
return leaf("expression", source.slice(expr.start, expr.end), child,
|
|
260
|
+
return leaf("expression", source.slice(expr.start, expr.end), child, path15, {});
|
|
244
261
|
}
|
|
245
|
-
if (child.type === "JSXSpreadChild") return leaf("expression", source.slice(child.start, child.end), child,
|
|
262
|
+
if (child.type === "JSXSpreadChild") return leaf("expression", source.slice(child.start, child.end), child, path15, {});
|
|
246
263
|
return null;
|
|
247
264
|
}
|
|
248
|
-
function leaf(kind, name, node,
|
|
265
|
+
function leaf(kind, name, node, path15, extra) {
|
|
249
266
|
return {
|
|
250
267
|
id: `${kind[0]}${idOf(node)}`,
|
|
251
268
|
kind,
|
|
@@ -258,7 +275,7 @@ function leaf(kind, name, node, path12, extra) {
|
|
|
258
275
|
end: node.end,
|
|
259
276
|
line: node.loc.start.line,
|
|
260
277
|
col: node.loc.start.column,
|
|
261
|
-
path:
|
|
278
|
+
path: path15,
|
|
262
279
|
attrs: [],
|
|
263
280
|
nameEnd: node.end,
|
|
264
281
|
openEnd: node.end,
|
|
@@ -322,10 +339,10 @@ function findNode(doc, id) {
|
|
|
322
339
|
function findFrame(doc, ref) {
|
|
323
340
|
return doc.frames.find((f) => f.id === ref || f.frameName === ref) ?? null;
|
|
324
341
|
}
|
|
325
|
-
function findByPath(doc,
|
|
326
|
-
if (!
|
|
327
|
-
let node = doc.frames[
|
|
328
|
-
for (const i of
|
|
342
|
+
function findByPath(doc, path15) {
|
|
343
|
+
if (!path15.length) return null;
|
|
344
|
+
let node = doc.frames[path15[0]];
|
|
345
|
+
for (const i of path15.slice(1)) node = node?.children[i];
|
|
329
346
|
return node ?? null;
|
|
330
347
|
}
|
|
331
348
|
|
|
@@ -418,7 +435,8 @@ var CanvasEditor = class {
|
|
|
418
435
|
} else if (node.openEnd === node.closeStart) {
|
|
419
436
|
this.s.appendLeft(node.openEnd, code);
|
|
420
437
|
} else {
|
|
421
|
-
this.
|
|
438
|
+
const comments = this.source.slice(node.openEnd, node.closeStart).match(/\{\s*\/\*[\s\S]*?\*\/\s*\}/g) ?? [];
|
|
439
|
+
this.s.overwrite(node.openEnd, node.closeStart, comments.join("") + code);
|
|
422
440
|
}
|
|
423
441
|
}
|
|
424
442
|
// ---------- indentation helpers: edits match the file's existing style ----------
|
|
@@ -454,13 +472,20 @@ var CanvasEditor = class {
|
|
|
454
472
|
}
|
|
455
473
|
return this.lineIndent(parent.start) + this.unit();
|
|
456
474
|
}
|
|
457
|
-
/**
|
|
475
|
+
/**
|
|
476
|
+
* Re-indents lines 2..n of a snippet so it sits at `indent`. Lines that start
|
|
477
|
+
* inside a multi-line string or template literal are content, not code: they
|
|
478
|
+
* are kept exactly as written.
|
|
479
|
+
*/
|
|
458
480
|
shift(code, indent) {
|
|
459
|
-
const
|
|
460
|
-
const
|
|
481
|
+
const trimmed = code.trim();
|
|
482
|
+
const lines = trimmed.split("\n");
|
|
483
|
+
const frozen = literalLines(trimmed);
|
|
484
|
+
const free = (i) => !frozen.has(i);
|
|
485
|
+
const rest = lines.slice(1).filter((l, i) => l.trim() && free(i + 1));
|
|
461
486
|
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);
|
|
487
|
+
const relative = lines.slice(1).map((l, i) => free(i + 1) ? l.trim() ? l.slice(base) : "" : l);
|
|
488
|
+
const steps = relative.filter((_, i) => free(i + 1)).map((l) => /^ */.exec(l)[0].length).filter((n) => n > 0);
|
|
464
489
|
const from = steps.length ? Math.min(...steps) : 0;
|
|
465
490
|
const unit = this.unit();
|
|
466
491
|
const convert = (l) => {
|
|
@@ -468,7 +493,7 @@ var CanvasEditor = class {
|
|
|
468
493
|
const n = /^ */.exec(l)[0].length;
|
|
469
494
|
return unit.repeat(Math.floor(n / from)) + " ".repeat(n % from) + l.slice(n);
|
|
470
495
|
};
|
|
471
|
-
return [lines[0], ...relative.map((l) => l ? indent + convert(l) : "")].join("\n");
|
|
496
|
+
return [lines[0], ...relative.map((l, i) => !free(i + 1) ? l : l ? indent + convert(l) : "")].join("\n");
|
|
472
497
|
}
|
|
473
498
|
openToClose(node, inner) {
|
|
474
499
|
let end = node.openEnd - 2;
|
|
@@ -532,16 +557,18 @@ ${pi}`);
|
|
|
532
557
|
this.placed.push({ x: right + 80, y: f.y, width: f.width, height: h });
|
|
533
558
|
code = rewriteFrameProps(code, { name, x: right + 80 });
|
|
534
559
|
}
|
|
560
|
+
const marked = withMarker(code.trim(), node);
|
|
535
561
|
this.s.appendLeft(this.contentEnd(node), `
|
|
536
|
-
${this.lineIndent(node.start)}${
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
562
|
+
${this.lineIndent(node.start)}${marked}`);
|
|
563
|
+
if (marked === code.trim()) {
|
|
564
|
+
const path15 = [...node.path];
|
|
565
|
+
path15[path15.length - 1] += 1;
|
|
566
|
+
this.selectPaths.push(path15);
|
|
567
|
+
}
|
|
540
568
|
}
|
|
541
569
|
move(node, target, index) {
|
|
542
570
|
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;
|
|
571
|
+
const code = withMarker(this.source.slice(this.contentStart(node), this.contentEnd(node)), node);
|
|
545
572
|
const kids = target.children.filter((c) => c.id !== node.id);
|
|
546
573
|
const i = index === void 0 || index < 0 || index > kids.length ? kids.length : index;
|
|
547
574
|
this.remove(node);
|
|
@@ -583,7 +610,7 @@ ${ind}${close}`);
|
|
|
583
610
|
this.remove(node);
|
|
584
611
|
return;
|
|
585
612
|
}
|
|
586
|
-
const inner = this.source.slice(
|
|
613
|
+
const inner = node.selfClosing ? "" : this.source.slice(node.openEnd, node.closeStart).trim();
|
|
587
614
|
this.s.overwrite(this.contentStart(node), this.contentEnd(node), this.shift(inner, this.lineIndent(this.contentStart(node))));
|
|
588
615
|
this.selectPaths.push(node.path);
|
|
589
616
|
}
|
|
@@ -601,8 +628,36 @@ ${ind}${close}`);
|
|
|
601
628
|
this.s.appendLeft(same.lastSpecifierEnd, `, ${name}`);
|
|
602
629
|
return;
|
|
603
630
|
}
|
|
631
|
+
this.addImportLine(`import { ${name} } from ${JSON.stringify(from)};`);
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Imports `name` from `from` and returns the local name to write in JSX. When
|
|
635
|
+
* the name is already taken by something else (another import, a local
|
|
636
|
+
* component), it is imported under `alias` instead: `{ Search as SearchIcon }`.
|
|
637
|
+
*/
|
|
638
|
+
importAs(name, from, alias) {
|
|
639
|
+
const existing = this.doc.imports.find((i) => i.source === from && !i.typeOnly)?.bindings.find((b) => b.imported === name);
|
|
640
|
+
if (existing) return existing.local;
|
|
641
|
+
const key = `${from}\0${name}`;
|
|
642
|
+
const pending = this.importedAs.get(key);
|
|
643
|
+
if (pending) return pending;
|
|
644
|
+
const taken = (n) => this.doc.declared.includes(n) || this.added.has(n) || this.doc.imports.some((i) => i.names.includes(n) || i.defaultName === n);
|
|
645
|
+
let local = name;
|
|
646
|
+
if (taken(local)) {
|
|
647
|
+
local = alias;
|
|
648
|
+
for (let k = 2; taken(local); k++) local = `${alias}${k}`;
|
|
649
|
+
}
|
|
650
|
+
this.added.add(local);
|
|
651
|
+
this.importedAs.set(key, local);
|
|
652
|
+
const specifier = local === name ? name : `${name} as ${local}`;
|
|
653
|
+
const same = this.doc.imports.find((i) => i.source === from && !i.typeOnly && i.lastSpecifierEnd !== null);
|
|
654
|
+
if (same) this.s.appendLeft(same.lastSpecifierEnd, `, ${specifier}`);
|
|
655
|
+
else this.addImportLine(`import { ${specifier} } from ${JSON.stringify(from)};`);
|
|
656
|
+
return local;
|
|
657
|
+
}
|
|
658
|
+
importedAs = /* @__PURE__ */ new Map();
|
|
659
|
+
addImportLine(line) {
|
|
604
660
|
const last = this.doc.imports[this.doc.imports.length - 1];
|
|
605
|
-
const line = `import { ${name} } from ${JSON.stringify(from)};`;
|
|
606
661
|
if (last) this.s.appendLeft(last.end, `
|
|
607
662
|
${line}`);
|
|
608
663
|
else if (this.doc.prologueEnd) this.s.appendLeft(this.doc.prologueEnd, `
|
|
@@ -622,12 +677,7 @@ ${line}`);
|
|
|
622
677
|
const known = /* @__PURE__ */ new Set([...this.doc.imports.flatMap((i) => [...i.names, ...i.defaultName ? [i.defaultName] : []]), ...this.added]);
|
|
623
678
|
if (names.length && names.every((n) => known.has(n))) return;
|
|
624
679
|
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);
|
|
680
|
+
this.addImportLine(statement);
|
|
631
681
|
}
|
|
632
682
|
importsPrepended = false;
|
|
633
683
|
/** First imports of a file: in order, with a blank line before the code. */
|
|
@@ -683,6 +733,43 @@ function componentNamesIn(jsx) {
|
|
|
683
733
|
});
|
|
684
734
|
return [...names];
|
|
685
735
|
}
|
|
736
|
+
function withMarker(code, node) {
|
|
737
|
+
if (node.kind !== "component" && node.kind !== "element") return code;
|
|
738
|
+
const m = /^<[\w.:-]+/.exec(code);
|
|
739
|
+
if (!m) return code;
|
|
740
|
+
let i = m[0].length;
|
|
741
|
+
if (code[i] === "<") {
|
|
742
|
+
for (let depth = 0; i < code.length; i++) {
|
|
743
|
+
if (code[i] === "<") depth++;
|
|
744
|
+
else if (code[i] === ">" && --depth === 0) {
|
|
745
|
+
i++;
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return `${code.slice(0, i)} __tcsel${code.slice(i)}`;
|
|
751
|
+
}
|
|
752
|
+
function literalLines(code) {
|
|
753
|
+
const out = /* @__PURE__ */ new Set();
|
|
754
|
+
if (!code.includes("\n") || !/[`"']/.test(code)) return out;
|
|
755
|
+
let ast;
|
|
756
|
+
try {
|
|
757
|
+
ast = parseModule(`<>${code}</>`);
|
|
758
|
+
} catch {
|
|
759
|
+
return out;
|
|
760
|
+
}
|
|
761
|
+
const ranges = [];
|
|
762
|
+
walk(ast, (n) => {
|
|
763
|
+
if (n.type === "TemplateLiteral" || n.type === "StringLiteral") ranges.push([n.start - 2, n.end - 2]);
|
|
764
|
+
});
|
|
765
|
+
if (!ranges.length) return out;
|
|
766
|
+
let offset = 0;
|
|
767
|
+
code.split("\n").forEach((line, i) => {
|
|
768
|
+
if (i > 0 && ranges.some(([a, b]) => offset > a && offset < b)) out.add(i);
|
|
769
|
+
offset += line.length + 1;
|
|
770
|
+
});
|
|
771
|
+
return out;
|
|
772
|
+
}
|
|
686
773
|
function parseJsx(jsx) {
|
|
687
774
|
try {
|
|
688
775
|
return parseModule(`<>${jsx}</>`);
|
|
@@ -725,20 +812,208 @@ function findDevice(id) {
|
|
|
725
812
|
}
|
|
726
813
|
|
|
727
814
|
// src/core/comments.ts
|
|
815
|
+
import fs3 from "fs";
|
|
816
|
+
import path3 from "path";
|
|
817
|
+
import { randomUUID } from "crypto";
|
|
818
|
+
|
|
819
|
+
// src/core/scaffold.ts
|
|
728
820
|
import fs2 from "fs";
|
|
729
821
|
import path2 from "path";
|
|
730
|
-
|
|
822
|
+
var CANVAS_SUFFIX = ".canvas.tsx";
|
|
823
|
+
var ROUTE = "truecanvas";
|
|
824
|
+
function canvasFiles(config) {
|
|
825
|
+
const dir = path2.join(config.root, config.canvasDir);
|
|
826
|
+
if (!fs2.existsSync(dir)) return [];
|
|
827
|
+
return fs2.readdirSync(dir).filter((f) => f.endsWith(CANVAS_SUFFIX)).sort((a, b) => canvasNameOf(a).localeCompare(canvasNameOf(b)));
|
|
828
|
+
}
|
|
829
|
+
function canvasNameOf(file) {
|
|
830
|
+
return path2.basename(file).slice(0, -CANVAS_SUFFIX.length);
|
|
831
|
+
}
|
|
832
|
+
function assertCanvasName(name) {
|
|
833
|
+
if (!/^[a-z0-9][a-z0-9-_]*$/i.test(name)) throw new Error(`Invalid canvas name "${name}". Use letters, digits, - and _.`);
|
|
834
|
+
return name;
|
|
835
|
+
}
|
|
836
|
+
function canvasPath(config, name) {
|
|
837
|
+
assertCanvasName(name);
|
|
838
|
+
return path2.join(config.root, config.canvasDir, `${name}${CANVAS_SUFFIX}`);
|
|
839
|
+
}
|
|
840
|
+
function writeIfChanged(file, content) {
|
|
841
|
+
if (fs2.existsSync(file) && fs2.readFileSync(file, "utf8") === content) return false;
|
|
842
|
+
fs2.mkdirSync(path2.dirname(file), { recursive: true });
|
|
843
|
+
fs2.writeFileSync(file, content);
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
function syncRoute(config) {
|
|
847
|
+
const routeDir = path2.join(config.root, config.appDir, ROUTE);
|
|
848
|
+
const pageFile = path2.join(routeDir, "[canvas]", "page.tsx");
|
|
849
|
+
const registryFile = path2.join(routeDir, "canvases.tsx");
|
|
850
|
+
const canvasDirAbs = path2.join(config.root, config.canvasDir);
|
|
851
|
+
let rel = path2.relative(routeDir, canvasDirAbs);
|
|
852
|
+
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
853
|
+
const compareDir = path2.join(routeDir, "compare");
|
|
854
|
+
const compares = fs2.existsSync(compareDir) ? fs2.readdirSync(compareDir).filter((f) => f.endsWith(CANVAS_SUFFIX)) : [];
|
|
855
|
+
const entries = [
|
|
856
|
+
...canvasFiles(config).map((f) => ` ${JSON.stringify(canvasNameOf(f))}: lazy(() => import(${JSON.stringify(`${rel}/${f.replace(/\.tsx$/, "")}`)})),`),
|
|
857
|
+
// older versions rendered for Compare
|
|
858
|
+
...compares.map((f) => ` ${JSON.stringify(`__compare__${canvasNameOf(f)}`)}: lazy(() => import(${JSON.stringify(`./compare/${f.replace(/\.tsx$/, "")}`)})),`)
|
|
859
|
+
].join("\n");
|
|
860
|
+
const registry = `"use client";
|
|
861
|
+
/* eslint-disable */
|
|
862
|
+
// Generated by Truecanvas. Do not edit: it is regenerated when canvases are added or removed.
|
|
863
|
+
import { lazy } from "react";
|
|
864
|
+
import { TruecanvasHost, type HostProps } from "truecanvas/host";
|
|
865
|
+
import { components } from "./components";
|
|
866
|
+
|
|
867
|
+
const canvases = {
|
|
868
|
+
${entries}
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
export default function Canvases(props: Omit<HostProps, "canvases" | "components">) {
|
|
872
|
+
return <TruecanvasHost {...props} canvases={canvases} components={components} />;
|
|
873
|
+
}
|
|
874
|
+
`;
|
|
875
|
+
const dm = config.darkMode;
|
|
876
|
+
const page = `/* eslint-disable */
|
|
877
|
+
// Generated by Truecanvas. Dev-only route that renders canvas frames.
|
|
878
|
+
import { notFound } from "next/navigation";
|
|
879
|
+
import Canvases from "../canvases";
|
|
880
|
+
|
|
881
|
+
export const dynamic = "force-dynamic";
|
|
882
|
+
|
|
883
|
+
export default async function TruecanvasFrame({
|
|
884
|
+
params,
|
|
885
|
+
searchParams,
|
|
886
|
+
}: {
|
|
887
|
+
params: Promise<{ canvas: string }>;
|
|
888
|
+
searchParams: Promise<Record<string, string | undefined>>;
|
|
889
|
+
}) {
|
|
890
|
+
if (process.env.NODE_ENV !== "development") notFound();
|
|
891
|
+
const { canvas } = await params;
|
|
892
|
+
const query = await searchParams;
|
|
893
|
+
return (
|
|
894
|
+
<Canvases
|
|
895
|
+
canvas={canvas}
|
|
896
|
+
frame={query.frame ?? null}
|
|
897
|
+
theme={(query.theme as "light" | "dark" | "system" | undefined) ?? "system"}
|
|
898
|
+
darkMode={${JSON.stringify(dm)}}
|
|
899
|
+
/>
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
`;
|
|
903
|
+
const componentsFile = path2.join(routeDir, "components.tsx");
|
|
904
|
+
if (!fs2.existsSync(componentsFile)) writeIfChanged(componentsFile, componentRegistry(config, []));
|
|
905
|
+
const a = writeIfChanged(registryFile, registry);
|
|
906
|
+
const b = writeIfChanged(pageFile, page);
|
|
907
|
+
return a || b;
|
|
908
|
+
}
|
|
909
|
+
var STARTER_CANVAS = `"use client";
|
|
910
|
+
import { Canvas, Frame } from "truecanvas";
|
|
911
|
+
|
|
912
|
+
/*
|
|
913
|
+
* A placeholder page. Replace the Welcome frame with your own components:
|
|
914
|
+
* drag them from Assets, draw frames with F, or ask your agent over MCP.
|
|
915
|
+
* Inline styles keep it readable in any app, with or without Tailwind.
|
|
916
|
+
*/
|
|
917
|
+
export default function StarterCanvas() {
|
|
918
|
+
return (
|
|
919
|
+
<Canvas>
|
|
920
|
+
<Frame name="Welcome" x={0} y={0} width={1200} height={760}>
|
|
921
|
+
<div
|
|
922
|
+
style={{
|
|
923
|
+
height: "100%",
|
|
924
|
+
display: "flex",
|
|
925
|
+
alignItems: "center",
|
|
926
|
+
justifyContent: "center",
|
|
927
|
+
fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif",
|
|
928
|
+
color: "light-dark(#1d1d1b, #ededea)",
|
|
929
|
+
backgroundColor: "light-dark(#f6f5f2, #121211)",
|
|
930
|
+
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%)",
|
|
931
|
+
backgroundSize: "22px 22px, 100% 100%",
|
|
932
|
+
}}
|
|
933
|
+
>
|
|
934
|
+
<div
|
|
935
|
+
style={{
|
|
936
|
+
width: 520,
|
|
937
|
+
padding: 40,
|
|
938
|
+
borderRadius: 24,
|
|
939
|
+
backgroundColor: "light-dark(#ffffff, #1b1b1a)",
|
|
940
|
+
border: "1px solid light-dark(#e9e8e4, #2a2a28)",
|
|
941
|
+
boxShadow: "0 1px 2px rgba(0,0,0,.04), 0 24px 60px -24px rgba(0,0,0,.25)",
|
|
942
|
+
}}
|
|
943
|
+
>
|
|
944
|
+
<svg width="44" height="44" viewBox="0 0 64 64" aria-hidden>
|
|
945
|
+
<defs>
|
|
946
|
+
<linearGradient id="tc-starter" x1="0" y1="0" x2="0" y2="1">
|
|
947
|
+
<stop offset="0" stopColor="#ff9466" />
|
|
948
|
+
<stop offset="1" stopColor="#ea6a3c" />
|
|
949
|
+
</linearGradient>
|
|
950
|
+
</defs>
|
|
951
|
+
<rect width="64" height="64" rx="15" fill="url(#tc-starter)" />
|
|
952
|
+
<path fill="#f6f5f1" fillRule="evenodd" d="M10.5 10.5h43v13.1h-43z M25.5 16.8h13v36.7h-13z" />
|
|
953
|
+
</svg>
|
|
954
|
+
<h1 style={{ margin: "22px 0 6px", fontSize: 26, lineHeight: "32px", fontWeight: 650, letterSpacing: "-0.02em" }}>Your canvas is ready</h1>
|
|
955
|
+
<p style={{ margin: 0, fontSize: 15, lineHeight: "23px", color: "light-dark(#6b6b66, #a3a39d)" }}>
|
|
956
|
+
Everything here is real code from your app. Replace this frame with your own components.
|
|
957
|
+
</p>
|
|
958
|
+
<div style={{ marginTop: 28, display: "flex", flexDirection: "column", gap: 14 }}>
|
|
959
|
+
<div style={{ display: "flex", alignItems: "center", gap: 14, fontSize: 14 }}>
|
|
960
|
+
<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>
|
|
961
|
+
<span>Drag a component from your design system onto a frame</span>
|
|
962
|
+
</div>
|
|
963
|
+
<div style={{ display: "flex", alignItems: "center", gap: 14, fontSize: 14 }}>
|
|
964
|
+
<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>
|
|
965
|
+
<span>Draw a frame, or pick an iPhone or desktop size</span>
|
|
966
|
+
</div>
|
|
967
|
+
<div style={{ display: "flex", alignItems: "center", gap: 14, fontSize: 14 }}>
|
|
968
|
+
<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>
|
|
969
|
+
<span>Ask: \u201CBuild a pricing page with our components\u201D</span>
|
|
970
|
+
</div>
|
|
971
|
+
</div>
|
|
972
|
+
</div>
|
|
973
|
+
</div>
|
|
974
|
+
</Frame>
|
|
975
|
+
</Canvas>
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
`;
|
|
979
|
+
function componentRegistry(config, specs) {
|
|
980
|
+
const routeDir = path2.join(config.root, config.appDir, ROUTE);
|
|
981
|
+
const lines = specs.map((c) => {
|
|
982
|
+
let from = c.library;
|
|
983
|
+
if (!from) {
|
|
984
|
+
from = path2.relative(routeDir, path2.join(config.root, c.file)).replace(/\.(tsx|ts|jsx|js)$/, "");
|
|
985
|
+
if (!from.startsWith(".")) from = `./${from}`;
|
|
986
|
+
}
|
|
987
|
+
return ` ${JSON.stringify(c.name)}: lazy(() => import(${JSON.stringify(from)}).then((m) => ({ default: m[${JSON.stringify(c.name)}] }))),`;
|
|
988
|
+
});
|
|
989
|
+
return `"use client";
|
|
990
|
+
/* eslint-disable */
|
|
991
|
+
// Generated by Truecanvas. Do not edit: it is regenerated when components change.
|
|
992
|
+
import { lazy, type ComponentType } from "react";
|
|
993
|
+
|
|
994
|
+
// any: components with required props are rendered with whatever the preview passes
|
|
995
|
+
export const components: Record<string, ComponentType<any>> = {
|
|
996
|
+
${lines.join("\n")}
|
|
997
|
+
};
|
|
998
|
+
`;
|
|
999
|
+
}
|
|
1000
|
+
function syncComponentRegistry(config, specs) {
|
|
1001
|
+
const file = path2.join(config.root, config.appDir, ROUTE, "components.tsx");
|
|
1002
|
+
return writeIfChanged(file, componentRegistry(config, specs));
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// src/core/comments.ts
|
|
731
1006
|
var Comments = class {
|
|
732
1007
|
constructor(config) {
|
|
733
1008
|
this.config = config;
|
|
734
1009
|
}
|
|
735
1010
|
config;
|
|
736
1011
|
file(canvas) {
|
|
737
|
-
return
|
|
1012
|
+
return path3.join(this.config.root, this.config.canvasDir, `${assertCanvasName(canvas)}.comments.json`);
|
|
738
1013
|
}
|
|
739
1014
|
list(canvas) {
|
|
740
1015
|
try {
|
|
741
|
-
return JSON.parse(
|
|
1016
|
+
return JSON.parse(fs3.readFileSync(this.file(canvas), "utf8")).threads ?? [];
|
|
742
1017
|
} catch {
|
|
743
1018
|
return [];
|
|
744
1019
|
}
|
|
@@ -746,17 +1021,18 @@ var Comments = class {
|
|
|
746
1021
|
save(canvas, threads) {
|
|
747
1022
|
const file = this.file(canvas);
|
|
748
1023
|
if (!threads.length) {
|
|
749
|
-
if (
|
|
1024
|
+
if (fs3.existsSync(file)) fs3.rmSync(file);
|
|
750
1025
|
return;
|
|
751
1026
|
}
|
|
752
|
-
|
|
753
|
-
|
|
1027
|
+
fs3.mkdirSync(path3.dirname(file), { recursive: true });
|
|
1028
|
+
fs3.writeFileSync(file, `${JSON.stringify({ version: 1, threads }, null, 2)}
|
|
754
1029
|
`);
|
|
755
1030
|
}
|
|
756
1031
|
update(canvas, id, fn) {
|
|
757
1032
|
const threads = this.list(canvas);
|
|
758
|
-
const
|
|
759
|
-
|
|
1033
|
+
const prefixed = id.length >= 4 ? threads.filter((x) => x.id.startsWith(id)) : [];
|
|
1034
|
+
const t = threads.find((x) => x.id === id) ?? (prefixed.length === 1 ? prefixed[0] : void 0);
|
|
1035
|
+
if (!t) throw new Error(prefixed.length > 1 ? `"${id}" matches several comment threads. Use the full id.` : `No comment thread "${id}".`);
|
|
760
1036
|
fn(t);
|
|
761
1037
|
this.save(canvas, threads);
|
|
762
1038
|
return t;
|
|
@@ -804,13 +1080,14 @@ var Comments = class {
|
|
|
804
1080
|
}
|
|
805
1081
|
renameCanvas(from, to) {
|
|
806
1082
|
const a = this.file(from);
|
|
807
|
-
if (
|
|
1083
|
+
if (fs3.existsSync(a)) fs3.renameSync(a, this.file(to));
|
|
808
1084
|
}
|
|
809
1085
|
};
|
|
810
1086
|
|
|
811
1087
|
// src/core/git.ts
|
|
812
1088
|
import { execFile } from "child_process";
|
|
813
|
-
import
|
|
1089
|
+
import fs4 from "fs";
|
|
1090
|
+
import path4 from "path";
|
|
814
1091
|
var GitError = class extends EditError {
|
|
815
1092
|
};
|
|
816
1093
|
function git(cwd, args, timeout = 3e4, opts = {}) {
|
|
@@ -833,6 +1110,10 @@ function run(cmd, cwd, args, timeout, opts = {}) {
|
|
|
833
1110
|
if (opts.input !== void 0) child.stdin?.end(opts.input);
|
|
834
1111
|
});
|
|
835
1112
|
}
|
|
1113
|
+
function safeRef(ref) {
|
|
1114
|
+
if (!ref || ref.startsWith("-") || !/^[\w./~^@{}+-]+$/.test(ref) || ref.includes("..")) throw new GitError(`Invalid git ref "${ref}".`);
|
|
1115
|
+
return ref;
|
|
1116
|
+
}
|
|
836
1117
|
var Git = class {
|
|
837
1118
|
constructor(cwd) {
|
|
838
1119
|
this.cwd = cwd;
|
|
@@ -845,9 +1126,11 @@ var Git = class {
|
|
|
845
1126
|
} catch {
|
|
846
1127
|
return { repo: false, root: null, branch: null, unborn: false, upstream: null, ahead: 0, behind: 0, files: [], user: null, defaultBranch: null, remote: null };
|
|
847
1128
|
}
|
|
848
|
-
const out = await git(this.cwd, ["status", "--porcelain=v2", "--branch", "--untracked-files=all", "--", "."]);
|
|
1129
|
+
const out = await git(this.cwd, ["status", "--porcelain=v2", "-z", "--branch", "--untracked-files=all", "--", "."]);
|
|
849
1130
|
const s = { repo: true, root, branch: null, unborn: false, upstream: null, ahead: 0, behind: 0, files: [], user: null, defaultBranch: null, remote: null };
|
|
850
|
-
|
|
1131
|
+
const entries = out.split("\0");
|
|
1132
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1133
|
+
const line = entries[i];
|
|
851
1134
|
if (line.startsWith("# branch.head ")) s.branch = line.slice(14) === "(detached)" ? null : line.slice(14);
|
|
852
1135
|
else if (line.startsWith("# branch.oid ")) s.unborn = line.slice(13) === "(initial)";
|
|
853
1136
|
else if (line.startsWith("# branch.upstream ")) s.upstream = line.slice(18);
|
|
@@ -857,9 +1140,12 @@ var Git = class {
|
|
|
857
1140
|
} else if (line.startsWith("1 ") || line.startsWith("2 ")) {
|
|
858
1141
|
const parts = line.split(" ");
|
|
859
1142
|
const xy = parts[1];
|
|
860
|
-
const file = line.startsWith("2 ") ?
|
|
1143
|
+
const file = parts.slice(line.startsWith("2 ") ? 9 : 8).join(" ");
|
|
1144
|
+
if (line.startsWith("2 ")) i++;
|
|
861
1145
|
const code = xy.replace(".", "")[0] ?? "M";
|
|
862
1146
|
s.files.push({ path: this.rel(root, file), status: code === "A" ? "added" : code === "D" ? "deleted" : code === "R" ? "renamed" : "modified", staged: xy[0] !== "." });
|
|
1147
|
+
} else if (line.startsWith("u ")) {
|
|
1148
|
+
s.files.push({ path: this.rel(root, line.split(" ").slice(10).join(" ")), status: "conflicted", staged: false });
|
|
863
1149
|
} else if (line.startsWith("? ")) s.files.push({ path: this.rel(root, line.slice(2)), status: "untracked", staged: false });
|
|
864
1150
|
}
|
|
865
1151
|
s.user = (await git(this.cwd, ["config", "user.name"]).catch(() => "")).trim() || null;
|
|
@@ -881,11 +1167,12 @@ var Git = class {
|
|
|
881
1167
|
}
|
|
882
1168
|
/** When a commit was made (ms). */
|
|
883
1169
|
async commitAt(ref) {
|
|
884
|
-
const out = await git(this.cwd, ["log", "-1", "--format=%ct", ref]).catch(() => "");
|
|
1170
|
+
const out = await git(this.cwd, ["log", "-1", "--format=%ct", safeRef(ref), "--"]).catch(() => "");
|
|
885
1171
|
return Number(out.trim() || 0) * 1e3;
|
|
886
1172
|
}
|
|
887
1173
|
/** Commit where this branch left `base`. */
|
|
888
1174
|
async mergeBase(base) {
|
|
1175
|
+
safeRef(base);
|
|
889
1176
|
for (const ref of [`origin/${base}`, base]) {
|
|
890
1177
|
const out = await git(this.cwd, ["merge-base", ref, "HEAD"]).catch(() => "");
|
|
891
1178
|
if (out.trim()) return out.trim();
|
|
@@ -894,7 +1181,7 @@ var Git = class {
|
|
|
894
1181
|
}
|
|
895
1182
|
/** Files changed between a commit and HEAD, from the project root. */
|
|
896
1183
|
async changedSince(ref) {
|
|
897
|
-
const out = await git(this.cwd, ["diff", "--name-only", "--relative", `${ref}...HEAD
|
|
1184
|
+
const out = await git(this.cwd, ["diff", "--name-only", "--relative", `${safeRef(ref)}...HEAD`, "--"]).catch(() => "");
|
|
898
1185
|
return out.split("\n").filter(Boolean);
|
|
899
1186
|
}
|
|
900
1187
|
/** Unified diff of these files against HEAD, including new files. */
|
|
@@ -911,10 +1198,11 @@ var Git = class {
|
|
|
911
1198
|
* (used for PR screenshots), and pushes it. Returns the branch tip.
|
|
912
1199
|
*/
|
|
913
1200
|
async commitToSideBranch(branch, files, message) {
|
|
1201
|
+
safeRef(branch);
|
|
914
1202
|
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
1203
|
const os = await import("os");
|
|
916
|
-
const
|
|
917
|
-
const index =
|
|
1204
|
+
const fs15 = await import("fs");
|
|
1205
|
+
const index = path4.join(os.tmpdir(), `truecanvas-index-${process.pid}-${Date.now()}`);
|
|
918
1206
|
const env = { GIT_INDEX_FILE: index };
|
|
919
1207
|
try {
|
|
920
1208
|
if (tip) await git(this.cwd, ["read-tree", tip], 3e4, { env });
|
|
@@ -928,7 +1216,7 @@ var Git = class {
|
|
|
928
1216
|
await git(this.cwd, ["update-ref", `refs/heads/${branch}`, commit]);
|
|
929
1217
|
await git(this.cwd, ["push", "origin", `refs/heads/${branch}:refs/heads/${branch}`], 12e4);
|
|
930
1218
|
} finally {
|
|
931
|
-
|
|
1219
|
+
fs15.rmSync(index, { force: true });
|
|
932
1220
|
}
|
|
933
1221
|
}
|
|
934
1222
|
// ---------- GitHub (gh CLI) ----------
|
|
@@ -952,7 +1240,7 @@ var Git = class {
|
|
|
952
1240
|
return { number: pr.number, url: pr.url, title: pr.title, state: pr.state, draft: pr.isDraft, review: pr.reviewDecision || null, checks };
|
|
953
1241
|
}
|
|
954
1242
|
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 });
|
|
1243
|
+
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
1244
|
return out.trim().split("\n").pop() ?? "";
|
|
957
1245
|
}
|
|
958
1246
|
/** Creates a GitHub repo for this project and pushes it (gh repo create). */
|
|
@@ -963,7 +1251,18 @@ var Git = class {
|
|
|
963
1251
|
}
|
|
964
1252
|
/** git reports paths from the repo root; we show them from the project root. */
|
|
965
1253
|
rel(root, file) {
|
|
966
|
-
return
|
|
1254
|
+
return path4.relative(this.realCwd(), path4.join(root, file));
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* The project folder as git sees it: git reports real paths, so a folder
|
|
1258
|
+
* reached through a symlink (macOS /var → /private/var) must be resolved too.
|
|
1259
|
+
*/
|
|
1260
|
+
realCwd() {
|
|
1261
|
+
try {
|
|
1262
|
+
return fs4.realpathSync(this.cwd);
|
|
1263
|
+
} catch {
|
|
1264
|
+
return this.cwd;
|
|
1265
|
+
}
|
|
967
1266
|
}
|
|
968
1267
|
async branches() {
|
|
969
1268
|
const out = await git(this.cwd, ["branch", "-a", "--format=%(refname)%09%(HEAD)"]).catch(() => "");
|
|
@@ -992,8 +1291,8 @@ var Git = class {
|
|
|
992
1291
|
/** A file's content at a ref, or null if it doesn't exist there. */
|
|
993
1292
|
async show(ref, file) {
|
|
994
1293
|
const root = (await git(this.cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
995
|
-
const inRepo =
|
|
996
|
-
return git(this.cwd, ["show", `${ref}:${inRepo}
|
|
1294
|
+
const inRepo = path4.relative(root, path4.join(this.realCwd(), file)).split(path4.sep).join("/");
|
|
1295
|
+
return git(this.cwd, ["show", `${safeRef(ref)}:${inRepo}`, "--"]).catch(() => null);
|
|
997
1296
|
}
|
|
998
1297
|
async commit(message, files) {
|
|
999
1298
|
if (!message.trim()) throw new GitError("Write a commit message.");
|
|
@@ -1015,23 +1314,23 @@ var Git = class {
|
|
|
1015
1314
|
});
|
|
1016
1315
|
}
|
|
1017
1316
|
async switch(branch, create = false) {
|
|
1018
|
-
|
|
1317
|
+
safeRef(branch);
|
|
1019
1318
|
await git(this.cwd, create ? ["switch", "-c", branch] : ["switch", branch]);
|
|
1020
1319
|
}
|
|
1021
1320
|
};
|
|
1022
1321
|
|
|
1023
1322
|
// src/core/workspace.ts
|
|
1024
|
-
import
|
|
1323
|
+
import fs9 from "fs";
|
|
1025
1324
|
import path9 from "path";
|
|
1026
1325
|
import { createRequire } from "module";
|
|
1027
1326
|
|
|
1028
1327
|
// src/core/link.ts
|
|
1029
|
-
import
|
|
1030
|
-
import
|
|
1328
|
+
import fs6 from "fs";
|
|
1329
|
+
import path6 from "path";
|
|
1031
1330
|
|
|
1032
1331
|
// src/core/routes.ts
|
|
1033
|
-
import
|
|
1034
|
-
import
|
|
1332
|
+
import fs5 from "fs";
|
|
1333
|
+
import path5 from "path";
|
|
1035
1334
|
|
|
1036
1335
|
// src/core/source.ts
|
|
1037
1336
|
function defaultReturn(source, layout) {
|
|
@@ -1243,28 +1542,28 @@ function parseTargetDoc(file, source) {
|
|
|
1243
1542
|
var PAGE = /^page\.(tsx|jsx|ts|js)$/;
|
|
1244
1543
|
var LAYOUT = ["layout.tsx", "layout.jsx", "layout.js", "layout.ts"];
|
|
1245
1544
|
function listRoutes(config) {
|
|
1246
|
-
const appDir =
|
|
1545
|
+
const appDir = path5.join(config.root, config.appDir);
|
|
1247
1546
|
const out = [];
|
|
1248
1547
|
const walkDir = (dir, segments) => {
|
|
1249
1548
|
let entries;
|
|
1250
1549
|
try {
|
|
1251
|
-
entries =
|
|
1550
|
+
entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
1252
1551
|
} catch {
|
|
1253
1552
|
return;
|
|
1254
1553
|
}
|
|
1255
1554
|
for (const e of entries) {
|
|
1256
1555
|
if (e.isFile() && PAGE.test(e.name)) {
|
|
1257
1556
|
const visible = segments.filter((s) => !/^\(.*\)$/.test(s) && !s.startsWith("@"));
|
|
1258
|
-
out.push({ route: `/${visible.join("/")}`, file:
|
|
1557
|
+
out.push({ route: `/${visible.join("/")}`, file: path5.relative(config.root, path5.join(dir, e.name)), dynamic: segments.some((s) => s.startsWith("[")) });
|
|
1259
1558
|
}
|
|
1260
|
-
if (e.isDirectory() && !e.name.startsWith("_") && e.name !== "truecanvas" && e.name !== "api" && e.name !== "node_modules") walkDir(
|
|
1559
|
+
if (e.isDirectory() && !e.name.startsWith("_") && e.name !== "truecanvas" && e.name !== "api" && e.name !== "node_modules") walkDir(path5.join(dir, e.name), [...segments, e.name]);
|
|
1261
1560
|
}
|
|
1262
1561
|
};
|
|
1263
1562
|
walkDir(appDir, []);
|
|
1264
1563
|
return out.sort((a, b) => a.route.localeCompare(b.route));
|
|
1265
1564
|
}
|
|
1266
1565
|
function extract(file, canvasFile, config, layout) {
|
|
1267
|
-
const source =
|
|
1566
|
+
const source = fs5.readFileSync(file, "utf8");
|
|
1268
1567
|
const info = defaultReturn(source, layout);
|
|
1269
1568
|
if (!info?.pure) return null;
|
|
1270
1569
|
const imports = [];
|
|
@@ -1274,32 +1573,32 @@ function extract(file, canvasFile, config, layout) {
|
|
|
1274
1573
|
if (/\.(css|scss|sass|less)$/.test(src)) continue;
|
|
1275
1574
|
const from = repoint(config, src, file, canvasFile);
|
|
1276
1575
|
const text = source.slice(stmt.start, stmt.end);
|
|
1277
|
-
imports.push(from === src ? text : text
|
|
1576
|
+
imports.push(from === src ? text : rewriteImportSource(text, src, from));
|
|
1278
1577
|
}
|
|
1279
1578
|
return { jsx: source.slice(info.node.start, info.node.end), imports };
|
|
1280
1579
|
}
|
|
1281
1580
|
function repoint(config, spec, fromFile, toFile) {
|
|
1282
1581
|
if (!spec.startsWith(".")) return spec;
|
|
1283
|
-
return importSpecifier(config.root,
|
|
1582
|
+
return importSpecifier(config.root, path5.relative(config.root, toFile), path5.relative(config.root, path5.resolve(path5.dirname(fromFile), spec)));
|
|
1284
1583
|
}
|
|
1285
1584
|
function layoutFiles(config, pageFile) {
|
|
1286
|
-
const appDir =
|
|
1585
|
+
const appDir = path5.join(config.root, config.appDir);
|
|
1287
1586
|
const out = [];
|
|
1288
|
-
for (let dir =
|
|
1289
|
-
const file = LAYOUT.map((f) =>
|
|
1290
|
-
if (file) out.push(
|
|
1587
|
+
for (let dir = path5.dirname(path5.resolve(config.root, pageFile)); dir.startsWith(appDir) && dir !== appDir; dir = path5.dirname(dir)) {
|
|
1588
|
+
const file = LAYOUT.map((f) => path5.join(dir, f)).find((f) => fs5.existsSync(f));
|
|
1589
|
+
if (file) out.push(path5.relative(config.root, file));
|
|
1291
1590
|
}
|
|
1292
1591
|
return out;
|
|
1293
1592
|
}
|
|
1294
1593
|
function routeOf(config, pageFile) {
|
|
1295
|
-
const rel =
|
|
1296
|
-
const visible = rel.split(
|
|
1594
|
+
const rel = path5.relative(path5.join(config.root, config.appDir), path5.dirname(path5.resolve(config.root, pageFile)));
|
|
1595
|
+
const visible = rel.split(path5.sep).filter((s) => s && !/^\(.*\)$/.test(s) && !s.startsWith("@"));
|
|
1297
1596
|
return `/${visible.join("/")}`;
|
|
1298
1597
|
}
|
|
1299
1598
|
function routeFrame(config, pageFile, canvasFile) {
|
|
1300
|
-
const appDir =
|
|
1301
|
-
const pageAbs =
|
|
1302
|
-
const canvasAbs =
|
|
1599
|
+
const appDir = path5.join(config.root, config.appDir);
|
|
1600
|
+
const pageAbs = path5.resolve(config.root, pageFile);
|
|
1601
|
+
const canvasAbs = path5.resolve(config.root, canvasFile);
|
|
1303
1602
|
const imports = [];
|
|
1304
1603
|
let mode = "inline";
|
|
1305
1604
|
const page = extract(pageAbs, canvasAbs, config, false);
|
|
@@ -1313,17 +1612,18 @@ function routeFrame(config, pageFile, canvasFile) {
|
|
|
1313
1612
|
jsx = "<RoutePage />";
|
|
1314
1613
|
}
|
|
1315
1614
|
let i = 0;
|
|
1316
|
-
for (let dir =
|
|
1317
|
-
const file = LAYOUT.map((f) =>
|
|
1615
|
+
for (let dir = path5.dirname(pageAbs); dir.startsWith(appDir) && dir !== appDir; dir = path5.dirname(dir)) {
|
|
1616
|
+
const file = LAYOUT.map((f) => path5.join(dir, f)).find((f) => fs5.existsSync(f));
|
|
1318
1617
|
if (!file) continue;
|
|
1319
1618
|
const layout = extract(file, canvasAbs, config, true);
|
|
1320
1619
|
const holes = layout ? (layout.jsx.match(/\{\s*children\s*\}/g) ?? []).length : 0;
|
|
1321
1620
|
if (layout && holes === 1) {
|
|
1322
|
-
|
|
1621
|
+
const inner = jsx.startsWith("<>") ? jsx.slice(2, -3) : jsx;
|
|
1622
|
+
jsx = layout.jsx.replace(/\{\s*children\s*\}/, () => inner);
|
|
1323
1623
|
imports.push(...layout.imports);
|
|
1324
1624
|
} else {
|
|
1325
1625
|
const local = `RouteLayout${i++ || ""}`;
|
|
1326
|
-
imports.push(`import ${local} from ${JSON.stringify(importSpecifier(config.root, canvasFile,
|
|
1626
|
+
imports.push(`import ${local} from ${JSON.stringify(importSpecifier(config.root, canvasFile, path5.relative(config.root, file)))};`);
|
|
1327
1627
|
jsx = `<${local}>${jsx}</${local}>`;
|
|
1328
1628
|
}
|
|
1329
1629
|
}
|
|
@@ -1344,11 +1644,23 @@ function wrapperChain(frame) {
|
|
|
1344
1644
|
}
|
|
1345
1645
|
function readSource(config, rel) {
|
|
1346
1646
|
try {
|
|
1347
|
-
return
|
|
1647
|
+
return fs6.readFileSync(path6.join(config.root, rel), "utf8");
|
|
1348
1648
|
} catch {
|
|
1349
1649
|
return null;
|
|
1350
1650
|
}
|
|
1351
1651
|
}
|
|
1652
|
+
var parseMemo = /* @__PURE__ */ new Map();
|
|
1653
|
+
function memoParse(key, source, parse2) {
|
|
1654
|
+
let hit = parseMemo.get(key);
|
|
1655
|
+
if (!hit || hit.source !== source) {
|
|
1656
|
+
hit = { source, doc: parse2() };
|
|
1657
|
+
parseMemo.set(key, hit);
|
|
1658
|
+
}
|
|
1659
|
+
return hit.doc && { ...hit.doc, frames: hit.doc.frames.map(cloneTree) };
|
|
1660
|
+
}
|
|
1661
|
+
function cloneTree(n) {
|
|
1662
|
+
return { ...n, children: n.children.map(cloneTree) };
|
|
1663
|
+
}
|
|
1352
1664
|
function resolveLinks(config, doc) {
|
|
1353
1665
|
if (!doc.frames.some((f) => f.page || f.component)) return doc;
|
|
1354
1666
|
return { ...doc, frames: doc.frames.map((f) => f.component ? linkComponent(config, f) : f.page ? linkFrame(config, f) : f) };
|
|
@@ -1361,7 +1673,7 @@ function linkComponent(config, original) {
|
|
|
1361
1673
|
const frame = { ...original };
|
|
1362
1674
|
const { file, name } = splitComponentRef(frame.component);
|
|
1363
1675
|
const source = readSource(config, file);
|
|
1364
|
-
const doc = source === null ? null : parseModuleDoc(file, source);
|
|
1676
|
+
const doc = source === null ? null : memoParse(`module:${file}`, source, () => parseModuleDoc(file, source));
|
|
1365
1677
|
const own = doc?.frames.find((f) => f.frameName === name);
|
|
1366
1678
|
if (!own) {
|
|
1367
1679
|
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 +1696,7 @@ function linkFrame(config, original) {
|
|
|
1384
1696
|
const chain = wrapperChain(original);
|
|
1385
1697
|
const files = [];
|
|
1386
1698
|
let current;
|
|
1387
|
-
const pageDoc = parseSourceDoc(page, pageSource, false);
|
|
1699
|
+
const pageDoc = memoParse(`page:${page}`, pageSource, () => parseSourceDoc(page, pageSource, false));
|
|
1388
1700
|
if (pageDoc) {
|
|
1389
1701
|
current = flatten(pageDoc.frames[0].children[0]);
|
|
1390
1702
|
files.push(page);
|
|
@@ -1394,7 +1706,7 @@ function linkFrame(config, original) {
|
|
|
1394
1706
|
const layouts = layoutFiles(config, page);
|
|
1395
1707
|
layouts.forEach((rel, i) => {
|
|
1396
1708
|
const src = readSource(config, rel);
|
|
1397
|
-
const ldoc = src === null ? null : parseSourceDoc(rel, src, true);
|
|
1709
|
+
const ldoc = src === null ? null : memoParse(`layout:${rel}`, src, () => parseSourceDoc(rel, src, true));
|
|
1398
1710
|
const root = ldoc?.frames[0].children[0];
|
|
1399
1711
|
const slot = root && findSlot(root);
|
|
1400
1712
|
if (root && slot) {
|
|
@@ -1443,7 +1755,7 @@ function linkedFrameBody(config, doc, canvasRel, page) {
|
|
|
1443
1755
|
const pageName = importName(config, doc, canvasRel, page, `${segments.length ? pascal(segments[segments.length - 1]) : "Home"}Page`, taken, imports);
|
|
1444
1756
|
let jsx = `<${pageName} />`;
|
|
1445
1757
|
for (const rel of layoutFiles(config, page)) {
|
|
1446
|
-
const dir =
|
|
1758
|
+
const dir = path6.basename(path6.dirname(rel));
|
|
1447
1759
|
const name = importName(config, doc, canvasRel, rel, `${pascal(dir) || "Route"}Layout`, taken, imports);
|
|
1448
1760
|
jsx = `<${name}>
|
|
1449
1761
|
${indentBlock(jsx)}
|
|
@@ -1465,7 +1777,7 @@ function dedent(code) {
|
|
|
1465
1777
|
function explorationBody(config, frame, canvasRel) {
|
|
1466
1778
|
if (frame.component) throw new EditError(`"${frame.frameName}" is a main component: edit it directly, every instance follows.`);
|
|
1467
1779
|
if (!frame.page) throw new EditError(`"${frame.frameName}" isn't linked to a page.`);
|
|
1468
|
-
const pageAbs =
|
|
1780
|
+
const pageAbs = path6.join(config.root, frame.page);
|
|
1469
1781
|
const source = readSource(config, frame.page);
|
|
1470
1782
|
if (source === null) throw new EditError(`${frame.page} doesn't exist anymore.`);
|
|
1471
1783
|
const chain = wrapperChain(frame);
|
|
@@ -1477,9 +1789,9 @@ function explorationBody(config, frame, canvasRel) {
|
|
|
1477
1789
|
for (const stmt of info.ast.program.body) {
|
|
1478
1790
|
if (stmt.type !== "ImportDeclaration" || stmt.importKind === "type" || /\.(css|scss|sass|less)$/.test(stmt.source.value)) continue;
|
|
1479
1791
|
const src = stmt.source.value;
|
|
1480
|
-
const from = repoint(config, src, pageAbs,
|
|
1792
|
+
const from = repoint(config, src, pageAbs, path6.join(config.root, canvasRel));
|
|
1481
1793
|
const text = source.slice(stmt.start, stmt.end);
|
|
1482
|
-
imports.push(from === src ? text : text
|
|
1794
|
+
imports.push(from === src ? text : rewriteImportSource(text, src, from));
|
|
1483
1795
|
}
|
|
1484
1796
|
jsx = unwrapFragment(source.slice(info.node.start, info.node.end));
|
|
1485
1797
|
} else {
|
|
@@ -1493,7 +1805,7 @@ ${indentBlock(jsx)}
|
|
|
1493
1805
|
function applyPlan(config, canvasSource, doc, frame, canvasRel) {
|
|
1494
1806
|
const page = frame.from;
|
|
1495
1807
|
if (!page) throw new EditError(`"${frame.frameName}" isn't an exploration of a page. Explore a copy of a linked frame first.`);
|
|
1496
|
-
if (!
|
|
1808
|
+
if (!fs6.existsSync(path6.join(config.root, page))) throw new EditError(`${page} doesn't exist anymore.`);
|
|
1497
1809
|
const layoutSpecs = new Set(doc.imports.filter((i) => /(^|\/)layout(\.\w+)?$/.test(i.source)).flatMap((i) => [...i.names, ...i.defaultName ? [i.defaultName] : []]));
|
|
1498
1810
|
let content = frame.children;
|
|
1499
1811
|
for (; ; ) {
|
|
@@ -1519,7 +1831,7 @@ ${indentBlock(body)}
|
|
|
1519
1831
|
continue;
|
|
1520
1832
|
}
|
|
1521
1833
|
const sp = decl.specifiers.find((x) => x.local.name === name);
|
|
1522
|
-
const from = repoint(config, decl.source.value,
|
|
1834
|
+
const from = repoint(config, decl.source.value, path6.join(config.root, canvasRel), path6.join(config.root, page));
|
|
1523
1835
|
const what = sp.type === "ImportDefaultSpecifier" ? name : sp.type === "ImportNamespaceSpecifier" ? `* as ${name}` : `{ ${sp.imported.type === "Identifier" && sp.imported.name !== name ? `${sp.imported.name} as ${name}` : name} }`;
|
|
1524
1836
|
imports.push(`import ${what} from ${JSON.stringify(from)};`);
|
|
1525
1837
|
}
|
|
@@ -1552,8 +1864,8 @@ function pruneImports(source, candidates) {
|
|
|
1552
1864
|
}
|
|
1553
1865
|
|
|
1554
1866
|
// src/core/motion.ts
|
|
1555
|
-
import
|
|
1556
|
-
import
|
|
1867
|
+
import fs7 from "fs";
|
|
1868
|
+
import path7 from "path";
|
|
1557
1869
|
var REVEAL = `"use client";
|
|
1558
1870
|
|
|
1559
1871
|
import { Children, cloneElement, isValidElement, useEffect, useRef, useState, type CSSProperties, type ReactElement, type ReactNode } from "react";
|
|
@@ -1778,8 +2090,8 @@ export function TextAnimate({ effect = "words", stagger = 0.06, delay = 0, durat
|
|
|
1778
2090
|
function motionFiles(config) {
|
|
1779
2091
|
const glob = config.components[0] ?? "components/**/*.tsx";
|
|
1780
2092
|
const base = glob.split("/").filter((p) => !p.includes("*")).join("/") || "components";
|
|
1781
|
-
const dir =
|
|
1782
|
-
return { reveal:
|
|
2093
|
+
const dir = path7.join(base, "motion");
|
|
2094
|
+
return { reveal: path7.join(dir, "reveal.tsx"), text: path7.join(dir, "text-animate.tsx") };
|
|
1783
2095
|
}
|
|
1784
2096
|
function ensureMotionComponents(config) {
|
|
1785
2097
|
const files = motionFiles(config);
|
|
@@ -1788,18 +2100,18 @@ function ensureMotionComponents(config) {
|
|
|
1788
2100
|
[files.reveal, REVEAL],
|
|
1789
2101
|
[files.text, TEXT_ANIMATE]
|
|
1790
2102
|
]) {
|
|
1791
|
-
const abs =
|
|
1792
|
-
if (
|
|
1793
|
-
|
|
1794
|
-
|
|
2103
|
+
const abs = path7.join(config.root, rel);
|
|
2104
|
+
if (fs7.existsSync(abs)) continue;
|
|
2105
|
+
fs7.mkdirSync(path7.dirname(abs), { recursive: true });
|
|
2106
|
+
fs7.writeFileSync(abs, content);
|
|
1795
2107
|
created = true;
|
|
1796
2108
|
}
|
|
1797
2109
|
return { ...files, created };
|
|
1798
2110
|
}
|
|
1799
2111
|
|
|
1800
2112
|
// src/core/components.ts
|
|
1801
|
-
import
|
|
1802
|
-
import
|
|
2113
|
+
import fs8 from "fs";
|
|
2114
|
+
import path8 from "path";
|
|
1803
2115
|
function componentsDir(config) {
|
|
1804
2116
|
const glob = config.components[0] ?? "components/**/*.tsx";
|
|
1805
2117
|
return glob.split("/").filter((p) => !p.includes("*")).join("/") || "components";
|
|
@@ -1863,11 +2175,11 @@ function importsFor(config, jsx, source, doc, fromRel, toRel) {
|
|
|
1863
2175
|
(s) => s.type === "ImportDeclaration" && s.importKind !== "type" && s.specifiers.some((sp2) => sp2.local.name === name)
|
|
1864
2176
|
);
|
|
1865
2177
|
if (!decl) {
|
|
1866
|
-
if (doc.declared.includes(name)) throw new EditError(`These layers use ${name}, defined in ${
|
|
2178
|
+
if (doc.declared.includes(name)) throw new EditError(`These layers use ${name}, defined in ${path8.basename(fromRel)}. Move it to its own file first, or ask your agent to extract the component.`);
|
|
1867
2179
|
throw new EditError(`These layers use \`${name}\` from the surrounding code, so they can't become a component automatically. Ask your agent to extract it with props.`);
|
|
1868
2180
|
}
|
|
1869
2181
|
const sp = decl.specifiers.find((x) => x.local.name === name);
|
|
1870
|
-
const from = repoint(config, decl.source.value,
|
|
2182
|
+
const from = repoint(config, decl.source.value, path8.join(config.root, fromRel), path8.join(config.root, toRel));
|
|
1871
2183
|
const what = sp.type === "ImportDefaultSpecifier" ? name : sp.type === "ImportNamespaceSpecifier" ? `* as ${name}` : `{ ${sp.imported.type === "Identifier" && sp.imported.name !== name ? `${sp.imported.name} as ${name}` : name} }`;
|
|
1872
2184
|
out.push(`import ${what} from ${JSON.stringify(from)};`);
|
|
1873
2185
|
}
|
|
@@ -1891,193 +2203,11 @@ ${indentBlock(indentBlock(jsx.trim()))}
|
|
|
1891
2203
|
`;
|
|
1892
2204
|
}
|
|
1893
2205
|
function newComponentPath(config, name) {
|
|
1894
|
-
const rel =
|
|
1895
|
-
if (
|
|
2206
|
+
const rel = path8.join(componentsDir(config), `${kebab(name)}.tsx`);
|
|
2207
|
+
if (fs8.existsSync(path8.join(config.root, rel))) throw new EditError(`${rel} already exists. Pick another name.`);
|
|
1896
2208
|
return rel;
|
|
1897
2209
|
}
|
|
1898
2210
|
|
|
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
2211
|
// src/core/workspace.ts
|
|
2082
2212
|
var require2 = createRequire(import.meta.url);
|
|
2083
2213
|
var MARK = "__tcsel";
|
|
@@ -2094,13 +2224,14 @@ var Workspace = class {
|
|
|
2094
2224
|
this.catalog = new Catalog(config.root, config.components, config.libraries);
|
|
2095
2225
|
this.comments = new Comments(config);
|
|
2096
2226
|
this.git = new Git(config.root);
|
|
2097
|
-
for (const c of this.canvases()) this.sources.set(c,
|
|
2227
|
+
for (const c of this.canvases()) this.sources.set(c, fs9.readFileSync(this.file(c), "utf8"));
|
|
2098
2228
|
}
|
|
2099
2229
|
config;
|
|
2100
2230
|
catalog;
|
|
2101
2231
|
sources = /* @__PURE__ */ new Map();
|
|
2102
2232
|
undoStacks = /* @__PURE__ */ new Map();
|
|
2103
2233
|
redoStacks = /* @__PURE__ */ new Map();
|
|
2234
|
+
/** Activity across canvases: labels only, never file copies. */
|
|
2104
2235
|
feed = [];
|
|
2105
2236
|
seq = 0;
|
|
2106
2237
|
queue = Promise.resolve();
|
|
@@ -2128,25 +2259,48 @@ var Workspace = class {
|
|
|
2128
2259
|
}
|
|
2129
2260
|
read(canvas) {
|
|
2130
2261
|
const file = this.file(canvas);
|
|
2131
|
-
if (!
|
|
2132
|
-
const src =
|
|
2262
|
+
if (!fs9.existsSync(file)) throw new EditError(`Canvas "${canvas}" does not exist. Canvases: ${this.canvases().join(", ") || "none"}`);
|
|
2263
|
+
const src = fs9.readFileSync(file, "utf8");
|
|
2133
2264
|
if (!this.sources.has(canvas)) this.sources.set(canvas, src);
|
|
2134
2265
|
return src;
|
|
2135
2266
|
}
|
|
2136
2267
|
doc(canvas) {
|
|
2137
2268
|
return this.docFrom(canvas, this.read(canvas));
|
|
2138
2269
|
}
|
|
2139
|
-
/**
|
|
2270
|
+
/**
|
|
2271
|
+
* A canvas doc with linked frames resolved to their page's layers. Resolving
|
|
2272
|
+
* reads and parses every linked page and layout, so the result is reused
|
|
2273
|
+
* until the canvas source or one of those files changes (checked with stat).
|
|
2274
|
+
*/
|
|
2140
2275
|
docFrom(canvas, source) {
|
|
2141
|
-
|
|
2276
|
+
const hit = this.docCache.get(canvas);
|
|
2277
|
+
if (hit && hit.source === source && hit.stamps.every(([rel, stamp]) => this.stamp(rel) === stamp)) return { ...hit.doc, rev: ++this.revision };
|
|
2278
|
+
const doc = resolveLinks(this.config, parseCanvas(canvas, this.relFile(canvas), source));
|
|
2279
|
+
const files = new Set(doc.frames.flatMap((f) => [...f.link?.files ?? [], ...f.page ? [f.page] : []]));
|
|
2280
|
+
this.docCache.set(canvas, { source, doc, stamps: [...files].map((rel) => [rel, this.stamp(rel)]) });
|
|
2281
|
+
return { ...doc, rev: ++this.revision };
|
|
2282
|
+
}
|
|
2283
|
+
/** a doc computed later reflects the files at least as recently: its rev is higher */
|
|
2284
|
+
revision = 0;
|
|
2285
|
+
docCache = /* @__PURE__ */ new Map();
|
|
2286
|
+
stamp(rel) {
|
|
2287
|
+
try {
|
|
2288
|
+
const st = fs9.statSync(path9.join(this.config.root, rel));
|
|
2289
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
2290
|
+
} catch {
|
|
2291
|
+
return "missing";
|
|
2292
|
+
}
|
|
2142
2293
|
}
|
|
2143
2294
|
/** Where a node's code lives: the canvas, or a linked page/layout file. */
|
|
2144
2295
|
nodeSource(canvas, id) {
|
|
2145
|
-
const
|
|
2296
|
+
const doc = this.doc(canvas);
|
|
2297
|
+
const hit = findNode(doc, id);
|
|
2146
2298
|
if (!hit) return null;
|
|
2147
|
-
const
|
|
2299
|
+
const linked = fileOfId(id);
|
|
2300
|
+
if (linked) assertShown(doc, linked);
|
|
2301
|
+
const rel = linked ?? this.relFile(canvas);
|
|
2148
2302
|
const abs = path9.join(this.config.root, rel);
|
|
2149
|
-
return { node: hit.node, rel, abs, source:
|
|
2303
|
+
return { node: hit.node, rel, abs, source: fs9.readFileSync(abs, "utf8") };
|
|
2150
2304
|
}
|
|
2151
2305
|
/** Page and layout files shown by linked frames, across all canvases (the Git panel counts them as design files). */
|
|
2152
2306
|
linkedFiles() {
|
|
@@ -2165,7 +2319,7 @@ var Workspace = class {
|
|
|
2165
2319
|
*/
|
|
2166
2320
|
designFiles() {
|
|
2167
2321
|
const m = motionFiles(this.config);
|
|
2168
|
-
return [...this.linkedFiles(), ...[m.reveal, m.text].filter((f) =>
|
|
2322
|
+
return [...this.linkedFiles(), ...[m.reveal, m.text].filter((f) => fs9.existsSync(path9.join(this.config.root, f)))];
|
|
2169
2323
|
}
|
|
2170
2324
|
/** Canvases with a linked frame showing this page or layout file. */
|
|
2171
2325
|
canvasesLinking(rel) {
|
|
@@ -2181,20 +2335,21 @@ var Workspace = class {
|
|
|
2181
2335
|
linkedFileChanged(rel) {
|
|
2182
2336
|
let next;
|
|
2183
2337
|
try {
|
|
2184
|
-
next =
|
|
2338
|
+
next = fs9.readFileSync(path9.join(this.config.root, rel), "utf8");
|
|
2185
2339
|
} catch {
|
|
2186
2340
|
next = "";
|
|
2187
2341
|
}
|
|
2188
2342
|
const key = `file:${rel}`;
|
|
2189
2343
|
if (this.sources.get(key) === next) return;
|
|
2190
2344
|
this.sources.set(key, next);
|
|
2345
|
+
this.docCache.clear();
|
|
2191
2346
|
for (const c of this.canvasesLinking(rel)) this.emit({ type: "doc", canvas: c, doc: this.doc(c) });
|
|
2192
2347
|
}
|
|
2193
2348
|
createCanvas(name, actor, content = STARTER_CANVAS) {
|
|
2194
2349
|
const file = canvasPath(this.config, name);
|
|
2195
|
-
if (
|
|
2196
|
-
|
|
2197
|
-
|
|
2350
|
+
if (fs9.existsSync(file)) throw new EditError(`Canvas "${name}" already exists.`);
|
|
2351
|
+
fs9.mkdirSync(path9.dirname(file), { recursive: true });
|
|
2352
|
+
fs9.writeFileSync(file, content);
|
|
2198
2353
|
this.sources.set(name, content);
|
|
2199
2354
|
this.canvasesChanged();
|
|
2200
2355
|
this.record(name, `Created canvas ${name}`, actor, "", content, []);
|
|
@@ -2229,10 +2384,10 @@ var Workspace = class {
|
|
|
2229
2384
|
}
|
|
2230
2385
|
renameCanvas(from, to) {
|
|
2231
2386
|
const src = this.file(from);
|
|
2232
|
-
if (!
|
|
2387
|
+
if (!fs9.existsSync(src)) throw new EditError(`Canvas "${from}" does not exist.`);
|
|
2233
2388
|
const dest = canvasPath(this.config, to);
|
|
2234
|
-
if (
|
|
2235
|
-
|
|
2389
|
+
if (fs9.existsSync(dest)) throw new EditError(`A canvas named "${to}" already exists.`);
|
|
2390
|
+
fs9.renameSync(src, dest);
|
|
2236
2391
|
this.comments.renameCanvas(from, to);
|
|
2237
2392
|
for (const map of [this.sources, this.undoStacks, this.redoStacks]) {
|
|
2238
2393
|
if (map.has(from)) {
|
|
@@ -2249,7 +2404,7 @@ var Workspace = class {
|
|
|
2249
2404
|
duplicateCanvas(name) {
|
|
2250
2405
|
const source = this.read(name);
|
|
2251
2406
|
const copy = this.uniqueCanvasName(`${name}-copy`);
|
|
2252
|
-
|
|
2407
|
+
fs9.writeFileSync(canvasPath(this.config, copy), source);
|
|
2253
2408
|
this.sources.set(copy, source);
|
|
2254
2409
|
this.canvasesChanged();
|
|
2255
2410
|
return copy;
|
|
@@ -2258,17 +2413,19 @@ var Workspace = class {
|
|
|
2258
2413
|
trash = /* @__PURE__ */ new Map();
|
|
2259
2414
|
deleteCanvas(name) {
|
|
2260
2415
|
const file = this.file(name);
|
|
2261
|
-
if (!
|
|
2262
|
-
this.trash.set(name,
|
|
2263
|
-
|
|
2416
|
+
if (!fs9.existsSync(file)) throw new EditError(`Canvas "${name}" does not exist.`);
|
|
2417
|
+
this.trash.set(name, fs9.readFileSync(file, "utf8"));
|
|
2418
|
+
fs9.unlinkSync(file);
|
|
2264
2419
|
this.sources.delete(name);
|
|
2420
|
+
this.undoStacks.delete(name);
|
|
2421
|
+
this.redoStacks.delete(name);
|
|
2265
2422
|
this.canvasesChanged();
|
|
2266
2423
|
}
|
|
2267
2424
|
restoreCanvas(name) {
|
|
2268
2425
|
const source = this.trash.get(name);
|
|
2269
2426
|
if (source === void 0) throw new EditError(`Nothing to restore for "${name}".`);
|
|
2270
2427
|
const target = this.uniqueCanvasName(name);
|
|
2271
|
-
|
|
2428
|
+
fs9.writeFileSync(canvasPath(this.config, target), source);
|
|
2272
2429
|
this.sources.set(target, source);
|
|
2273
2430
|
this.trash.delete(name);
|
|
2274
2431
|
this.canvasesChanged();
|
|
@@ -2279,14 +2436,14 @@ var Workspace = class {
|
|
|
2279
2436
|
if (!fileName.endsWith(CANVAS_SUFFIX)) return;
|
|
2280
2437
|
const canvas = canvasNameOf(fileName);
|
|
2281
2438
|
const file = this.file(canvas);
|
|
2282
|
-
if (!
|
|
2439
|
+
if (!fs9.existsSync(file)) {
|
|
2283
2440
|
this.sources.delete(canvas);
|
|
2284
2441
|
syncRoute(this.config);
|
|
2285
2442
|
this.emit({ type: "canvases", canvases: this.canvases() });
|
|
2286
2443
|
return;
|
|
2287
2444
|
}
|
|
2288
2445
|
const prev = this.sources.get(canvas);
|
|
2289
|
-
const next =
|
|
2446
|
+
const next = fs9.readFileSync(file, "utf8");
|
|
2290
2447
|
if (prev === next) return;
|
|
2291
2448
|
this.sources.set(canvas, next);
|
|
2292
2449
|
if (prev === void 0) {
|
|
@@ -2306,6 +2463,7 @@ var Workspace = class {
|
|
|
2306
2463
|
if (!(err instanceof ConflictError)) throw err;
|
|
2307
2464
|
if (err.file) this.linkedFileChanged(err.file);
|
|
2308
2465
|
else this.externalChange(path9.basename(this.file(cmd.canvas)));
|
|
2466
|
+
if (nodeRefs(cmd).length || "frame" in cmd && cmd.frame) throw new EditError(`${path9.basename(err.file ?? this.relFile(cmd.canvas))} changed while editing. Re-read the canvas and try again.`);
|
|
2309
2467
|
return this.exec(cmd, actor);
|
|
2310
2468
|
}
|
|
2311
2469
|
});
|
|
@@ -2314,32 +2472,55 @@ var Workspace = class {
|
|
|
2314
2472
|
}
|
|
2315
2473
|
async exec(cmd, actor) {
|
|
2316
2474
|
cmd = { ...cmd };
|
|
2317
|
-
|
|
2475
|
+
let prepared = null;
|
|
2476
|
+
if (cmd.op === "add_component_frame" && !fs9.existsSync(this.file(cmd.canvas))) {
|
|
2477
|
+
await this.catalog.load();
|
|
2478
|
+
const spec = this.catalog.get(cmd.component);
|
|
2479
|
+
if (!spec) throw new EditError(`Unknown component "${cmd.component}". Use list_components.`);
|
|
2480
|
+
if (spec.library) throw new EditError(`${spec.name} comes from ${spec.library}: it can't be edited here.`);
|
|
2481
|
+
this.createCanvas(cmd.canvas, actor, COMPONENTS_CANVAS);
|
|
2482
|
+
}
|
|
2318
2483
|
const canvasSource = this.read(cmd.canvas);
|
|
2319
2484
|
const canvasDoc = parseCanvas(cmd.canvas, this.relFile(cmd.canvas), canvasSource);
|
|
2320
2485
|
if (canvasDoc.error) throw new EditError(`The canvas file does not parse: ${canvasDoc.error}`);
|
|
2321
2486
|
const view = resolveLinks(this.config, canvasDoc);
|
|
2322
2487
|
cmd = this.routeLinked(cmd, view);
|
|
2323
2488
|
let target = this.targetOf(cmd);
|
|
2489
|
+
if (target) assertShown(view, target);
|
|
2324
2490
|
if (cmd.op === "explore_copy") {
|
|
2325
2491
|
const frame = findFrame(canvasDoc, cmd.frame);
|
|
2326
2492
|
if (!frame) throw new EditError(`Frame "${cmd.frame}" not found.`);
|
|
2327
2493
|
const body = explorationBody(this.config, frame, this.relFile(cmd.canvas));
|
|
2328
|
-
|
|
2494
|
+
prepared = { kind: "explore", frame, imports: body.imports, jsx: await this.formatSnippet(body.jsx) };
|
|
2329
2495
|
}
|
|
2330
2496
|
if (cmd.op === "apply_to_page") {
|
|
2331
2497
|
const frame = findFrame(canvasDoc, cmd.frame);
|
|
2332
2498
|
if (!frame) throw new EditError(`Frame "${cmd.frame}" not found.`);
|
|
2333
2499
|
const plan = applyPlan(this.config, canvasSource, canvasDoc, frame, this.relFile(cmd.canvas));
|
|
2334
2500
|
target = plan.page;
|
|
2335
|
-
|
|
2501
|
+
prepared = { kind: "apply", frame, ...plan };
|
|
2336
2502
|
}
|
|
2337
2503
|
const rel = target ?? this.relFile(cmd.canvas);
|
|
2338
2504
|
const file = path9.join(this.config.root, rel);
|
|
2339
|
-
const
|
|
2505
|
+
const original = target ? fs9.readFileSync(file, "utf8") : canvasSource;
|
|
2506
|
+
const crlf = original.includes("\r\n");
|
|
2507
|
+
const before = crlf ? original.replace(/\r\n/g, "\n") : original;
|
|
2340
2508
|
const parse2 = (src) => target ? parseTargetDoc(target, src) : parseCanvas(cmd.canvas, rel, src);
|
|
2341
2509
|
const doc = parse2(before);
|
|
2342
2510
|
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.`);
|
|
2511
|
+
if (cmd.op === "insert_icon") {
|
|
2512
|
+
const icon = cmd;
|
|
2513
|
+
const lib = iconLibrary(icon.library);
|
|
2514
|
+
if (!lib) throw new EditError(`Unknown icon library "${icon.library}". Known: ${ICON_LIBRARIES.map((l) => l.id).join(", ")}.`);
|
|
2515
|
+
const icons = await loadIcons(this.config.root, lib.id);
|
|
2516
|
+
const wanted = icon.name.toLowerCase();
|
|
2517
|
+
const exact = icons.find((i) => i.name === icon.name) ?? icons.find((i) => i.name.toLowerCase() === wanted);
|
|
2518
|
+
if (!exact) {
|
|
2519
|
+
const near = searchIcons(icons, icon.name, 5).icons.map((i) => i.name);
|
|
2520
|
+
throw new EditError(`No icon "${icon.name}" in ${lib.label}.${near.length ? ` Did you mean ${near.join(", ")}?` : " Use search_icons."}`);
|
|
2521
|
+
}
|
|
2522
|
+
cmd = { ...icon, library: lib.id, name: exact.name };
|
|
2523
|
+
}
|
|
2343
2524
|
if ((cmd.op === "insert_jsx" || cmd.op === "replace" || cmd.op === "create_frame") && cmd.jsx) {
|
|
2344
2525
|
cmd = { ...cmd, jsx: await this.formatSnippet(cmd.jsx) };
|
|
2345
2526
|
}
|
|
@@ -2349,16 +2530,16 @@ var Workspace = class {
|
|
|
2349
2530
|
if (!r) throw new EditError(`No page "${cmd.route}". Pages: ${routes.map((x) => x.route).join(", ") || "none"}`);
|
|
2350
2531
|
if (cmd.copy) {
|
|
2351
2532
|
const built = routeFrame(this.config, r.file, this.relFile(cmd.canvas));
|
|
2352
|
-
|
|
2533
|
+
prepared = { kind: "route", route: r.route, page: null, imports: built.imports, mode: built.mode, jsx: await this.formatSnippet(built.jsx) };
|
|
2353
2534
|
} else {
|
|
2354
2535
|
const built = linkedFrameBody(this.config, canvasDoc, this.relFile(cmd.canvas), r.file);
|
|
2355
|
-
|
|
2536
|
+
prepared = { kind: "route", route: r.route, page: r.file, imports: built.imports, mode: "linked", jsx: built.jsx };
|
|
2356
2537
|
}
|
|
2357
2538
|
}
|
|
2358
2539
|
if (cmd.op === "add_animation") {
|
|
2359
2540
|
const files = ensureMotionComponents(this.config);
|
|
2360
2541
|
if (files.created) this.catalog.invalidate();
|
|
2361
|
-
|
|
2542
|
+
prepared = { kind: "motion", files };
|
|
2362
2543
|
}
|
|
2363
2544
|
let newComponent = null;
|
|
2364
2545
|
let movedNames = /* @__PURE__ */ new Set();
|
|
@@ -2381,13 +2562,13 @@ var Workspace = class {
|
|
|
2381
2562
|
}
|
|
2382
2563
|
const abs = path9.join(this.config.root, fileRel);
|
|
2383
2564
|
newComponent = { rel: fileRel, content: await this.format(componentFile(name, jsx, imports), abs).catch(() => componentFile(name, jsx, imports)) };
|
|
2384
|
-
|
|
2565
|
+
prepared = { kind: "component", name, file: fileRel };
|
|
2385
2566
|
}
|
|
2386
2567
|
await this.catalog.load().catch(() => []);
|
|
2387
2568
|
const ed = new CanvasEditor(before, doc);
|
|
2388
2569
|
const renamedFrame = cmd.op === "update_frame" && cmd.name !== void 0 ? findFrame(doc, cmd.frame) : null;
|
|
2389
2570
|
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);
|
|
2571
|
+
const label = this.apply(ed, cmd, rel, prepared);
|
|
2391
2572
|
const pretty = await this.isPrettierClean(before, file);
|
|
2392
2573
|
const finish = async (code) => pretty ? this.format(code, file) : validate(code);
|
|
2393
2574
|
let result = ed.result();
|
|
@@ -2421,19 +2602,25 @@ var Workspace = class {
|
|
|
2421
2602
|
ids = paths.map((p) => pathId(clean, p)).filter(Boolean);
|
|
2422
2603
|
}
|
|
2423
2604
|
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" });
|
|
2605
|
+
const writeNewComponent = () => {
|
|
2606
|
+
if (!newComponent) return;
|
|
2607
|
+
fs9.mkdirSync(path9.dirname(path9.join(this.config.root, newComponent.rel)), { recursive: true });
|
|
2608
|
+
fs9.writeFileSync(path9.join(this.config.root, newComponent.rel), newComponent.content, { flag: "wx" });
|
|
2429
2609
|
this.catalog.invalidate();
|
|
2430
2610
|
this.emit({ type: "catalog" });
|
|
2611
|
+
};
|
|
2612
|
+
if (crlf) after = after.replace(/\n/g, "\r\n");
|
|
2613
|
+
if (after === original) {
|
|
2614
|
+
writeNewComponent();
|
|
2615
|
+
return { doc: target ? this.doc(cmd.canvas) : view, ids, label };
|
|
2431
2616
|
}
|
|
2617
|
+
if (fs9.readFileSync(file, "utf8") !== original) throw new ConflictError(`${path9.basename(rel)} changed during the edit.`, target ?? void 0);
|
|
2618
|
+
writeNewComponent();
|
|
2432
2619
|
if (target) this.sources.set(`file:${target}`, after);
|
|
2433
2620
|
else this.sources.set(cmd.canvas, after);
|
|
2434
|
-
|
|
2621
|
+
fs9.writeFileSync(file, after);
|
|
2435
2622
|
const newDoc = this.doc(cmd.canvas);
|
|
2436
|
-
this.record(cmd.canvas, label, actor,
|
|
2623
|
+
this.record(cmd.canvas, label, actor, original, after, ids, target ?? void 0, touchedFrames(cmd, ids, view, newDoc));
|
|
2437
2624
|
if (renamedFrame) {
|
|
2438
2625
|
const now = newDoc.frames[renamedFrame.path[0]];
|
|
2439
2626
|
if (now && now.frameName !== renamedFrame.frameName) {
|
|
@@ -2457,7 +2644,7 @@ var Workspace = class {
|
|
|
2457
2644
|
const f = findFrame(view, cmd.parent);
|
|
2458
2645
|
if (f?.link) {
|
|
2459
2646
|
if (!f.link.editable) throw viewOnly(f);
|
|
2460
|
-
const src =
|
|
2647
|
+
const src = fs9.readFileSync(path9.join(this.config.root, f.link.page), "utf8");
|
|
2461
2648
|
if (f.link.kind === "component") {
|
|
2462
2649
|
const own = parseModuleDoc(f.link.page, src)?.frames.find((x) => x.frameName === f.link.route);
|
|
2463
2650
|
if (!own) throw viewOnly(f);
|
|
@@ -2487,7 +2674,46 @@ var Workspace = class {
|
|
|
2487
2674
|
}
|
|
2488
2675
|
return [...files][0] ?? null;
|
|
2489
2676
|
}
|
|
2490
|
-
|
|
2677
|
+
/** Applies a command to the file being edited and returns its history label. */
|
|
2678
|
+
apply(ed, cmd, rel, prepared) {
|
|
2679
|
+
switch (cmd.op) {
|
|
2680
|
+
case "set_props":
|
|
2681
|
+
case "set_text":
|
|
2682
|
+
case "set_class":
|
|
2683
|
+
case "insert_jsx":
|
|
2684
|
+
case "insert_component":
|
|
2685
|
+
case "insert_icon":
|
|
2686
|
+
case "replace":
|
|
2687
|
+
case "duplicate":
|
|
2688
|
+
case "delete":
|
|
2689
|
+
case "move":
|
|
2690
|
+
case "reorder":
|
|
2691
|
+
case "wrap":
|
|
2692
|
+
return this.applyLayerEdit(ed, cmd, rel, prepared);
|
|
2693
|
+
case "create_frame":
|
|
2694
|
+
case "update_frame":
|
|
2695
|
+
case "add_background":
|
|
2696
|
+
case "apply_preset":
|
|
2697
|
+
case "create_variants":
|
|
2698
|
+
case "add_component_frame":
|
|
2699
|
+
return this.applyFrameEdit(ed, cmd, rel, prepared);
|
|
2700
|
+
case "import_route":
|
|
2701
|
+
case "explore_copy":
|
|
2702
|
+
case "apply_to_page":
|
|
2703
|
+
return this.applyPageEdit(ed, cmd, rel, prepared);
|
|
2704
|
+
case "add_animation":
|
|
2705
|
+
case "remove_animation":
|
|
2706
|
+
return this.applyMotionEdit(ed, cmd, rel, prepared);
|
|
2707
|
+
case "create_component":
|
|
2708
|
+
return this.applyComponentEdit(ed, cmd, rel, prepared);
|
|
2709
|
+
default: {
|
|
2710
|
+
const unknown = cmd;
|
|
2711
|
+
throw new EditError(`Unknown command ${unknown.op}.`);
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
/** Edits to layers: props, text, classes, inserting, moving, wrapping. */
|
|
2716
|
+
applyLayerEdit(ed, cmd, rel, prepared) {
|
|
2491
2717
|
const doc = ed.doc;
|
|
2492
2718
|
const guard = (node, verb) => {
|
|
2493
2719
|
if (doc.canvasNode) return;
|
|
@@ -2545,6 +2771,14 @@ var Workspace = class {
|
|
|
2545
2771
|
ed.insertChild(parent, cmd.index, jsx);
|
|
2546
2772
|
return `Inserted ${spec.name}`;
|
|
2547
2773
|
}
|
|
2774
|
+
case "insert_icon": {
|
|
2775
|
+
const parent = ed.container(cmd.parent);
|
|
2776
|
+
const lib = iconLibrary(cmd.library);
|
|
2777
|
+
const local = ed.importAs(cmd.name, lib.from, `${cmd.name.replace(/Icon$/, "")}Icon`);
|
|
2778
|
+
const className = cmd.className?.trim() ?? "size-4";
|
|
2779
|
+
ed.insertChild(parent, cmd.index, `<${local}${className ? ` className=${JSON.stringify(className)}` : ""} />`);
|
|
2780
|
+
return `Inserted ${cmd.name} icon`;
|
|
2781
|
+
}
|
|
2548
2782
|
case "replace": {
|
|
2549
2783
|
const { node } = ed.node(cmd.id);
|
|
2550
2784
|
parseJsx(cmd.jsx);
|
|
@@ -2553,7 +2787,8 @@ var Workspace = class {
|
|
|
2553
2787
|
return `Replaced ${node.name}`;
|
|
2554
2788
|
}
|
|
2555
2789
|
case "duplicate": {
|
|
2556
|
-
const
|
|
2790
|
+
const all = cmd.ids.map((id) => ed.node(id));
|
|
2791
|
+
const nodes = all.filter(({ node: n }) => !all.some(({ node: o }) => o !== n && n.start >= o.start && n.end <= o.end));
|
|
2557
2792
|
for (const { node } of nodes) guard(node, "duplicate");
|
|
2558
2793
|
for (const { node } of nodes) ed.duplicate(node);
|
|
2559
2794
|
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 +2825,19 @@ var Workspace = class {
|
|
|
2590
2825
|
ed.wrap(nodes, `<div className="${cls}">`, "</div>");
|
|
2591
2826
|
return nodes.length === 1 ? `Wrapped ${nodes[0].name} in auto layout` : `Wrapped ${nodes.length} layers in auto layout`;
|
|
2592
2827
|
}
|
|
2828
|
+
default:
|
|
2829
|
+
throw new Error(`Internal: ${cmd.op} isn't handled by applyLayerEdit.`);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
/** Frames: creating, updating, backgrounds, presets, variant grids, main component frames. */
|
|
2833
|
+
applyFrameEdit(ed, cmd, rel, prepared) {
|
|
2834
|
+
const doc = ed.doc;
|
|
2835
|
+
switch (cmd.op) {
|
|
2593
2836
|
case "create_frame": {
|
|
2594
2837
|
const device = findDevice(cmd.device);
|
|
2595
2838
|
if (cmd.device && !device) throw new EditError(`Unknown device "${cmd.device}". Devices: ${DEVICES.map((d) => d.id).join(", ")}`);
|
|
2596
2839
|
const name = uniqueFrameName(doc, cmd.name ?? device?.name ?? "Frame");
|
|
2597
|
-
const right = doc
|
|
2840
|
+
const right = rightEdge(doc);
|
|
2598
2841
|
const top = doc.frames.length ? Math.min(...doc.frames.map((f) => f.y)) : 0;
|
|
2599
2842
|
if (cmd.jsx) {
|
|
2600
2843
|
parseJsx(cmd.jsx);
|
|
@@ -2684,19 +2927,68 @@ var Workspace = class {
|
|
|
2684
2927
|
ed.selectPaths.push(node.path);
|
|
2685
2928
|
return `Applied ${preset.name} preset to ${spec.name}`;
|
|
2686
2929
|
}
|
|
2930
|
+
case "create_variants": {
|
|
2931
|
+
const spec = this.catalog.get(cmd.component);
|
|
2932
|
+
if (!spec) throw new EditError(`Unknown component "${cmd.component}".`);
|
|
2933
|
+
const prop = spec.props.find((p) => p.name === cmd.prop);
|
|
2934
|
+
if (!prop) throw new EditError(`${spec.name} has no prop "${cmd.prop}".`);
|
|
2935
|
+
const values = prop.type === "enum" ? prop.options : prop.type === "boolean" ? [false, true] : [];
|
|
2936
|
+
if (!values.length) throw new EditError(`Prop "${cmd.prop}" is ${prop.typeText}; variants need a union of string literals or a boolean.`);
|
|
2937
|
+
ed.ensureImport(spec.name, importSpecifier(this.config.root, rel, spec.file));
|
|
2938
|
+
const label = (v) => typeof v === "string" ? v[0].toUpperCase() + v.slice(1) : `${prop.name}: ${v}`;
|
|
2939
|
+
const rows = indentBlock(values.map((v) => componentJsx(spec, { ...cmd.props, [prop.name]: v }, spec.acceptsChildren ? label(v) : void 0)).join("\n"));
|
|
2940
|
+
const right = rightEdge(doc);
|
|
2941
|
+
const name = uniqueFrameName(doc, `${spec.name} \xB7 ${prop.name}`);
|
|
2942
|
+
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">
|
|
2943
|
+
${rows}
|
|
2944
|
+
</div>` }));
|
|
2945
|
+
return `Created ${spec.name} ${prop.name} variants`;
|
|
2946
|
+
}
|
|
2947
|
+
case "add_component_frame": {
|
|
2948
|
+
const spec = this.catalog.get(cmd.component);
|
|
2949
|
+
if (!spec) throw new EditError(`Unknown component "${cmd.component}". Use list_components.`);
|
|
2950
|
+
if (spec.library) throw new EditError(`${spec.name} comes from ${spec.library}: it can't be edited here.`);
|
|
2951
|
+
const existing = doc.frames.find((f) => f.component === `${spec.file}#${spec.name}`);
|
|
2952
|
+
if (existing) {
|
|
2953
|
+
ed.selectPaths.push(existing.path);
|
|
2954
|
+
return `Opened ${spec.name}`;
|
|
2955
|
+
}
|
|
2956
|
+
ed.ensureImport(spec.name, importSpecifier(this.config.root, rel, spec.file));
|
|
2957
|
+
const right = rightEdge(doc);
|
|
2958
|
+
const top = doc.frames.length ? Math.min(...doc.frames.map((f) => f.y)) : 0;
|
|
2959
|
+
ed.insertFrame(
|
|
2960
|
+
frameCode({
|
|
2961
|
+
name: uniqueFrameName(doc, spec.name),
|
|
2962
|
+
x: cmd.x ?? right + 80,
|
|
2963
|
+
y: cmd.y ?? top,
|
|
2964
|
+
width: cmd.width ?? 640,
|
|
2965
|
+
component: `${spec.file}#${spec.name}`,
|
|
2966
|
+
body: `<div className="p-10">
|
|
2967
|
+
${componentJsx(spec)}
|
|
2968
|
+
</div>`
|
|
2969
|
+
})
|
|
2970
|
+
);
|
|
2971
|
+
return `Opened main component ${spec.name}`;
|
|
2972
|
+
}
|
|
2973
|
+
default:
|
|
2974
|
+
throw new Error(`Internal: ${cmd.op} isn't handled by applyFrameEdit.`);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
/** Linked pages: adding a page, exploring a copy, applying it back. */
|
|
2978
|
+
applyPageEdit(ed, cmd, rel, prepared) {
|
|
2979
|
+
const doc = ed.doc;
|
|
2980
|
+
switch (cmd.op) {
|
|
2687
2981
|
case "import_route": {
|
|
2688
|
-
const built =
|
|
2689
|
-
this.pending = null;
|
|
2982
|
+
const built = take(prepared, "route");
|
|
2690
2983
|
for (const stmt of built.imports) ed.addImportStatement(stmt);
|
|
2691
|
-
const right = doc
|
|
2984
|
+
const right = rightEdge(doc);
|
|
2692
2985
|
const top = doc.frames.length ? Math.min(...doc.frames.map((f) => f.y)) : 0;
|
|
2693
2986
|
const name = uniqueFrameName(doc, cmd.name ?? (built.route === "/" ? "Home" : built.route.split("/").filter(Boolean).map((p) => p.replace(/^./, (c) => c.toUpperCase())).join(" / ")));
|
|
2694
2987
|
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
2988
|
return built.mode === "linked" ? `Linked ${built.route}` : built.mode === "inline" ? `Copied ${built.route} as layers` : `Copied ${built.route}`;
|
|
2696
2989
|
}
|
|
2697
2990
|
case "explore_copy": {
|
|
2698
|
-
const built =
|
|
2699
|
-
this.pending = null;
|
|
2991
|
+
const built = take(prepared, "explore");
|
|
2700
2992
|
const f = built.frame;
|
|
2701
2993
|
for (const stmt of built.imports) ed.addImportStatement(stmt);
|
|
2702
2994
|
const h = (fr) => fr.height ?? 2e3;
|
|
@@ -2707,17 +2999,23 @@ var Workspace = class {
|
|
|
2707
2999
|
return `Explored a copy of ${f.frameName}`;
|
|
2708
3000
|
}
|
|
2709
3001
|
case "apply_to_page": {
|
|
2710
|
-
const plan =
|
|
2711
|
-
this.pending = null;
|
|
3002
|
+
const plan = take(prepared, "apply");
|
|
2712
3003
|
for (const stmt of plan.imports) ed.addImportStatement(stmt);
|
|
2713
3004
|
ed.replace(doc.frames[0].children[0], plan.jsx);
|
|
2714
3005
|
return `Applied ${plan.frame.frameName} to ${plan.page}`;
|
|
2715
3006
|
}
|
|
3007
|
+
default:
|
|
3008
|
+
throw new Error(`Internal: ${cmd.op} isn't handled by applyPageEdit.`);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
/** Scroll reveals and text animations. */
|
|
3012
|
+
applyMotionEdit(ed, cmd, rel, prepared) {
|
|
3013
|
+
switch (cmd.op) {
|
|
2716
3014
|
case "add_animation": {
|
|
2717
3015
|
const { node, parent } = ed.node(cmd.id);
|
|
2718
3016
|
if ("frameName" in node) throw new EditError("Animate a layer inside the frame, not the frame itself.");
|
|
2719
3017
|
if (node.kind === "expression") throw new EditError("Expressions can't be animated; pick the element around it.");
|
|
2720
|
-
const files =
|
|
3018
|
+
const files = prepared?.kind === "motion" ? prepared.files : motionFiles(this.config);
|
|
2721
3019
|
const props = {};
|
|
2722
3020
|
if (cmd.effect !== void 0) props.effect = cmd.effect;
|
|
2723
3021
|
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 +3063,16 @@ var Workspace = class {
|
|
|
2765
3063
|
ed.selectPaths.push(wrapper === kids[0] ? node.path : wrapper.path);
|
|
2766
3064
|
return cmd.kind === "reveal" ? "Removed the reveal" : "Removed the text animation";
|
|
2767
3065
|
}
|
|
3066
|
+
default:
|
|
3067
|
+
throw new Error(`Internal: ${cmd.op} isn't handled by applyMotionEdit.`);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
/** New components, from scratch or from selected layers. */
|
|
3071
|
+
applyComponentEdit(ed, cmd, rel, prepared) {
|
|
3072
|
+
switch (cmd.op) {
|
|
2768
3073
|
case "create_component": {
|
|
2769
|
-
const made =
|
|
2770
|
-
this.
|
|
2771
|
-
ed.ensureImport(made.name, importSpecifier(this.config.root, rel, made.file));
|
|
3074
|
+
const made = take(prepared, "component");
|
|
3075
|
+
if (cmd.from || cmd.parent) ed.ensureImport(made.name, importSpecifier(this.config.root, rel, made.file));
|
|
2772
3076
|
if (cmd.from) {
|
|
2773
3077
|
const { node } = ed.node(cmd.from);
|
|
2774
3078
|
ed.replace(node, `<${made.name} />`);
|
|
@@ -2778,49 +3082,8 @@ var Workspace = class {
|
|
|
2778
3082
|
ed.insertChild(ed.container(cmd.parent), cmd.index, `<${made.name} />`);
|
|
2779
3083
|
return `Created component ${made.name}`;
|
|
2780
3084
|
}
|
|
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
|
-
}
|
|
3085
|
+
default:
|
|
3086
|
+
throw new Error(`Internal: ${cmd.op} isn't handled by applyComponentEdit.`);
|
|
2824
3087
|
}
|
|
2825
3088
|
}
|
|
2826
3089
|
autoImport(ed, rel, jsx) {
|
|
@@ -2833,9 +3096,6 @@ ${rows}
|
|
|
2833
3096
|
ed.ensureImport(name, importSpecifier(this.config.root, rel, spec.file));
|
|
2834
3097
|
}
|
|
2835
3098
|
}
|
|
2836
|
-
motion = null;
|
|
2837
|
-
/** work resolved (async) in exec before the synchronous apply */
|
|
2838
|
-
pending = null;
|
|
2839
3099
|
cleanCache = /* @__PURE__ */ new Map();
|
|
2840
3100
|
async isPrettierClean(source, file) {
|
|
2841
3101
|
const hit = this.cleanCache.get(source);
|
|
@@ -2880,11 +3140,29 @@ ${rows}
|
|
|
2880
3140
|
if (stack.length > 200) stack.shift();
|
|
2881
3141
|
this.undoStacks.set(canvas, stack);
|
|
2882
3142
|
this.redoStacks.set(canvas, []);
|
|
2883
|
-
this.feed.push(entry);
|
|
3143
|
+
this.feed.push(publicEntry(entry));
|
|
2884
3144
|
if (this.feed.length > 300) this.feed.shift();
|
|
3145
|
+
this.trimHistory();
|
|
2885
3146
|
this.emit({ type: "change", entry: publicEntry(entry), ids });
|
|
2886
3147
|
this.emitHistory(canvas);
|
|
2887
3148
|
}
|
|
3149
|
+
/**
|
|
3150
|
+
* Undo entries hold whole files (before and after). Keep their total under a
|
|
3151
|
+
* budget by dropping the oldest entries across canvases, so long sessions on
|
|
3152
|
+
* big pages don't grow without limit.
|
|
3153
|
+
*/
|
|
3154
|
+
trimHistory(budget = 48 * 1024 * 1024) {
|
|
3155
|
+
const all = [...this.undoStacks.values(), ...this.redoStacks.values()];
|
|
3156
|
+
let total = 0;
|
|
3157
|
+
for (const stack of all) for (const e of stack) total += e.before.length + e.after.length;
|
|
3158
|
+
while (total > budget) {
|
|
3159
|
+
let oldest = null;
|
|
3160
|
+
for (const stack of all) if (stack.length && (!oldest || stack[0].at < oldest[0].at)) oldest = stack;
|
|
3161
|
+
if (!oldest) break;
|
|
3162
|
+
const e = oldest.shift();
|
|
3163
|
+
total -= e.before.length + e.after.length;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
2888
3166
|
/** Changes still in effect (not undone) on a canvas since a time, oldest first. */
|
|
2889
3167
|
changesSince(canvas, since) {
|
|
2890
3168
|
return (this.undoStacks.get(canvas) ?? []).filter((e) => e.at > since).map(publicEntry);
|
|
@@ -2909,17 +3187,20 @@ ${rows}
|
|
|
2909
3187
|
const entry = stack[stack.length - 1];
|
|
2910
3188
|
if (!entry) throw new EditError(`Nothing to ${dir}.`);
|
|
2911
3189
|
if (dir === "undo" && entry.before === "") throw new EditError("Cannot undo creating the canvas; delete the file instead.");
|
|
2912
|
-
stack.pop();
|
|
2913
3190
|
const file = entry.file ? path9.join(this.config.root, entry.file) : this.file(canvas);
|
|
2914
|
-
const
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
3191
|
+
const name = entry.file ? path9.basename(entry.file) : "the file";
|
|
3192
|
+
let current;
|
|
3193
|
+
try {
|
|
3194
|
+
current = entry.file ? fs9.readFileSync(file, "utf8") : this.read(canvas);
|
|
3195
|
+
} catch {
|
|
3196
|
+
throw new EditError(`Cannot ${dir} "${entry.label}": ${name} can't be read right now.`);
|
|
2919
3197
|
}
|
|
3198
|
+
const expected = dir === "undo" ? entry.after : entry.before;
|
|
3199
|
+
if (current !== expected) throw new EditError(`Cannot ${dir} "${entry.label}": ${name} changed since.`);
|
|
3200
|
+
stack.pop();
|
|
2920
3201
|
const next = dir === "undo" ? entry.before : entry.after;
|
|
2921
3202
|
this.sources.set(entry.file ? `file:${entry.file}` : canvas, next);
|
|
2922
|
-
|
|
3203
|
+
fs9.writeFileSync(file, next);
|
|
2923
3204
|
const list = to.get(canvas) ?? [];
|
|
2924
3205
|
list.push(entry);
|
|
2925
3206
|
to.set(canvas, list);
|
|
@@ -2929,9 +3210,10 @@ ${rows}
|
|
|
2929
3210
|
for (const c of this.canvasesLinking(entry.file)) if (c !== canvas) this.emit({ type: "doc", canvas: c, doc: this.doc(c) });
|
|
2930
3211
|
}
|
|
2931
3212
|
const label = `${dir === "undo" ? "Undid" : "Redid"} \u201C${entry.label}\u201D`;
|
|
2932
|
-
const marker = { ...entry, id: ++this.seq, label, actor, at: Date.now() };
|
|
3213
|
+
const marker = publicEntry({ ...entry, id: ++this.seq, label, actor, at: Date.now() });
|
|
2933
3214
|
this.feed.push(marker);
|
|
2934
|
-
this.
|
|
3215
|
+
if (this.feed.length > 300) this.feed.shift();
|
|
3216
|
+
this.emit({ type: "change", entry: marker, ids: [] });
|
|
2935
3217
|
this.emitHistory(canvas);
|
|
2936
3218
|
return { doc, label };
|
|
2937
3219
|
});
|
|
@@ -2939,7 +3221,7 @@ ${rows}
|
|
|
2939
3221
|
return job;
|
|
2940
3222
|
}
|
|
2941
3223
|
publicFeed() {
|
|
2942
|
-
return this.feed
|
|
3224
|
+
return this.feed;
|
|
2943
3225
|
}
|
|
2944
3226
|
// ---------- agents ----------
|
|
2945
3227
|
touchAgent(session, name) {
|
|
@@ -2959,17 +3241,17 @@ ${rows}
|
|
|
2959
3241
|
if (this.agents.delete(session)) this.emit({ type: "agents", agents: [...this.agents.values()] });
|
|
2960
3242
|
}
|
|
2961
3243
|
ensureRoute() {
|
|
2962
|
-
if (!
|
|
2963
|
-
|
|
3244
|
+
if (!fs9.existsSync(path9.join(this.config.root, this.config.canvasDir))) {
|
|
3245
|
+
fs9.mkdirSync(path9.join(this.config.root, this.config.canvasDir), { recursive: true });
|
|
2964
3246
|
}
|
|
2965
3247
|
if (!this.canvases().length) {
|
|
2966
3248
|
const file = canvasPath(this.config, "home");
|
|
2967
3249
|
const content = this.homeCanvas();
|
|
2968
|
-
|
|
3250
|
+
fs9.writeFileSync(file, content);
|
|
2969
3251
|
void this.format(content, file).then((pretty) => {
|
|
2970
|
-
if (
|
|
3252
|
+
if (fs9.readFileSync(file, "utf8") !== content) return;
|
|
2971
3253
|
this.sources.set("home", pretty);
|
|
2972
|
-
|
|
3254
|
+
fs9.writeFileSync(file, pretty);
|
|
2973
3255
|
}).catch(() => {
|
|
2974
3256
|
});
|
|
2975
3257
|
}
|
|
@@ -3020,6 +3302,10 @@ export default function ComponentsCanvas() {
|
|
|
3020
3302
|
return <Canvas></Canvas>;
|
|
3021
3303
|
}
|
|
3022
3304
|
`;
|
|
3305
|
+
function take(prepared, kind) {
|
|
3306
|
+
if (prepared?.kind !== kind) throw new Error(`Internal: the ${kind} step wasn't prepared.`);
|
|
3307
|
+
return prepared;
|
|
3308
|
+
}
|
|
3023
3309
|
function validate(code) {
|
|
3024
3310
|
try {
|
|
3025
3311
|
parseModule(code);
|
|
@@ -3032,6 +3318,13 @@ function publicEntry(e) {
|
|
|
3032
3318
|
const { before: _b, after: _a, ...rest } = e;
|
|
3033
3319
|
return rest;
|
|
3034
3320
|
}
|
|
3321
|
+
function assertShown(view, rel) {
|
|
3322
|
+
const shown = view.frames.some((f) => f.link?.files.includes(rel));
|
|
3323
|
+
if (!shown || path9.isAbsolute(rel) || rel.split(/[\\/]/).includes("..")) throw new EditError(`${rel} isn't shown on this canvas. Re-read the canvas for current ids.`);
|
|
3324
|
+
}
|
|
3325
|
+
function rightEdge(doc) {
|
|
3326
|
+
return doc.frames.reduce((m, f) => Math.max(m, f.x + f.width), -80);
|
|
3327
|
+
}
|
|
3035
3328
|
function nodeRefs(cmd) {
|
|
3036
3329
|
const refs = [];
|
|
3037
3330
|
if ("id" in cmd && cmd.id) refs.push(cmd.id);
|
|
@@ -3097,6 +3390,124 @@ function componentJsx(spec, props = {}, text) {
|
|
|
3097
3390
|
return childText !== void 0 ? `${open}>${textCode(childText)}</${spec.name}>` : `${open} />`;
|
|
3098
3391
|
}
|
|
3099
3392
|
|
|
3393
|
+
// src/core/pm.ts
|
|
3394
|
+
import { spawn } from "child_process";
|
|
3395
|
+
import fs10 from "fs";
|
|
3396
|
+
import path10 from "path";
|
|
3397
|
+
function detectPm(dir) {
|
|
3398
|
+
for (let d = dir; ; d = path10.dirname(d)) {
|
|
3399
|
+
if (fs10.existsSync(path10.join(d, "pnpm-lock.yaml")) || fs10.existsSync(path10.join(d, "pnpm-workspace.yaml"))) return "pnpm";
|
|
3400
|
+
if (fs10.existsSync(path10.join(d, "yarn.lock"))) return "yarn";
|
|
3401
|
+
if (fs10.existsSync(path10.join(d, "bun.lock")) || fs10.existsSync(path10.join(d, "bun.lockb"))) return "bun";
|
|
3402
|
+
if (fs10.existsSync(path10.join(d, "package-lock.json"))) return "npm";
|
|
3403
|
+
if (path10.dirname(d) === d || fs10.existsSync(path10.join(d, ".git"))) break;
|
|
3404
|
+
}
|
|
3405
|
+
const agent = process.env.npm_config_user_agent ?? "";
|
|
3406
|
+
if (agent.startsWith("pnpm")) return "pnpm";
|
|
3407
|
+
if (agent.startsWith("yarn")) return "yarn";
|
|
3408
|
+
if (agent.startsWith("bun")) return "bun";
|
|
3409
|
+
return "npm";
|
|
3410
|
+
}
|
|
3411
|
+
function addArgs(pm, specs, dev = false) {
|
|
3412
|
+
if (pm === "npm") return ["install", ...dev ? ["-D"] : [], ...specs];
|
|
3413
|
+
if (pm === "bun") return ["add", ...dev ? ["-d"] : [], ...specs];
|
|
3414
|
+
return ["add", ...dev ? ["-D"] : [], ...specs];
|
|
3415
|
+
}
|
|
3416
|
+
function dlx(pm, pkg, args) {
|
|
3417
|
+
if (pm === "pnpm") return ["pnpm", ["dlx", pkg, ...args]];
|
|
3418
|
+
if (pm === "yarn") return ["yarn", ["dlx", pkg, ...args]];
|
|
3419
|
+
if (pm === "bun") return ["bunx", [pkg, ...args]];
|
|
3420
|
+
return ["npx", ["--yes", pkg, ...args]];
|
|
3421
|
+
}
|
|
3422
|
+
function toolEnv(extra = {}) {
|
|
3423
|
+
return { ...process.env, PATH: `${path10.dirname(process.execPath)}${path10.delimiter}${process.env.PATH ?? ""}`, ...extra };
|
|
3424
|
+
}
|
|
3425
|
+
function run2(cmd, args, cwd, timeoutMs = 3e5) {
|
|
3426
|
+
return new Promise((resolve) => {
|
|
3427
|
+
let out = "";
|
|
3428
|
+
let done = false;
|
|
3429
|
+
const finish = (ok, extra = "") => {
|
|
3430
|
+
if (done) return;
|
|
3431
|
+
done = true;
|
|
3432
|
+
clearTimeout(timer);
|
|
3433
|
+
resolve({ ok, out: (out + extra).slice(-2e4) });
|
|
3434
|
+
};
|
|
3435
|
+
const p = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: toolEnv({ CI: "1", FORCE_COLOR: "0", NO_COLOR: "1" }), shell: process.platform === "win32" });
|
|
3436
|
+
const timer = setTimeout(() => {
|
|
3437
|
+
p.kill();
|
|
3438
|
+
finish(false, `
|
|
3439
|
+
${cmd} timed out after ${Math.round(timeoutMs / 1e3)}s`);
|
|
3440
|
+
}, timeoutMs);
|
|
3441
|
+
p.stdout.on("data", (d) => out += d);
|
|
3442
|
+
p.stderr.on("data", (d) => out += d);
|
|
3443
|
+
p.on("error", (err) => finish(false, `
|
|
3444
|
+
${err.message}`));
|
|
3445
|
+
p.on("close", (code) => finish(code === 0));
|
|
3446
|
+
});
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
// src/core/shadcn.ts
|
|
3450
|
+
import fs11 from "fs";
|
|
3451
|
+
import path11 from "path";
|
|
3452
|
+
function shadcnStatus(root) {
|
|
3453
|
+
const file = path11.join(root, "components.json");
|
|
3454
|
+
if (!fs11.existsSync(file)) return { initialized: false, uiDir: null, installed: [] };
|
|
3455
|
+
let ui = "@/components/ui";
|
|
3456
|
+
try {
|
|
3457
|
+
ui = JSON.parse(fs11.readFileSync(file, "utf8"))?.aliases?.ui ?? ui;
|
|
3458
|
+
} catch {
|
|
3459
|
+
}
|
|
3460
|
+
const dir = resolveAliasPath(root, ui);
|
|
3461
|
+
let installed = [];
|
|
3462
|
+
try {
|
|
3463
|
+
installed = fs11.readdirSync(dir).filter((f) => /\.(tsx|jsx)$/.test(f)).map((f) => f.replace(/\.(tsx|jsx)$/, "")).sort();
|
|
3464
|
+
} catch {
|
|
3465
|
+
installed = [];
|
|
3466
|
+
}
|
|
3467
|
+
return { initialized: true, uiDir: path11.relative(root, dir), installed };
|
|
3468
|
+
}
|
|
3469
|
+
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(" ");
|
|
3470
|
+
var registryCache = null;
|
|
3471
|
+
async function shadcnRegistry() {
|
|
3472
|
+
if (registryCache && Date.now() - registryCache.at < 36e5) return { names: registryCache.names, offline: false };
|
|
3473
|
+
try {
|
|
3474
|
+
const res = await fetch("https://ui.shadcn.com/r/index.json", { signal: AbortSignal.timeout(8e3) });
|
|
3475
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
3476
|
+
const items = await res.json();
|
|
3477
|
+
const names = items.filter((i) => i.type === "registry:ui" && VALID.test(i.name)).map((i) => i.name).sort();
|
|
3478
|
+
if (!names.length) throw new Error("empty registry");
|
|
3479
|
+
registryCache = { at: Date.now(), names };
|
|
3480
|
+
return { names, offline: false };
|
|
3481
|
+
} catch {
|
|
3482
|
+
return { names: FALLBACK, offline: true };
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
var VALID = /^[a-z0-9][a-z0-9-]*$/;
|
|
3486
|
+
async function shadcnInit(root) {
|
|
3487
|
+
const [cmd, args] = dlx(detectPm(root), "shadcn@latest", ["init", "--defaults", "--yes", "--no-monorepo"]);
|
|
3488
|
+
return run2(cmd, args, root);
|
|
3489
|
+
}
|
|
3490
|
+
async function shadcnAdd(root, names) {
|
|
3491
|
+
const bad = names.filter((n) => !VALID.test(n));
|
|
3492
|
+
if (bad.length) return { ok: false, out: `Not a component name: ${bad.join(", ")}`, added: [] };
|
|
3493
|
+
const before = new Set(shadcnStatus(root).installed);
|
|
3494
|
+
let log = "";
|
|
3495
|
+
if (!shadcnStatus(root).initialized) {
|
|
3496
|
+
const init = await shadcnInit(root);
|
|
3497
|
+
log += init.out;
|
|
3498
|
+
if (!init.ok) return { ok: false, out: log, added: [] };
|
|
3499
|
+
}
|
|
3500
|
+
const wanted = names.filter((n) => !before.has(n));
|
|
3501
|
+
if (wanted.length) {
|
|
3502
|
+
const [cmd, args] = dlx(detectPm(root), "shadcn@latest", ["add", ...wanted, "--yes"]);
|
|
3503
|
+
const res = await run2(cmd, args, root);
|
|
3504
|
+
log += res.out;
|
|
3505
|
+
if (!res.ok) return { ok: false, out: log, added: [] };
|
|
3506
|
+
}
|
|
3507
|
+
const added = shadcnStatus(root).installed.filter((n) => !before.has(n));
|
|
3508
|
+
return { ok: true, out: log, added };
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3100
3511
|
// src/core/outline.ts
|
|
3101
3512
|
function propText(name, v) {
|
|
3102
3513
|
if (v.kind === "boolean") return v.value ? name : `${name}={false}`;
|
|
@@ -3185,8 +3596,8 @@ function describeComponent(c, verbose = false) {
|
|
|
3185
3596
|
}
|
|
3186
3597
|
|
|
3187
3598
|
// src/core/tokens.ts
|
|
3188
|
-
import
|
|
3189
|
-
import
|
|
3599
|
+
import fs12 from "fs";
|
|
3600
|
+
import path12 from "path";
|
|
3190
3601
|
import { globSync } from "tinyglobby";
|
|
3191
3602
|
var GROUPS = [
|
|
3192
3603
|
["--color-", "Colors", "bg-{name} text-{name} border-{name}"],
|
|
@@ -3203,9 +3614,9 @@ function readDesignTokens(root) {
|
|
|
3203
3614
|
const sources = [];
|
|
3204
3615
|
let darkVariant = null;
|
|
3205
3616
|
for (const file of files) {
|
|
3206
|
-
const css =
|
|
3617
|
+
const css = fs12.readFileSync(file, "utf8");
|
|
3207
3618
|
if (!css.includes("@theme") && !css.includes("@custom-variant")) continue;
|
|
3208
|
-
sources.push(
|
|
3619
|
+
sources.push(path12.relative(root, file));
|
|
3209
3620
|
const dv = /@custom-variant\s+dark\s*\(([^;]+)\);/.exec(css);
|
|
3210
3621
|
if (dv) darkVariant = dv[1].trim();
|
|
3211
3622
|
for (const block of css.matchAll(/@theme[^{]*\{([\s\S]*?)\n\}/g)) {
|
|
@@ -3244,7 +3655,7 @@ function readTokenData(root) {
|
|
|
3244
3655
|
for (const m of body.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)) into.set(m[1], m[2].trim());
|
|
3245
3656
|
};
|
|
3246
3657
|
for (const file of files) {
|
|
3247
|
-
const css =
|
|
3658
|
+
const css = fs12.readFileSync(file, "utf8");
|
|
3248
3659
|
for (const block of css.matchAll(/@theme[^{]*\{([\s\S]*?)\n\}/g)) decls(block[1], theme);
|
|
3249
3660
|
for (const block of css.matchAll(/(?:^|\n)\s*:root\s*\{([\s\S]*?)\n\}/g)) decls(block[1], lightVars);
|
|
3250
3661
|
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 +3687,30 @@ function readTokenData(root) {
|
|
|
3276
3687
|
}
|
|
3277
3688
|
|
|
3278
3689
|
// src/core/compare.ts
|
|
3279
|
-
import
|
|
3280
|
-
import
|
|
3690
|
+
import fs13 from "fs";
|
|
3691
|
+
import path13 from "path";
|
|
3281
3692
|
var COMPARE_PREFIX = "__compare__";
|
|
3282
3693
|
function writeCompare(config, canvas, source, files = {}, as = canvas) {
|
|
3283
|
-
const routeDir =
|
|
3284
|
-
const dir =
|
|
3285
|
-
|
|
3286
|
-
const canvasDir =
|
|
3287
|
-
const canvasRel =
|
|
3694
|
+
const routeDir = path13.join(config.root, config.appDir, "truecanvas");
|
|
3695
|
+
const dir = path13.join(routeDir, "compare");
|
|
3696
|
+
fs13.mkdirSync(dir, { recursive: true });
|
|
3697
|
+
const canvasDir = path13.join(config.root, config.canvasDir);
|
|
3698
|
+
const canvasRel = path13.join(config.canvasDir, `${canvas}.canvas.tsx`);
|
|
3288
3699
|
const swapped = /* @__PURE__ */ new Map();
|
|
3289
3700
|
for (const [rel, content] of Object.entries(files)) {
|
|
3290
|
-
const ext =
|
|
3701
|
+
const ext = path13.extname(rel);
|
|
3291
3702
|
const name = `${as}--${rel.slice(0, -ext.length).replace(/[^\w.-]+/g, "_")}${ext}`;
|
|
3292
|
-
const original =
|
|
3293
|
-
|
|
3703
|
+
const original = path13.join(config.root, rel);
|
|
3704
|
+
fs13.writeFileSync(path13.join(dir, name), rewriteImports(content, (spec) => spec.startsWith(".") ? relativeSpec(dir, path13.resolve(path13.dirname(original), spec)) : spec));
|
|
3294
3705
|
swapped.set(importSpecifier(config.root, canvasRel, rel), `./${name.slice(0, -ext.length)}`);
|
|
3295
3706
|
}
|
|
3296
|
-
const out = rewriteImports(source, (spec) => swapped.get(spec) ?? (spec.startsWith(".") ? relativeSpec(dir,
|
|
3297
|
-
|
|
3707
|
+
const out = rewriteImports(source, (spec) => swapped.get(spec) ?? (spec.startsWith(".") ? relativeSpec(dir, path13.resolve(canvasDir, spec)) : spec));
|
|
3708
|
+
fs13.writeFileSync(path13.join(dir, `${as}.canvas.tsx`), out);
|
|
3298
3709
|
syncRoute(config);
|
|
3299
3710
|
return `${COMPARE_PREFIX}${as}`;
|
|
3300
3711
|
}
|
|
3301
3712
|
function relativeSpec(fromDir, target) {
|
|
3302
|
-
const next =
|
|
3713
|
+
const next = path13.relative(fromDir, target);
|
|
3303
3714
|
return next.startsWith(".") ? next : `./${next}`;
|
|
3304
3715
|
}
|
|
3305
3716
|
function rewriteImports(source, map) {
|
|
@@ -3308,15 +3719,16 @@ function rewriteImports(source, map) {
|
|
|
3308
3719
|
for (const imp of [...doc.imports].sort((a, b) => b.start - a.start)) {
|
|
3309
3720
|
const next = map(imp.source);
|
|
3310
3721
|
if (next === imp.source) continue;
|
|
3311
|
-
const stmt = out.slice(imp.start, imp.end)
|
|
3722
|
+
const stmt = rewriteImportSource(out.slice(imp.start, imp.end), imp.source, next);
|
|
3312
3723
|
out = out.slice(0, imp.start) + stmt + out.slice(imp.end);
|
|
3313
3724
|
}
|
|
3314
3725
|
return out;
|
|
3315
3726
|
}
|
|
3316
3727
|
function clearCompare(config, canvas) {
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3728
|
+
if (canvas) assertCanvasName(canvas);
|
|
3729
|
+
const dir = path13.join(config.root, config.appDir, "truecanvas", "compare");
|
|
3730
|
+
if (!fs13.existsSync(dir)) return;
|
|
3731
|
+
for (const f of fs13.readdirSync(dir)) if (!canvas || f === `${canvas}.canvas.tsx` || f.startsWith(`${canvas}--`)) fs13.rmSync(path13.join(dir, f));
|
|
3320
3732
|
syncRoute(config);
|
|
3321
3733
|
}
|
|
3322
3734
|
function frameChanges(before, after) {
|
|
@@ -3332,6 +3744,137 @@ function frameChanges(before, after) {
|
|
|
3332
3744
|
return out;
|
|
3333
3745
|
}
|
|
3334
3746
|
|
|
3747
|
+
// src/core/init.ts
|
|
3748
|
+
import fs14 from "fs";
|
|
3749
|
+
import MagicString2 from "magic-string";
|
|
3750
|
+
import path14 from "path";
|
|
3751
|
+
var CONFIG_FILES = ["next.config.ts", "next.config.mjs", "next.config.js", "next.config.mts"];
|
|
3752
|
+
function nextConfigStatus(root) {
|
|
3753
|
+
for (const name of CONFIG_FILES) {
|
|
3754
|
+
const file = path14.join(root, name);
|
|
3755
|
+
if (fs14.existsSync(file)) return { file, wrapped: fs14.readFileSync(file, "utf8").includes("withTruecanvas") };
|
|
3756
|
+
}
|
|
3757
|
+
return { file: null, wrapped: false };
|
|
3758
|
+
}
|
|
3759
|
+
function initProject(config) {
|
|
3760
|
+
const { root } = config;
|
|
3761
|
+
const report = { done: [], todo: [] };
|
|
3762
|
+
const status = nextConfigStatus(root);
|
|
3763
|
+
if (!status.file) {
|
|
3764
|
+
const file = path14.join(root, "next.config.ts");
|
|
3765
|
+
fs14.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(`${path14.basename(status.file)} already uses withTruecanvas()`);
|
|
3775
|
+
} else {
|
|
3776
|
+
const src = fs14.readFileSync(status.file, "utf8");
|
|
3777
|
+
const next = wrapConfigExport(src);
|
|
3778
|
+
if (next && next !== src && next.includes("withTruecanvas(")) {
|
|
3779
|
+
fs14.writeFileSync(status.file, next);
|
|
3780
|
+
report.done.push(`Wrapped ${path14.basename(status.file)} with withTruecanvas()`);
|
|
3781
|
+
} else {
|
|
3782
|
+
report.todo.push(`Wrap your ${path14.basename(status.file)} export with withTruecanvas() from "truecanvas/next"`);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
const pkgFile = path14.join(root, "package.json");
|
|
3786
|
+
try {
|
|
3787
|
+
const raw = fs14.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
|
+
fs14.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)) report.todo.push("Next.js isn't in package.json: Truecanvas currently needs a Next.js (App Router) app");
|
|
3800
|
+
} catch {
|
|
3801
|
+
report.todo.push("Couldn't read package.json");
|
|
3802
|
+
}
|
|
3803
|
+
const ignoreFile = path14.join(root, ".gitignore");
|
|
3804
|
+
const entry = `/${config.appDir}/truecanvas/`;
|
|
3805
|
+
const ignore = fs14.existsSync(ignoreFile) ? fs14.readFileSync(ignoreFile, "utf8") : "";
|
|
3806
|
+
if (!ignore.split("\n").some((l) => l.trim() === entry || l.trim() === entry.slice(1))) {
|
|
3807
|
+
fs14.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 = path14.join(root, name);
|
|
3815
|
+
if (!fs14.existsSync(file)) continue;
|
|
3816
|
+
const src = fs14.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
|
+
fs14.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 (!fs14.existsSync(path14.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
|
+
|
|
3335
3878
|
export {
|
|
3336
3879
|
loadConfig,
|
|
3337
3880
|
parseModule,
|
|
@@ -3351,16 +3894,28 @@ export {
|
|
|
3351
3894
|
componentsDir,
|
|
3352
3895
|
DEVICES,
|
|
3353
3896
|
findDevice,
|
|
3897
|
+
assertCanvasName,
|
|
3898
|
+
syncComponentRegistry,
|
|
3354
3899
|
Comments,
|
|
3355
3900
|
Git,
|
|
3356
|
-
syncComponentRegistry,
|
|
3357
3901
|
Workspace,
|
|
3358
3902
|
componentJsx,
|
|
3903
|
+
detectPm,
|
|
3904
|
+
addArgs,
|
|
3905
|
+
toolEnv,
|
|
3906
|
+
run2 as run,
|
|
3907
|
+
shadcnStatus,
|
|
3908
|
+
shadcnRegistry,
|
|
3909
|
+
shadcnInit,
|
|
3910
|
+
shadcnAdd,
|
|
3359
3911
|
outlineDoc,
|
|
3360
3912
|
describeComponent,
|
|
3361
3913
|
readDesignTokens,
|
|
3362
3914
|
readTokenData,
|
|
3363
3915
|
writeCompare,
|
|
3364
3916
|
clearCompare,
|
|
3365
|
-
frameChanges
|
|
3917
|
+
frameChanges,
|
|
3918
|
+
nextConfigStatus,
|
|
3919
|
+
initProject,
|
|
3920
|
+
wrapConfigExport
|
|
3366
3921
|
};
|