truecanvas 0.5.0 → 0.6.1
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 +6 -0
- package/dist/chunk-D565QDII.js +814 -0
- package/dist/chunk-LKFOLM3L.js +54 -0
- package/dist/chunk-NAK4DQWO.js +1005 -0
- package/dist/{chunk-DV76O5SZ.js → chunk-OPKROZI2.js} +261 -4
- package/dist/chunk-OZOV4Y6C.js +45 -0
- package/dist/{chunk-A4QIAVTG.js → chunk-QJ5M3G4E.js} +88 -852
- package/dist/{chunk-TSHD2S36.js → chunk-VWKLYIY6.js} +1 -1
- package/dist/cli.js +739 -938
- package/dist/core/index.js +14 -11
- package/dist/editor/assets/Menu-BGfgJ59U.css +1 -0
- package/dist/editor/assets/hub-DOR9_rf9.js +2 -0
- package/dist/editor/assets/main-vJb_YsG2.js +27 -0
- package/dist/editor/hub.html +3 -3
- package/dist/editor/index.html +3 -3
- package/dist/hub.d.ts +88 -0
- package/dist/hub.js +15 -0
- package/dist/share/index.d.ts +73 -1
- package/dist/share/index.js +13 -1
- package/dist/vite/index.js +3 -2
- package/package.json +5 -1
- package/dist/chunk-PBKX3SBC.js +0 -97
- package/dist/editor/assets/Menu-BJ3OmqzH.css +0 -1
- package/dist/editor/assets/hub-B5hqZCM2.js +0 -2
- package/dist/editor/assets/main-woTyWmya.js +0 -26
- /package/dist/editor/assets/{Menu-C0Ti-6T2.js → Menu-BbEG2pRj.js} +0 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
import {
|
|
2
|
+
idOf,
|
|
3
|
+
isComponentName,
|
|
4
|
+
jsxName,
|
|
5
|
+
parseModule,
|
|
6
|
+
walk
|
|
7
|
+
} from "./chunk-LKFOLM3L.js";
|
|
8
|
+
|
|
9
|
+
// src/core/parse.ts
|
|
10
|
+
function parseCanvas(name, file, source) {
|
|
11
|
+
let ast;
|
|
12
|
+
try {
|
|
13
|
+
ast = parseModule(source);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return { name, file, frames: [], imports: [], canvasNode: null, prologueEnd: 0, declared: [], error: err.message };
|
|
16
|
+
}
|
|
17
|
+
const imports = readImports(ast);
|
|
18
|
+
const directives = ast.program.directives;
|
|
19
|
+
const prologueEnd = directives.length ? directives[directives.length - 1].end : 0;
|
|
20
|
+
const declared = topLevelNames(ast);
|
|
21
|
+
let canvasEl = null;
|
|
22
|
+
walk(ast, (node) => {
|
|
23
|
+
if (canvasEl) return false;
|
|
24
|
+
if (node.type === "JSXElement" && jsxName(node.openingElement.name) === "Canvas") {
|
|
25
|
+
canvasEl = node;
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
if (!canvasEl) {
|
|
30
|
+
return { name, file, frames: [], imports, canvasNode: null, prologueEnd, declared, error: "No <Canvas> element found in the default export." };
|
|
31
|
+
}
|
|
32
|
+
const canvasNode = buildNode(canvasEl, source, []);
|
|
33
|
+
const frames = [];
|
|
34
|
+
let frameIndex = 0;
|
|
35
|
+
for (const child of canvasNode.children) {
|
|
36
|
+
if (child.kind !== "component" || child.name !== "Frame") continue;
|
|
37
|
+
const path2 = [frameIndex++];
|
|
38
|
+
reindex(child, path2);
|
|
39
|
+
frames.push({
|
|
40
|
+
...child,
|
|
41
|
+
frameName: str(child.props.name) ?? `Frame ${frameIndex}`,
|
|
42
|
+
x: num(child.props.x) ?? 0,
|
|
43
|
+
y: num(child.props.y) ?? 0,
|
|
44
|
+
width: num(child.props.width) ?? 1024,
|
|
45
|
+
height: num(child.props.height) ?? null,
|
|
46
|
+
theme: str(child.props.theme) ?? null,
|
|
47
|
+
device: str(child.props.device) ?? null,
|
|
48
|
+
page: str(child.props.page) ?? null,
|
|
49
|
+
from: str(child.props.from) ?? null,
|
|
50
|
+
component: str(child.props.component) ?? null,
|
|
51
|
+
link: null
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return { name, file, frames, imports, canvasNode, prologueEnd, declared };
|
|
55
|
+
}
|
|
56
|
+
function readImports(ast) {
|
|
57
|
+
const imports = [];
|
|
58
|
+
for (const stmt of ast.program.body) {
|
|
59
|
+
if (stmt.type !== "ImportDeclaration") continue;
|
|
60
|
+
const typeOnly = stmt.importKind === "type";
|
|
61
|
+
const named = stmt.specifiers.filter((s) => s.type === "ImportSpecifier");
|
|
62
|
+
const def = stmt.specifiers.find((s) => s.type === "ImportDefaultSpecifier");
|
|
63
|
+
const ns = stmt.specifiers.find((s) => s.type === "ImportNamespaceSpecifier");
|
|
64
|
+
imports.push({
|
|
65
|
+
source: stmt.source.value,
|
|
66
|
+
typeOnly,
|
|
67
|
+
// value bindings only: `import type` and `{ type X }` don't exist at runtime
|
|
68
|
+
names: typeOnly ? [] : [...named.filter((s) => s.importKind !== "type").map((s) => s.local.name), ...ns ? [ns.local.name] : []],
|
|
69
|
+
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 })),
|
|
70
|
+
defaultName: def ? def.local.name : null,
|
|
71
|
+
start: stmt.start,
|
|
72
|
+
end: stmt.end,
|
|
73
|
+
lastSpecifierEnd: named.length && !typeOnly ? named[named.length - 1].end : null
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return imports;
|
|
77
|
+
}
|
|
78
|
+
function topLevelNames(ast) {
|
|
79
|
+
const out = [];
|
|
80
|
+
for (let stmt of ast.program.body) {
|
|
81
|
+
if ((stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportDefaultDeclaration") && stmt.declaration) stmt = stmt.declaration;
|
|
82
|
+
if ((stmt.type === "FunctionDeclaration" || stmt.type === "ClassDeclaration") && stmt.id) out.push(stmt.id.name);
|
|
83
|
+
if (stmt.type === "VariableDeclaration") {
|
|
84
|
+
for (const d of stmt.declarations) if (d.id.type === "Identifier") out.push(d.id.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
function reindex(node, path2) {
|
|
90
|
+
node.path = path2;
|
|
91
|
+
node.children.forEach((c, i) => reindex(c, [...path2, i]));
|
|
92
|
+
}
|
|
93
|
+
function str(v) {
|
|
94
|
+
return v?.kind === "string" ? v.value : void 0;
|
|
95
|
+
}
|
|
96
|
+
function num(v) {
|
|
97
|
+
return v?.kind === "number" ? v.value : void 0;
|
|
98
|
+
}
|
|
99
|
+
function buildNode(el, source, path2) {
|
|
100
|
+
const children = [];
|
|
101
|
+
for (const child of el.children) {
|
|
102
|
+
const built = buildChild(child, source, [...path2, children.length]);
|
|
103
|
+
if (built) children.push(built);
|
|
104
|
+
}
|
|
105
|
+
if (el.type === "JSXFragment") {
|
|
106
|
+
return {
|
|
107
|
+
id: idOf(el),
|
|
108
|
+
kind: "fragment",
|
|
109
|
+
name: "Fragment",
|
|
110
|
+
props: {},
|
|
111
|
+
spreads: 0,
|
|
112
|
+
children,
|
|
113
|
+
start: el.start,
|
|
114
|
+
end: el.end,
|
|
115
|
+
line: el.loc.start.line,
|
|
116
|
+
col: el.loc.start.column,
|
|
117
|
+
path: path2,
|
|
118
|
+
attrs: [],
|
|
119
|
+
nameEnd: el.openingFragment.end - 1,
|
|
120
|
+
openEnd: el.openingFragment.end,
|
|
121
|
+
selfClosing: false,
|
|
122
|
+
closeStart: el.closingFragment.start
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const open = el.openingElement;
|
|
126
|
+
const name = jsxName(open.name);
|
|
127
|
+
const props = {};
|
|
128
|
+
const attrs = [];
|
|
129
|
+
let spreads = 0;
|
|
130
|
+
for (const attr of open.attributes) {
|
|
131
|
+
if (attr.type === "JSXSpreadAttribute") {
|
|
132
|
+
spreads++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const attrName = attr.name.type === "JSXIdentifier" ? attr.name.name : `${attr.name.namespace.name}:${attr.name.name.name}`;
|
|
136
|
+
if (attrName === "data-tc") continue;
|
|
137
|
+
const literal = classLiteral(attr.value);
|
|
138
|
+
attrs.push({ name: attrName, start: attr.start, end: attr.end, ...literal ? { literal: { start: literal.start, end: literal.end } } : {} });
|
|
139
|
+
props[attrName] = literal ? { kind: "string", value: literal.type === "StringLiteral" ? literal.value : literal.quasis.map((q) => q.value.cooked ?? "").join("") } : readValue(attr.value, source);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
id: idOf(open),
|
|
143
|
+
kind: isComponentName(name) ? "component" : "element",
|
|
144
|
+
name,
|
|
145
|
+
props,
|
|
146
|
+
spreads,
|
|
147
|
+
children,
|
|
148
|
+
start: el.start,
|
|
149
|
+
end: el.end,
|
|
150
|
+
line: open.loc.start.line,
|
|
151
|
+
col: open.loc.start.column,
|
|
152
|
+
path: path2,
|
|
153
|
+
attrs,
|
|
154
|
+
// after type arguments: `<Select<Opt> …>`
|
|
155
|
+
nameEnd: (open.typeParameters ?? open.name).end,
|
|
156
|
+
openEnd: open.end,
|
|
157
|
+
selfClosing: open.selfClosing,
|
|
158
|
+
closeStart: el.closingElement ? el.closingElement.start : open.end
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function buildChild(child, source, path2) {
|
|
162
|
+
if (child.type === "JSXElement" || child.type === "JSXFragment") return buildNode(child, source, path2);
|
|
163
|
+
if (child.type === "JSXText") {
|
|
164
|
+
const text = child.value.replace(/\s+/g, " ").trim();
|
|
165
|
+
if (!text) return null;
|
|
166
|
+
return leaf("text", text, child, path2, { text });
|
|
167
|
+
}
|
|
168
|
+
if (child.type === "JSXExpressionContainer") {
|
|
169
|
+
const expr = child.expression;
|
|
170
|
+
if (expr.type === "JSXEmptyExpression") return null;
|
|
171
|
+
if (expr.type === "StringLiteral") return leaf("text", expr.value, child, path2, { text: expr.value });
|
|
172
|
+
if (expr.type === "TemplateLiteral" && expr.expressions.length === 0) {
|
|
173
|
+
const text = expr.quasis.map((q) => q.value.cooked ?? "").join("");
|
|
174
|
+
return leaf("text", text, child, path2, { text });
|
|
175
|
+
}
|
|
176
|
+
return leaf("expression", source.slice(expr.start, expr.end), child, path2, {});
|
|
177
|
+
}
|
|
178
|
+
if (child.type === "JSXSpreadChild") return leaf("expression", source.slice(child.start, child.end), child, path2, {});
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function leaf(kind, name, node, path2, extra) {
|
|
182
|
+
return {
|
|
183
|
+
id: `${kind[0]}${idOf(node)}`,
|
|
184
|
+
kind,
|
|
185
|
+
name,
|
|
186
|
+
props: {},
|
|
187
|
+
spreads: 0,
|
|
188
|
+
children: [],
|
|
189
|
+
...extra,
|
|
190
|
+
start: node.start,
|
|
191
|
+
end: node.end,
|
|
192
|
+
line: node.loc.start.line,
|
|
193
|
+
col: node.loc.start.column,
|
|
194
|
+
path: path2,
|
|
195
|
+
attrs: [],
|
|
196
|
+
nameEnd: node.end,
|
|
197
|
+
openEnd: node.end,
|
|
198
|
+
selfClosing: true,
|
|
199
|
+
closeStart: node.end
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
var CLASS_HELPERS = /* @__PURE__ */ new Set(["cn", "clsx", "cx", "twMerge", "classNames", "classnames"]);
|
|
203
|
+
function classLiteral(value) {
|
|
204
|
+
if (value?.type !== "JSXExpressionContainer") return null;
|
|
205
|
+
const e = value.expression;
|
|
206
|
+
if (e.type !== "CallExpression" || e.callee.type !== "Identifier" || !CLASS_HELPERS.has(e.callee.name)) return null;
|
|
207
|
+
const first = e.arguments[0];
|
|
208
|
+
if (first?.type === "StringLiteral") return first;
|
|
209
|
+
if (first?.type === "TemplateLiteral" && first.expressions.length === 0) return first;
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
function readValue(value, source) {
|
|
213
|
+
if (value == null) return { kind: "boolean", value: true };
|
|
214
|
+
if (value.type === "StringLiteral") return { kind: "string", value: value.value };
|
|
215
|
+
if (value.type === "JSXExpressionContainer") {
|
|
216
|
+
const e = value.expression;
|
|
217
|
+
if (e.type === "StringLiteral") return { kind: "string", value: e.value };
|
|
218
|
+
if (e.type === "NumericLiteral") return { kind: "number", value: e.value };
|
|
219
|
+
if (e.type === "BooleanLiteral") return { kind: "boolean", value: e.value };
|
|
220
|
+
if (e.type === "UnaryExpression" && e.operator === "-" && e.argument.type === "NumericLiteral") {
|
|
221
|
+
return { kind: "number", value: -e.argument.value };
|
|
222
|
+
}
|
|
223
|
+
if (e.type === "TemplateLiteral" && e.expressions.length === 0) {
|
|
224
|
+
return { kind: "string", value: e.quasis.map((q) => q.value.cooked ?? "").join("") };
|
|
225
|
+
}
|
|
226
|
+
if (e.type === "ArrayExpression") {
|
|
227
|
+
const items = [];
|
|
228
|
+
for (const el of e.elements) {
|
|
229
|
+
if (el?.type === "StringLiteral" || el?.type === "NumericLiteral" || el?.type === "BooleanLiteral") items.push(el.value);
|
|
230
|
+
else if (el?.type === "UnaryExpression" && el.operator === "-" && el.argument.type === "NumericLiteral") items.push(-el.argument.value);
|
|
231
|
+
else return { kind: "expression", code: source.slice(e.start, e.end) };
|
|
232
|
+
}
|
|
233
|
+
return { kind: "array", value: items };
|
|
234
|
+
}
|
|
235
|
+
if (e.type === "JSXEmptyExpression") return { kind: "expression", code: "" };
|
|
236
|
+
return { kind: "expression", code: source.slice(e.start, e.end) };
|
|
237
|
+
}
|
|
238
|
+
return { kind: "expression", code: source.slice(value.start, value.end) };
|
|
239
|
+
}
|
|
240
|
+
function* allNodes(doc) {
|
|
241
|
+
for (const frame of doc.frames) {
|
|
242
|
+
yield { node: frame, parent: null, frame };
|
|
243
|
+
const stack = frame.children.map((c) => ({ node: c, parent: frame })).reverse();
|
|
244
|
+
while (stack.length) {
|
|
245
|
+
const item = stack.pop();
|
|
246
|
+
yield { ...item, frame };
|
|
247
|
+
for (let i = item.node.children.length - 1; i >= 0; i--) stack.push({ node: item.node.children[i], parent: item.node });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function findNode(doc, id) {
|
|
252
|
+
for (const entry of allNodes(doc)) if (entry.node.id === id) return entry;
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
function findFrame(doc, ref) {
|
|
256
|
+
return doc.frames.find((f) => f.id === ref || f.frameName === ref) ?? null;
|
|
257
|
+
}
|
|
258
|
+
function findByPath(doc, path2) {
|
|
259
|
+
if (!path2.length) return null;
|
|
260
|
+
let node = doc.frames[path2[0]];
|
|
261
|
+
for (const i of path2.slice(1)) node = node?.children[i];
|
|
262
|
+
return node ?? null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/core/edit.ts
|
|
266
|
+
import MagicString from "magic-string";
|
|
267
|
+
var EditError = class extends Error {
|
|
268
|
+
};
|
|
269
|
+
function attrCode(name, value) {
|
|
270
|
+
if (value === true) return name;
|
|
271
|
+
if (typeof value === "string") {
|
|
272
|
+
return /["\n\\{}&]/.test(value) ? `${name}={${JSON.stringify(value)}}` : `${name}="${value}"`;
|
|
273
|
+
}
|
|
274
|
+
if (typeof value === "number" && !Number.isFinite(value)) throw new EditError(`Invalid number for ${name}.`);
|
|
275
|
+
if (Array.isArray(value)) return `${name}={[${value.map((v) => JSON.stringify(v)).join(", ")}]}`;
|
|
276
|
+
return `${name}={${JSON.stringify(value)}}`;
|
|
277
|
+
}
|
|
278
|
+
var PROP_NAME = /^[A-Za-z_$][\w$-]*(:[\w$-]+)?$/;
|
|
279
|
+
function textCode(text) {
|
|
280
|
+
if (text === "") return "";
|
|
281
|
+
if (/[{}<>&\n]/.test(text) || /^\s|\s$/.test(text)) return `{${JSON.stringify(text)}}`;
|
|
282
|
+
return text;
|
|
283
|
+
}
|
|
284
|
+
var CanvasEditor = class {
|
|
285
|
+
constructor(source, doc) {
|
|
286
|
+
this.source = source;
|
|
287
|
+
this.doc = doc;
|
|
288
|
+
this.s = new MagicString(source);
|
|
289
|
+
}
|
|
290
|
+
source;
|
|
291
|
+
doc;
|
|
292
|
+
s;
|
|
293
|
+
/** Path of the node that should be selected after the edit (best effort, before formatting). */
|
|
294
|
+
selectPaths = [];
|
|
295
|
+
usedFrameNames = null;
|
|
296
|
+
placed = null;
|
|
297
|
+
node(id) {
|
|
298
|
+
const found = findNode(this.doc, id);
|
|
299
|
+
if (!found) throw new EditError(`Node ${id} not found. Re-read the canvas, ids change after every edit.`);
|
|
300
|
+
return found;
|
|
301
|
+
}
|
|
302
|
+
frame(ref) {
|
|
303
|
+
const f = findFrame(this.doc, ref);
|
|
304
|
+
if (!f) throw new EditError(`Frame "${ref}" not found. Available: ${this.doc.frames.map((x) => `"${x.frameName}"`).join(", ")}`);
|
|
305
|
+
return f;
|
|
306
|
+
}
|
|
307
|
+
/** A frame id/name or a node id: whatever an agent passes as "parent". */
|
|
308
|
+
container(ref) {
|
|
309
|
+
const f = findFrame(this.doc, ref);
|
|
310
|
+
if (f) return f;
|
|
311
|
+
const n = this.node(ref).node;
|
|
312
|
+
if (n.kind === "text" || n.kind === "expression") throw new EditError(`${ref} is a ${n.kind} node and cannot have children.`);
|
|
313
|
+
return n;
|
|
314
|
+
}
|
|
315
|
+
setProp(node, name, value, opts = {}) {
|
|
316
|
+
if (node.kind !== "component" && node.kind !== "element") throw new EditError(`Cannot set props on a ${node.kind} node.`);
|
|
317
|
+
if (!PROP_NAME.test(name) || name === "data-tc") throw new EditError(`Invalid prop name "${name}".`);
|
|
318
|
+
const existing = node.attrs.find((a) => a.name === name);
|
|
319
|
+
if (existing?.literal && opts.raw === void 0 && (typeof value === "string" || value === null)) {
|
|
320
|
+
this.s.overwrite(existing.literal.start, existing.literal.end, JSON.stringify(value ?? ""));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const code = opts.raw !== void 0 ? `${name}={${opts.raw}}` : value === null || value === void 0 ? null : attrCode(name, value);
|
|
324
|
+
if (existing) {
|
|
325
|
+
if (code === null) {
|
|
326
|
+
let start = existing.start;
|
|
327
|
+
while (start > 0 && /\s/.test(this.source[start - 1])) start--;
|
|
328
|
+
this.s.remove(start, existing.end);
|
|
329
|
+
} else {
|
|
330
|
+
this.s.overwrite(existing.start, existing.end, code, { contentOnly: true });
|
|
331
|
+
}
|
|
332
|
+
} else if (code !== null) {
|
|
333
|
+
const at = node.attrs.length ? node.attrs[node.attrs.length - 1].end : node.nameEnd;
|
|
334
|
+
this.s.appendRight(at, ` ${code}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
setText(node, text) {
|
|
338
|
+
if (node.kind === "text") {
|
|
339
|
+
this.s.overwrite(this.contentStart(node), this.contentEnd(node), textCode(text) || '{""}');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (node.kind !== "component" && node.kind !== "element") throw new EditError(`Cannot set text on a ${node.kind} node.`);
|
|
343
|
+
if (node.children.some((c) => c.kind !== "text")) {
|
|
344
|
+
throw new EditError(`Node ${node.id} has element children; edit a text child instead.`);
|
|
345
|
+
}
|
|
346
|
+
const code = textCode(text);
|
|
347
|
+
if (node.selfClosing) {
|
|
348
|
+
let end = node.openEnd - 2;
|
|
349
|
+
while (/\s/.test(this.source[end - 1])) end--;
|
|
350
|
+
this.s.overwrite(end, node.openEnd, `>${code}</${node.name}>`);
|
|
351
|
+
} else if (node.openEnd === node.closeStart) {
|
|
352
|
+
this.s.appendLeft(node.openEnd, code);
|
|
353
|
+
} else {
|
|
354
|
+
const comments = this.source.slice(node.openEnd, node.closeStart).match(/\{\s*\/\*[\s\S]*?\*\/\s*\}/g) ?? [];
|
|
355
|
+
this.s.overwrite(node.openEnd, node.closeStart, comments.join("") + code);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// ---------- indentation helpers: edits match the file's existing style ----------
|
|
359
|
+
unitCache = null;
|
|
360
|
+
/** The file's indent unit: tabs, or the smallest non-zero space indent. */
|
|
361
|
+
unit() {
|
|
362
|
+
if (this.unitCache) return this.unitCache;
|
|
363
|
+
if (/^\t/m.test(this.source)) return this.unitCache = " ";
|
|
364
|
+
let min = Infinity;
|
|
365
|
+
for (const m of this.source.matchAll(/^( +)(?!\*)\S/gm)) min = Math.min(min, m[1].length);
|
|
366
|
+
return this.unitCache = " ".repeat(Number.isFinite(min) && min <= 8 ? min : 2);
|
|
367
|
+
}
|
|
368
|
+
lineIndent(pos) {
|
|
369
|
+
const lineStart = this.source.lastIndexOf("\n", pos - 1) + 1;
|
|
370
|
+
return /^[ \t]*/.exec(this.source.slice(lineStart))[0];
|
|
371
|
+
}
|
|
372
|
+
/** Where a node's real content starts/ends (JSX text includes surrounding whitespace). */
|
|
373
|
+
contentStart(n) {
|
|
374
|
+
if (n.kind !== "text") return n.start;
|
|
375
|
+
const raw = this.source.slice(n.start, n.end);
|
|
376
|
+
return n.start + (raw.length - raw.trimStart().length);
|
|
377
|
+
}
|
|
378
|
+
contentEnd(n) {
|
|
379
|
+
if (n.kind !== "text") return n.end;
|
|
380
|
+
return n.start + this.source.slice(n.start, n.end).trimEnd().length;
|
|
381
|
+
}
|
|
382
|
+
childIndent(parent) {
|
|
383
|
+
const kid = parent.children.find((c) => c.kind !== "text") ?? parent.children[0];
|
|
384
|
+
if (kid) {
|
|
385
|
+
const pos = this.contentStart(kid);
|
|
386
|
+
const lineStart = this.source.lastIndexOf("\n", pos - 1) + 1;
|
|
387
|
+
if (/^[ \t]*$/.test(this.source.slice(lineStart, pos))) return this.lineIndent(pos);
|
|
388
|
+
}
|
|
389
|
+
return this.lineIndent(parent.start) + this.unit();
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Re-indents lines 2..n of a snippet so it sits at `indent`. Lines that start
|
|
393
|
+
* inside a multi-line string or template literal are content, not code: they
|
|
394
|
+
* are kept exactly as written.
|
|
395
|
+
*/
|
|
396
|
+
shift(code, indent) {
|
|
397
|
+
const trimmed = code.trim();
|
|
398
|
+
const lines = trimmed.split("\n");
|
|
399
|
+
const frozen = literalLines(trimmed);
|
|
400
|
+
const free = (i) => !frozen.has(i);
|
|
401
|
+
const rest = lines.slice(1).filter((l, i) => l.trim() && free(i + 1));
|
|
402
|
+
const base = rest.length ? Math.min(...rest.map((l) => /^[ \t]*/.exec(l)[0].length)) : 0;
|
|
403
|
+
const relative = lines.slice(1).map((l, i) => free(i + 1) ? l.trim() ? l.slice(base) : "" : l);
|
|
404
|
+
const steps = relative.filter((_, i) => free(i + 1)).map((l) => /^ */.exec(l)[0].length).filter((n) => n > 0);
|
|
405
|
+
const from = steps.length ? Math.min(...steps) : 0;
|
|
406
|
+
const unit = this.unit();
|
|
407
|
+
const convert = (l) => {
|
|
408
|
+
if (!from || unit === " ".repeat(from)) return l;
|
|
409
|
+
const n = /^ */.exec(l)[0].length;
|
|
410
|
+
return unit.repeat(Math.floor(n / from)) + " ".repeat(n % from) + l.slice(n);
|
|
411
|
+
};
|
|
412
|
+
return [lines[0], ...relative.map((l, i) => !free(i + 1) ? l : l ? indent + convert(l) : "")].join("\n");
|
|
413
|
+
}
|
|
414
|
+
openToClose(node, inner) {
|
|
415
|
+
let end = node.openEnd - 2;
|
|
416
|
+
while (/\s/.test(this.source[end - 1])) end--;
|
|
417
|
+
this.s.overwrite(end, node.openEnd, `>${inner}</${node.name}>`);
|
|
418
|
+
}
|
|
419
|
+
insertAt(parent, kids, i, code) {
|
|
420
|
+
const ci = this.childIndent(parent);
|
|
421
|
+
const pi = this.lineIndent(parent.start);
|
|
422
|
+
const body = this.shift(code, ci);
|
|
423
|
+
if (parent.selfClosing) {
|
|
424
|
+
this.openToClose(parent, `
|
|
425
|
+
${ci}${body}
|
|
426
|
+
${pi}`);
|
|
427
|
+
} else if (i < kids.length) {
|
|
428
|
+
this.s.appendRight(this.contentStart(kids[i]), `${body}
|
|
429
|
+
${ci}`);
|
|
430
|
+
} else if (kids.length) {
|
|
431
|
+
this.s.appendLeft(this.contentEnd(kids[kids.length - 1]), `
|
|
432
|
+
${ci}${body}`);
|
|
433
|
+
} else if (parent.openEnd === parent.closeStart) {
|
|
434
|
+
this.s.appendLeft(parent.openEnd, `
|
|
435
|
+
${ci}${body}
|
|
436
|
+
${pi}`);
|
|
437
|
+
} else if (/^\s*$/.test(this.source.slice(parent.openEnd, parent.closeStart))) {
|
|
438
|
+
this.s.overwrite(parent.openEnd, parent.closeStart, `
|
|
439
|
+
${ci}${body}
|
|
440
|
+
${pi}`);
|
|
441
|
+
} else {
|
|
442
|
+
this.s.appendRight(parent.closeStart, `${body}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
insertChild(parent, index, jsx) {
|
|
446
|
+
const kids = parent.children;
|
|
447
|
+
const i = index === void 0 || index < 0 || index > kids.length ? kids.length : index;
|
|
448
|
+
this.insertAt(parent, kids, i, jsx);
|
|
449
|
+
this.selectPaths.push([...parent.path, i]);
|
|
450
|
+
}
|
|
451
|
+
/** Removes a node together with its line when it sits alone on it. */
|
|
452
|
+
remove(node) {
|
|
453
|
+
const start = this.contentStart(node);
|
|
454
|
+
const end = this.contentEnd(node);
|
|
455
|
+
let a = start;
|
|
456
|
+
while (a > 0 && (this.source[a - 1] === " " || this.source[a - 1] === " ")) a--;
|
|
457
|
+
const restOfLine = /^[ \t]*(\n|$)/.test(this.source.slice(end));
|
|
458
|
+
if (a > 0 && this.source[a - 1] === "\n" && restOfLine) this.s.remove(a - 1, end);
|
|
459
|
+
else this.s.remove(node.start, node.end);
|
|
460
|
+
}
|
|
461
|
+
duplicate(node) {
|
|
462
|
+
let code = this.source.slice(node.start, node.end);
|
|
463
|
+
if ("frameName" in node) {
|
|
464
|
+
const f = node;
|
|
465
|
+
this.usedFrameNames ??= new Set(this.doc.frames.map((x) => x.frameName));
|
|
466
|
+
this.placed ??= this.doc.frames.map((o) => ({ x: o.x, y: o.y, width: o.width, height: o.height ?? 600 }));
|
|
467
|
+
let name = `${f.frameName} copy`;
|
|
468
|
+
for (let n = 2; this.usedFrameNames.has(name); n++) name = `${f.frameName} copy ${n}`;
|
|
469
|
+
this.usedFrameNames.add(name);
|
|
470
|
+
const h = f.height ?? 600;
|
|
471
|
+
const row = this.placed.filter((o) => o.y < f.y + Math.max(h, 1) && o.y + Math.max(o.height, 1) > f.y);
|
|
472
|
+
const right = row.length ? Math.max(...row.map((o) => o.x + o.width)) : f.x + f.width;
|
|
473
|
+
this.placed.push({ x: right + 80, y: f.y, width: f.width, height: h });
|
|
474
|
+
code = rewriteFrameProps(code, { name, x: right + 80 });
|
|
475
|
+
}
|
|
476
|
+
const marked = withMarker(code.trim(), node);
|
|
477
|
+
this.s.appendLeft(this.contentEnd(node), `
|
|
478
|
+
${this.lineIndent(node.start)}${marked}`);
|
|
479
|
+
if (marked === code.trim()) {
|
|
480
|
+
const path2 = [...node.path];
|
|
481
|
+
path2[path2.length - 1] += 1;
|
|
482
|
+
this.selectPaths.push(path2);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
move(node, target, index) {
|
|
486
|
+
if (isInside(target, node)) throw new EditError("Cannot move a node into itself.");
|
|
487
|
+
const code = withMarker(this.source.slice(this.contentStart(node), this.contentEnd(node)), node);
|
|
488
|
+
const kids = target.children.filter((c) => c.id !== node.id);
|
|
489
|
+
const i = index === void 0 || index < 0 || index > kids.length ? kids.length : index;
|
|
490
|
+
this.remove(node);
|
|
491
|
+
this.insertAt(target, kids, i, code);
|
|
492
|
+
}
|
|
493
|
+
wrap(nodes, open, close) {
|
|
494
|
+
const sorted = [...nodes].sort((a, b) => a.start - b.start);
|
|
495
|
+
const first = sorted[0];
|
|
496
|
+
const last = sorted[sorted.length - 1];
|
|
497
|
+
if (sorted.some((n) => "frameName" in n)) throw new EditError("Frames can't be wrapped; wrap the layers inside a frame.");
|
|
498
|
+
const parentPath = first.path.slice(0, -1).join(",");
|
|
499
|
+
if (sorted.some((n) => n.path.slice(0, -1).join(",") !== parentPath)) throw new EditError("Nodes to wrap must share the same parent.");
|
|
500
|
+
const indices = sorted.map((n) => n.path[n.path.length - 1]);
|
|
501
|
+
const parent = findByPath(this.doc, first.path.slice(0, -1));
|
|
502
|
+
const between = parent?.children.slice(indices[0], indices[indices.length - 1] + 1) ?? [];
|
|
503
|
+
if (between.some((c) => c.kind !== "text" && !sorted.includes(c))) throw new EditError("Nodes to wrap must be adjacent siblings.");
|
|
504
|
+
const ind = this.lineIndent(this.contentStart(first));
|
|
505
|
+
const inner = ind + this.unit();
|
|
506
|
+
const slice = this.source.slice(this.contentStart(first), this.contentEnd(last));
|
|
507
|
+
this.s.overwrite(this.contentStart(first), this.contentEnd(last), `${open}
|
|
508
|
+
${inner}${this.shift(slice, inner)}
|
|
509
|
+
${ind}${close}`);
|
|
510
|
+
this.selectPaths.push(first.path);
|
|
511
|
+
}
|
|
512
|
+
/** Like wrap, but keeps content that sits on one line on one line: `<p><X>Hi</X></p>`. */
|
|
513
|
+
wrapInline(nodes, open, close) {
|
|
514
|
+
const sorted = [...nodes].sort((a, b) => a.start - b.start);
|
|
515
|
+
const start = this.contentStart(sorted[0]);
|
|
516
|
+
const end = this.contentEnd(sorted[sorted.length - 1]);
|
|
517
|
+
const slice = this.source.slice(start, end);
|
|
518
|
+
if (slice.includes("\n")) return this.wrap(nodes, open, close);
|
|
519
|
+
this.s.overwrite(start, end, `${open}${slice}${close}`);
|
|
520
|
+
this.selectPaths.push(sorted[0].path);
|
|
521
|
+
}
|
|
522
|
+
/** Replaces a wrapper element with its children (re-indented to its place). */
|
|
523
|
+
unwrap(node) {
|
|
524
|
+
const kids = node.children;
|
|
525
|
+
if (!kids.length) {
|
|
526
|
+
this.remove(node);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const inner = node.selfClosing ? "" : this.source.slice(node.openEnd, node.closeStart).trim();
|
|
530
|
+
this.s.overwrite(this.contentStart(node), this.contentEnd(node), this.shift(inner, this.lineIndent(this.contentStart(node))));
|
|
531
|
+
this.selectPaths.push(node.path);
|
|
532
|
+
}
|
|
533
|
+
replace(node, jsx) {
|
|
534
|
+
this.s.overwrite(this.contentStart(node), this.contentEnd(node), this.shift(jsx, this.lineIndent(this.contentStart(node))));
|
|
535
|
+
this.selectPaths.push(node.path);
|
|
536
|
+
}
|
|
537
|
+
added = /* @__PURE__ */ new Set();
|
|
538
|
+
ensureImport(name, from, isDefault = false) {
|
|
539
|
+
if (this.doc.imports.some((i) => i.names.includes(name) || i.defaultName === name)) return;
|
|
540
|
+
if (this.added.has(name)) return;
|
|
541
|
+
this.added.add(name);
|
|
542
|
+
if (isDefault) {
|
|
543
|
+
this.addImportLine(`import ${name} from ${JSON.stringify(from)};`);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const same = this.doc.imports.find((i) => i.source === from && !i.typeOnly && i.lastSpecifierEnd !== null);
|
|
547
|
+
if (same) {
|
|
548
|
+
this.s.appendLeft(same.lastSpecifierEnd, `, ${name}`);
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
this.addImportLine(`import { ${name} } from ${JSON.stringify(from)};`);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Imports `name` from `from` and returns the local name to write in JSX. When
|
|
555
|
+
* the name is already taken by something else (another import, a local
|
|
556
|
+
* component), it is imported under `alias` instead: `{ Search as SearchIcon }`.
|
|
557
|
+
*/
|
|
558
|
+
importAs(name, from, alias) {
|
|
559
|
+
const existing = this.doc.imports.find((i) => i.source === from && !i.typeOnly)?.bindings.find((b) => b.imported === name);
|
|
560
|
+
if (existing) return existing.local;
|
|
561
|
+
const key = `${from}\0${name}`;
|
|
562
|
+
const pending = this.importedAs.get(key);
|
|
563
|
+
if (pending) return pending;
|
|
564
|
+
const taken = (n) => this.doc.declared.includes(n) || this.added.has(n) || this.doc.imports.some((i) => i.names.includes(n) || i.defaultName === n);
|
|
565
|
+
let local = name;
|
|
566
|
+
if (taken(local)) {
|
|
567
|
+
local = alias;
|
|
568
|
+
for (let k = 2; taken(local); k++) local = `${alias}${k}`;
|
|
569
|
+
}
|
|
570
|
+
this.added.add(local);
|
|
571
|
+
this.importedAs.set(key, local);
|
|
572
|
+
const specifier = local === name ? name : `${name} as ${local}`;
|
|
573
|
+
const same = this.doc.imports.find((i) => i.source === from && !i.typeOnly && i.lastSpecifierEnd !== null);
|
|
574
|
+
if (same) this.s.appendLeft(same.lastSpecifierEnd, `, ${specifier}`);
|
|
575
|
+
else this.addImportLine(`import { ${specifier} } from ${JSON.stringify(from)};`);
|
|
576
|
+
return local;
|
|
577
|
+
}
|
|
578
|
+
importedAs = /* @__PURE__ */ new Map();
|
|
579
|
+
addImportLine(line) {
|
|
580
|
+
const last = this.doc.imports[this.doc.imports.length - 1];
|
|
581
|
+
if (last) this.s.appendLeft(last.end, `
|
|
582
|
+
${line}`);
|
|
583
|
+
else if (this.doc.prologueEnd) this.s.appendLeft(this.doc.prologueEnd, `
|
|
584
|
+
${line}`);
|
|
585
|
+
else this.prependImport(line);
|
|
586
|
+
}
|
|
587
|
+
/** Adds a whole import statement unless every name it binds is already imported. */
|
|
588
|
+
addImportStatement(statement) {
|
|
589
|
+
let names = [];
|
|
590
|
+
try {
|
|
591
|
+
const ast = parseModule(statement);
|
|
592
|
+
const decl = ast.program.body[0];
|
|
593
|
+
if (decl?.type === "ImportDeclaration") names = decl.specifiers.map((sp) => sp.local.name);
|
|
594
|
+
} catch {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
const known = /* @__PURE__ */ new Set([...this.doc.imports.flatMap((i) => [...i.names, ...i.defaultName ? [i.defaultName] : []]), ...this.added]);
|
|
598
|
+
if (names.length && names.every((n) => known.has(n))) return;
|
|
599
|
+
for (const n of names) this.added.add(n);
|
|
600
|
+
this.addImportLine(statement);
|
|
601
|
+
}
|
|
602
|
+
importsPrepended = false;
|
|
603
|
+
/** First imports of a file: in order, with a blank line before the code. */
|
|
604
|
+
prependImport(line) {
|
|
605
|
+
if (!this.importsPrepended) {
|
|
606
|
+
this.importsPrepended = true;
|
|
607
|
+
this.s.prependRight(0, "\n");
|
|
608
|
+
}
|
|
609
|
+
this.s.appendLeft(0, `${line}
|
|
610
|
+
`);
|
|
611
|
+
}
|
|
612
|
+
insertFrame(code) {
|
|
613
|
+
const canvas = this.doc.canvasNode;
|
|
614
|
+
if (!canvas) throw new EditError("No <Canvas> element to add a frame to.");
|
|
615
|
+
this.insertAt(canvas, canvas.children, canvas.children.length, code);
|
|
616
|
+
this.selectPaths.push([this.doc.frames.length]);
|
|
617
|
+
}
|
|
618
|
+
result() {
|
|
619
|
+
return this.s.toString();
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
function isInside(target, node) {
|
|
623
|
+
return target.start >= node.start && target.end <= node.end;
|
|
624
|
+
}
|
|
625
|
+
function rewriteFrameProps(code, props) {
|
|
626
|
+
const wrapped = `(${code})`;
|
|
627
|
+
const ast = parseModule(wrapped);
|
|
628
|
+
let open = null;
|
|
629
|
+
walk(ast, (n) => {
|
|
630
|
+
if (open) return false;
|
|
631
|
+
if (n.type === "JSXOpeningElement") open = n;
|
|
632
|
+
});
|
|
633
|
+
if (!open) return code;
|
|
634
|
+
const o = open;
|
|
635
|
+
const s = new MagicString(wrapped);
|
|
636
|
+
for (const [name, value] of Object.entries(props)) {
|
|
637
|
+
const attr = o.attributes.find((a) => a.type === "JSXAttribute" && a.name.name === name);
|
|
638
|
+
if (attr) {
|
|
639
|
+
if (value === null) s.remove(attr.start - 1, attr.end);
|
|
640
|
+
else s.overwrite(attr.start, attr.end, attrCode(name, value));
|
|
641
|
+
} else if (value !== null) s.appendLeft(o.name.end, ` ${attrCode(name, value)}`);
|
|
642
|
+
}
|
|
643
|
+
return s.toString().slice(1, -1);
|
|
644
|
+
}
|
|
645
|
+
function componentNamesIn(jsx) {
|
|
646
|
+
const names = /* @__PURE__ */ new Set();
|
|
647
|
+
const ast = parseJsx(jsx);
|
|
648
|
+
walk(ast, (n) => {
|
|
649
|
+
if (n.type === "JSXOpeningElement") {
|
|
650
|
+
const name = jsxName(n.name);
|
|
651
|
+
if (/^[A-Z]/.test(name) || name.includes(".")) names.add(name.split(".")[0]);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
return [...names];
|
|
655
|
+
}
|
|
656
|
+
function withMarker(code, node) {
|
|
657
|
+
if (node.kind !== "component" && node.kind !== "element") return code;
|
|
658
|
+
const m = /^<[\w.:-]+/.exec(code);
|
|
659
|
+
if (!m) return code;
|
|
660
|
+
let i = m[0].length;
|
|
661
|
+
if (code[i] === "<") {
|
|
662
|
+
for (let depth = 0; i < code.length; i++) {
|
|
663
|
+
if (code[i] === "<") depth++;
|
|
664
|
+
else if (code[i] === ">" && --depth === 0) {
|
|
665
|
+
i++;
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return `${code.slice(0, i)} __tcsel${code.slice(i)}`;
|
|
671
|
+
}
|
|
672
|
+
function literalLines(code) {
|
|
673
|
+
const out = /* @__PURE__ */ new Set();
|
|
674
|
+
if (!code.includes("\n") || !/[`"']/.test(code)) return out;
|
|
675
|
+
let ast;
|
|
676
|
+
try {
|
|
677
|
+
ast = parseModule(`<>${code}</>`);
|
|
678
|
+
} catch {
|
|
679
|
+
return out;
|
|
680
|
+
}
|
|
681
|
+
const ranges = [];
|
|
682
|
+
walk(ast, (n) => {
|
|
683
|
+
if (n.type === "TemplateLiteral" || n.type === "StringLiteral") ranges.push([n.start - 2, n.end - 2]);
|
|
684
|
+
});
|
|
685
|
+
if (!ranges.length) return out;
|
|
686
|
+
let offset = 0;
|
|
687
|
+
code.split("\n").forEach((line, i) => {
|
|
688
|
+
if (i > 0 && ranges.some(([a, b]) => offset > a && offset < b)) out.add(i);
|
|
689
|
+
offset += line.length + 1;
|
|
690
|
+
});
|
|
691
|
+
return out;
|
|
692
|
+
}
|
|
693
|
+
function parseJsx(jsx) {
|
|
694
|
+
try {
|
|
695
|
+
return parseModule(`<>${jsx}</>`);
|
|
696
|
+
} catch (err) {
|
|
697
|
+
throw new EditError(`Invalid JSX: ${err.message.replace(/\(\d+:\d+\)$/, "")}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function frameCode(opts) {
|
|
701
|
+
const attrs = [attrCode("name", opts.name), attrCode("x", Math.round(opts.x)), attrCode("y", Math.round(opts.y)), attrCode("width", Math.round(opts.width))];
|
|
702
|
+
if (opts.height) attrs.push(attrCode("height", Math.round(opts.height)));
|
|
703
|
+
if (opts.theme) attrs.push(attrCode("theme", opts.theme));
|
|
704
|
+
if (opts.device) attrs.push(attrCode("device", opts.device));
|
|
705
|
+
if (opts.page) attrs.push(attrCode("page", opts.page));
|
|
706
|
+
if (opts.from) attrs.push(attrCode("from", opts.from));
|
|
707
|
+
if (opts.component) attrs.push(attrCode("component", opts.component));
|
|
708
|
+
const body = opts.body ?? `<div className="flex flex-col gap-3 p-6"></div>`;
|
|
709
|
+
return `<Frame ${attrs.join(" ")}>
|
|
710
|
+
${indentBlock(body)}
|
|
711
|
+
</Frame>`;
|
|
712
|
+
}
|
|
713
|
+
function indentBlock(code, unit = " ") {
|
|
714
|
+
return code.split("\n").map((l) => l.trim() ? unit + l : l).join("\n");
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/core/pm.ts
|
|
718
|
+
import { spawn } from "child_process";
|
|
719
|
+
import fs from "fs";
|
|
720
|
+
import path from "path";
|
|
721
|
+
function detectPm(dir) {
|
|
722
|
+
for (let d = dir; ; d = path.dirname(d)) {
|
|
723
|
+
if (fs.existsSync(path.join(d, "pnpm-lock.yaml")) || fs.existsSync(path.join(d, "pnpm-workspace.yaml"))) return "pnpm";
|
|
724
|
+
if (fs.existsSync(path.join(d, "yarn.lock"))) return "yarn";
|
|
725
|
+
if (fs.existsSync(path.join(d, "bun.lock")) || fs.existsSync(path.join(d, "bun.lockb"))) return "bun";
|
|
726
|
+
if (fs.existsSync(path.join(d, "package-lock.json"))) return "npm";
|
|
727
|
+
if (path.dirname(d) === d || fs.existsSync(path.join(d, ".git"))) break;
|
|
728
|
+
}
|
|
729
|
+
const agent = process.env.npm_config_user_agent ?? "";
|
|
730
|
+
if (agent.startsWith("pnpm")) return "pnpm";
|
|
731
|
+
if (agent.startsWith("yarn")) return "yarn";
|
|
732
|
+
if (agent.startsWith("bun")) return "bun";
|
|
733
|
+
return "npm";
|
|
734
|
+
}
|
|
735
|
+
function addArgs(pm, specs, dev = false) {
|
|
736
|
+
if (pm === "npm") return ["install", ...dev ? ["-D"] : [], ...specs];
|
|
737
|
+
if (pm === "bun") return ["add", ...dev ? ["-d"] : [], ...specs];
|
|
738
|
+
return ["add", ...dev ? ["-D"] : [], ...specs];
|
|
739
|
+
}
|
|
740
|
+
function dlx(pm, pkg, args) {
|
|
741
|
+
if (pm === "pnpm") return ["pnpm", ["dlx", pkg, ...args]];
|
|
742
|
+
if (pm === "yarn") return ["yarn", ["dlx", pkg, ...args]];
|
|
743
|
+
if (pm === "bun") return ["bunx", [pkg, ...args]];
|
|
744
|
+
return ["npx", ["--yes", pkg, ...args]];
|
|
745
|
+
}
|
|
746
|
+
var runtime = null;
|
|
747
|
+
function useNodeRuntime(node, env) {
|
|
748
|
+
runtime = { node, env };
|
|
749
|
+
}
|
|
750
|
+
function nodeBin() {
|
|
751
|
+
return runtime ? runtime.node : process.execPath;
|
|
752
|
+
}
|
|
753
|
+
function toolEnv(extra = {}) {
|
|
754
|
+
const base = runtime?.env ?? process.env;
|
|
755
|
+
const node = nodeBin();
|
|
756
|
+
return { ...base, PATH: node ? `${path.dirname(node)}${path.delimiter}${base.PATH ?? ""}` : base.PATH ?? "", ...extra };
|
|
757
|
+
}
|
|
758
|
+
function shellSafe(cmd, args) {
|
|
759
|
+
if (process.platform !== "win32") return { cmd, args, shell: false };
|
|
760
|
+
const quote = (a) => /[\s"&|<>^()%!]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a;
|
|
761
|
+
return { cmd: quote(cmd), args: args.map(quote), shell: true };
|
|
762
|
+
}
|
|
763
|
+
function run(cmd, args, cwd, timeoutMs = 3e5) {
|
|
764
|
+
return new Promise((resolve) => {
|
|
765
|
+
let out = "";
|
|
766
|
+
let done = false;
|
|
767
|
+
const finish = (ok, extra = "") => {
|
|
768
|
+
if (done) return;
|
|
769
|
+
done = true;
|
|
770
|
+
clearTimeout(timer);
|
|
771
|
+
resolve({ ok, out: (out + extra).slice(-2e4) });
|
|
772
|
+
};
|
|
773
|
+
const safe = shellSafe(cmd, args);
|
|
774
|
+
const p = spawn(safe.cmd, safe.args, { cwd, stdio: ["ignore", "pipe", "pipe"], env: toolEnv({ CI: "1", FORCE_COLOR: "0", NO_COLOR: "1" }), shell: safe.shell });
|
|
775
|
+
const timer = setTimeout(() => {
|
|
776
|
+
p.kill();
|
|
777
|
+
finish(false, `
|
|
778
|
+
${cmd} timed out after ${Math.round(timeoutMs / 1e3)}s`);
|
|
779
|
+
}, timeoutMs);
|
|
780
|
+
p.stdout.on("data", (d) => out += d);
|
|
781
|
+
p.stderr.on("data", (d) => out += d);
|
|
782
|
+
p.on("error", (err) => finish(false, `
|
|
783
|
+
${err.message}`));
|
|
784
|
+
p.on("close", (code) => finish(code === 0));
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
export {
|
|
789
|
+
parseCanvas,
|
|
790
|
+
readImports,
|
|
791
|
+
topLevelNames,
|
|
792
|
+
reindex,
|
|
793
|
+
buildNode,
|
|
794
|
+
allNodes,
|
|
795
|
+
findNode,
|
|
796
|
+
findFrame,
|
|
797
|
+
findByPath,
|
|
798
|
+
EditError,
|
|
799
|
+
attrCode,
|
|
800
|
+
textCode,
|
|
801
|
+
CanvasEditor,
|
|
802
|
+
componentNamesIn,
|
|
803
|
+
parseJsx,
|
|
804
|
+
frameCode,
|
|
805
|
+
indentBlock,
|
|
806
|
+
detectPm,
|
|
807
|
+
addArgs,
|
|
808
|
+
dlx,
|
|
809
|
+
useNodeRuntime,
|
|
810
|
+
nodeBin,
|
|
811
|
+
toolEnv,
|
|
812
|
+
shellSafe,
|
|
813
|
+
run
|
|
814
|
+
};
|