@aaqiljamal/visual-editor-server 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/bin/visual-editor-server.cjs +21 -0
- package/dist/chunk-QEI2RGE2.js +1584 -0
- package/dist/chunk-QEI2RGE2.js.map +1 -0
- package/dist/cli.js +83 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +285 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1584 @@
|
|
|
1
|
+
// src/fs/applyToFile.ts
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import { createPatch } from "diff";
|
|
4
|
+
|
|
5
|
+
// src/ast/className.ts
|
|
6
|
+
import * as recast from "recast";
|
|
7
|
+
import babelTsParser from "recast/parsers/babel-ts.js";
|
|
8
|
+
import { twMerge } from "tailwind-merge";
|
|
9
|
+
var KNOWN_MERGERS = /* @__PURE__ */ new Set([
|
|
10
|
+
"cn",
|
|
11
|
+
"clsx",
|
|
12
|
+
"classnames",
|
|
13
|
+
"twMerge",
|
|
14
|
+
"twJoin"
|
|
15
|
+
]);
|
|
16
|
+
function mutateClassName(input) {
|
|
17
|
+
const { source, line, col, before, after } = input;
|
|
18
|
+
let ast;
|
|
19
|
+
try {
|
|
20
|
+
ast = recast.parse(source, { parser: babelTsParser });
|
|
21
|
+
} catch (err) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
reason: "dynamic-other",
|
|
25
|
+
details: `parse error: ${err.message}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const out = {
|
|
29
|
+
value: null,
|
|
30
|
+
visited: false
|
|
31
|
+
};
|
|
32
|
+
recast.visit(ast, {
|
|
33
|
+
visitJSXOpeningElement(path4) {
|
|
34
|
+
const node = path4.node;
|
|
35
|
+
const loc = node.loc;
|
|
36
|
+
if (!loc) {
|
|
37
|
+
this.traverse(path4);
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
if (loc.start.line !== line || loc.start.column !== col) {
|
|
41
|
+
this.traverse(path4);
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
out.visited = true;
|
|
45
|
+
out.value = mutateOnNode(node, before, after);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
if (!out.visited) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: "no-jsx-at-location",
|
|
53
|
+
details: `No JSXOpeningElement found at ${line}:${col}`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (!out.value) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
reason: "dynamic-other",
|
|
60
|
+
details: "Internal: visitor matched but produced no result"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (out.value.ok) {
|
|
64
|
+
return { ok: true, output: recast.print(ast).code };
|
|
65
|
+
}
|
|
66
|
+
return out.value;
|
|
67
|
+
}
|
|
68
|
+
function mutateOnNode(node, before, after) {
|
|
69
|
+
const open = node;
|
|
70
|
+
const attrs = open.attributes ?? [];
|
|
71
|
+
const classNameAttr = attrs.find(
|
|
72
|
+
(a) => a.type === "JSXAttribute" && a.name?.type === "JSXIdentifier" && a.name?.name === "className"
|
|
73
|
+
);
|
|
74
|
+
if (!classNameAttr) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
reason: "no-classname-attribute",
|
|
78
|
+
details: "JSX element has no className attribute (possibly composed via {...spread} or passed as a prop)"
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const value = classNameAttr.value;
|
|
82
|
+
if (!value) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
reason: "no-classname-attribute",
|
|
86
|
+
details: "className attribute has no value"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (value.type === "StringLiteral" || value.type === "Literal") {
|
|
90
|
+
const lit = value;
|
|
91
|
+
return swapAndReturn(lit, before, after);
|
|
92
|
+
}
|
|
93
|
+
if (value.type === "JSXExpressionContainer") {
|
|
94
|
+
const ec = value;
|
|
95
|
+
return mutateOnExpression(ec.expression, before, after);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
reason: "dynamic-other",
|
|
100
|
+
details: `className uses unsupported value node type: ${value.type}`
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function mutateOnExpression(expr, before, after) {
|
|
104
|
+
if (expr.type === "StringLiteral" || expr.type === "Literal") {
|
|
105
|
+
const lit = expr;
|
|
106
|
+
return swapAndReturn(lit, before, after);
|
|
107
|
+
}
|
|
108
|
+
if (expr.type === "TemplateLiteral") {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
reason: "dynamic-template-literal",
|
|
112
|
+
details: "className uses a template literal; v0.1 only mutates static string literals at the source location"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (expr.type === "CallExpression") {
|
|
116
|
+
return mutateOnCallExpression(expr, before, after);
|
|
117
|
+
}
|
|
118
|
+
if (expr.type === "ConditionalExpression") {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
reason: "dynamic-conditional",
|
|
122
|
+
details: "className uses a conditional (ternary) expression; v0.1 only mutates static string literals"
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
reason: "dynamic-other",
|
|
128
|
+
details: `className uses unsupported expression type: ${expr.type}`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function swapAndReturn(lit, before, after) {
|
|
132
|
+
const swapped = swapToken(lit.value, before, after);
|
|
133
|
+
if (swapped === null) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
reason: "token-not-found",
|
|
137
|
+
details: `Token "${before}" not found in className value "${lit.value}"`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
lit.value = swapped;
|
|
141
|
+
if (lit.extra) lit.extra = void 0;
|
|
142
|
+
return { ok: true, output: "" };
|
|
143
|
+
}
|
|
144
|
+
function mutateOnCallExpression(call, before, after) {
|
|
145
|
+
const calleeName = getCalleeName(call.callee);
|
|
146
|
+
if (!calleeName || !KNOWN_MERGERS.has(calleeName)) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
reason: "unknown-merger",
|
|
150
|
+
details: `className uses ${calleeName ?? "(call)"}(...). v0.2 only mutates inside known classname mergers: ${[...KNOWN_MERGERS].join(", ")}.`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const args = call.arguments ?? [];
|
|
154
|
+
let targetIdx = -1;
|
|
155
|
+
for (let i = 0; i < args.length; i++) {
|
|
156
|
+
const a = args[i];
|
|
157
|
+
if (a && (a.type === "StringLiteral" || a.type === "Literal") && typeof a.value === "string" && tokensFromValue(a.value).includes(before)) {
|
|
158
|
+
targetIdx = i;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (targetIdx === -1) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
reason: "token-not-found",
|
|
166
|
+
details: `Token "${before}" not present in any static string argument of ${calleeName}(...)`
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
for (let i = 0; i < args.length; i++) {
|
|
170
|
+
if (i === targetIdx) continue;
|
|
171
|
+
const a = args[i];
|
|
172
|
+
if (!a) continue;
|
|
173
|
+
if (a.type === "StringLiteral" || a.type === "Literal") continue;
|
|
174
|
+
if (a.type === "SpreadElement") {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
reason: "dynamic-spread-arg",
|
|
178
|
+
details: `Argument ${i} of ${calleeName}() is a spread. v0.2 cannot analyze spread contents \u2014 refuse to mutate.`
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
ok: false,
|
|
183
|
+
reason: "dynamic-uncertain-arg",
|
|
184
|
+
details: `Argument ${i} of ${calleeName}() is ${a.type}. v0.2 refuses to mutate when any other argument is non-static.`
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const targetArg = args[targetIdx];
|
|
188
|
+
const newTargetValue = swapToken(targetArg.value, before, after);
|
|
189
|
+
if (newTargetValue === null) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
reason: "token-not-found",
|
|
193
|
+
details: `Internal: target arg matched but swap couldn't find "${before}"`
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
const concatenated = args.map(
|
|
197
|
+
(a, i) => i === targetIdx ? newTargetValue : a.value ?? ""
|
|
198
|
+
).join(" ").trim();
|
|
199
|
+
const mergedTokens = twMerge(concatenated).split(/\s+/).filter(Boolean);
|
|
200
|
+
const afterTokens = after.split(/\s+/).filter(Boolean);
|
|
201
|
+
for (const tok of afterTokens) {
|
|
202
|
+
if (!mergedTokens.includes(tok)) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
reason: "dynamic-conflict",
|
|
206
|
+
details: `Mutation would be silently overridden: tailwind-merge resolves "${concatenated}" to "${mergedTokens.join(" ")}", which doesn't include "${tok}". A later argument wins.`
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
targetArg.value = newTargetValue;
|
|
211
|
+
if (targetArg.extra) targetArg.extra = void 0;
|
|
212
|
+
return { ok: true, output: "" };
|
|
213
|
+
}
|
|
214
|
+
function mutateAttribute(input) {
|
|
215
|
+
const { source, line, col, attribute, before, after } = input;
|
|
216
|
+
let ast;
|
|
217
|
+
try {
|
|
218
|
+
ast = recast.parse(source, { parser: babelTsParser });
|
|
219
|
+
} catch (err) {
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
reason: "parse-error",
|
|
223
|
+
details: err.message
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const out = { value: null, visited: false };
|
|
227
|
+
recast.visit(ast, {
|
|
228
|
+
visitJSXOpeningElement(path4) {
|
|
229
|
+
const node = path4.node;
|
|
230
|
+
const loc = node.loc;
|
|
231
|
+
if (!loc) {
|
|
232
|
+
this.traverse(path4);
|
|
233
|
+
return void 0;
|
|
234
|
+
}
|
|
235
|
+
if (loc.start.line !== line || loc.start.column !== col) {
|
|
236
|
+
this.traverse(path4);
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
out.visited = true;
|
|
240
|
+
const attrs = node.attributes ?? [];
|
|
241
|
+
const targetAttr = attrs.find(
|
|
242
|
+
(a) => a.type === "JSXAttribute" && a.name?.type === "JSXIdentifier" && a.name?.name === attribute
|
|
243
|
+
);
|
|
244
|
+
if (!targetAttr) {
|
|
245
|
+
out.value = {
|
|
246
|
+
ok: false,
|
|
247
|
+
reason: "no-such-attribute",
|
|
248
|
+
details: `JSX element has no \`${attribute}\` attribute`
|
|
249
|
+
};
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
const v = targetAttr.value;
|
|
253
|
+
if (!v) {
|
|
254
|
+
out.value = {
|
|
255
|
+
ok: false,
|
|
256
|
+
reason: "no-such-attribute",
|
|
257
|
+
details: `Attribute \`${attribute}\` has no value`
|
|
258
|
+
};
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
let lit = null;
|
|
262
|
+
if (v.type === "StringLiteral" || v.type === "Literal") {
|
|
263
|
+
lit = v;
|
|
264
|
+
} else if (v.type === "JSXExpressionContainer") {
|
|
265
|
+
const expr = v.expression;
|
|
266
|
+
if (expr.type === "StringLiteral" || expr.type === "Literal") {
|
|
267
|
+
lit = expr;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (!lit) {
|
|
271
|
+
out.value = {
|
|
272
|
+
ok: false,
|
|
273
|
+
reason: "dynamic-value",
|
|
274
|
+
details: `Attribute \`${attribute}\` value is ${v.type}, not a static string literal`
|
|
275
|
+
};
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
if (before !== null && lit.value !== before) {
|
|
279
|
+
out.value = {
|
|
280
|
+
ok: false,
|
|
281
|
+
reason: "token-not-found",
|
|
282
|
+
details: `Current value "${lit.value}" doesn't match expected "${before}" \u2014 file may have changed externally`
|
|
283
|
+
};
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const previousValue = lit.value;
|
|
287
|
+
lit.value = after;
|
|
288
|
+
if (lit.extra) lit.extra = void 0;
|
|
289
|
+
out.value = {
|
|
290
|
+
ok: true,
|
|
291
|
+
output: "",
|
|
292
|
+
previousValue
|
|
293
|
+
};
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
if (!out.visited) {
|
|
298
|
+
return {
|
|
299
|
+
ok: false,
|
|
300
|
+
reason: "no-jsx-at-location",
|
|
301
|
+
details: `No JSXOpeningElement found at ${line}:${col}`
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (!out.value) {
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
reason: "no-jsx-at-location",
|
|
308
|
+
details: "Visitor matched location but produced no result"
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (out.value.ok) {
|
|
312
|
+
return {
|
|
313
|
+
ok: true,
|
|
314
|
+
output: recast.print(ast).code,
|
|
315
|
+
previousValue: out.value.previousValue
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return out.value;
|
|
319
|
+
}
|
|
320
|
+
function getCalleeName(callee) {
|
|
321
|
+
if (!callee || typeof callee !== "object") return null;
|
|
322
|
+
const c = callee;
|
|
323
|
+
if (c.type === "Identifier") return c.name ?? null;
|
|
324
|
+
if (c.type === "MemberExpression") return c.property?.name ?? null;
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
function tokensFromValue(value) {
|
|
328
|
+
return value.split(/\s+/).filter(Boolean);
|
|
329
|
+
}
|
|
330
|
+
function swapToken(value, before, after) {
|
|
331
|
+
const parts = value.split(/(\s+)/);
|
|
332
|
+
let found = false;
|
|
333
|
+
const out = parts.map((p) => {
|
|
334
|
+
if (!found && p === before) {
|
|
335
|
+
found = true;
|
|
336
|
+
return after;
|
|
337
|
+
}
|
|
338
|
+
return p;
|
|
339
|
+
});
|
|
340
|
+
if (!found) return null;
|
|
341
|
+
return out.join("");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/fs/resolveSafe.ts
|
|
345
|
+
import * as path from "path";
|
|
346
|
+
function resolveWithinWorkspace(workspaceRoot, relativePath) {
|
|
347
|
+
if (typeof relativePath !== "string" || relativePath.length === 0) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const root = path.resolve(workspaceRoot);
|
|
351
|
+
const candidate = path.resolve(root, relativePath);
|
|
352
|
+
if (candidate !== root && !candidate.startsWith(root + path.sep)) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
return candidate;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/fs/applyToFile.ts
|
|
359
|
+
async function applyToFile(input, options) {
|
|
360
|
+
if (!isValidApplyInput(input)) {
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
status: 400,
|
|
364
|
+
reason: "invalid-input",
|
|
365
|
+
details: "Body must be { file: string, line: number, col: number, before: string, after: string }"
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const absolutePath = resolveWithinWorkspace(options.workspaceRoot, input.file);
|
|
369
|
+
if (!absolutePath) {
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
status: 403,
|
|
373
|
+
reason: "path-outside-workspace",
|
|
374
|
+
details: `Refusing to touch path outside workspace root: ${input.file}`
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
let source;
|
|
378
|
+
try {
|
|
379
|
+
source = await fs.readFile(absolutePath, "utf8");
|
|
380
|
+
} catch (err) {
|
|
381
|
+
const e = err;
|
|
382
|
+
if (e.code === "ENOENT") {
|
|
383
|
+
return {
|
|
384
|
+
ok: false,
|
|
385
|
+
status: 404,
|
|
386
|
+
reason: "file-not-found",
|
|
387
|
+
details: `File not found: ${input.file}`
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
ok: false,
|
|
392
|
+
status: 500,
|
|
393
|
+
reason: "read-failed",
|
|
394
|
+
details: `Failed to read ${input.file}: ${e.message}`
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const attribute = input.attribute ?? "className";
|
|
398
|
+
const mutation = attribute === "className" ? mutateClassName({
|
|
399
|
+
source,
|
|
400
|
+
line: input.line,
|
|
401
|
+
col: input.col,
|
|
402
|
+
// mutateClassName requires before; reject if it's null
|
|
403
|
+
before: input.before ?? "",
|
|
404
|
+
after: input.after
|
|
405
|
+
}) : mutateAttribute({
|
|
406
|
+
source,
|
|
407
|
+
line: input.line,
|
|
408
|
+
col: input.col,
|
|
409
|
+
attribute,
|
|
410
|
+
before: input.before,
|
|
411
|
+
after: input.after
|
|
412
|
+
});
|
|
413
|
+
if (!mutation.ok) {
|
|
414
|
+
const status = mutation.reason === "token-not-found" ? 409 : 400;
|
|
415
|
+
return {
|
|
416
|
+
ok: false,
|
|
417
|
+
status,
|
|
418
|
+
reason: mutation.reason,
|
|
419
|
+
details: mutation.details
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const diff = createPatch(input.file, source, mutation.output, "before", "after");
|
|
423
|
+
if (!options.dryRun) {
|
|
424
|
+
try {
|
|
425
|
+
await fs.writeFile(absolutePath, mutation.output, "utf8");
|
|
426
|
+
} catch (err) {
|
|
427
|
+
const e = err;
|
|
428
|
+
return {
|
|
429
|
+
ok: false,
|
|
430
|
+
status: 500,
|
|
431
|
+
reason: "write-failed",
|
|
432
|
+
details: `Failed to write ${input.file}: ${e.message}`
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const previousValue = "previousValue" in mutation && typeof mutation.previousValue === "string" ? mutation.previousValue : input.before ?? void 0;
|
|
437
|
+
return { ok: true, diff, absolutePath, previousValue };
|
|
438
|
+
}
|
|
439
|
+
function isValidApplyInput(x) {
|
|
440
|
+
if (!x || typeof x !== "object") return false;
|
|
441
|
+
const o = x;
|
|
442
|
+
return typeof o.file === "string" && typeof o.line === "number" && typeof o.col === "number" && (typeof o.before === "string" || o.before === null) && typeof o.after === "string" && (o.attribute === void 0 || typeof o.attribute === "string") && Number.isInteger(o.line) && Number.isInteger(o.col) && o.line > 0 && o.col >= 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/fs/revertToFile.ts
|
|
446
|
+
async function revertToFile(input, options, recent) {
|
|
447
|
+
const partial = input.file !== void 0 || input.line !== void 0 || input.col !== void 0;
|
|
448
|
+
const complete = typeof input.file === "string" && typeof input.line === "number" && typeof input.col === "number" && Number.isInteger(input.line) && Number.isInteger(input.col);
|
|
449
|
+
if (partial && !complete) {
|
|
450
|
+
return {
|
|
451
|
+
ok: false,
|
|
452
|
+
status: 400,
|
|
453
|
+
reason: "invalid-input",
|
|
454
|
+
details: "Either pass all of {file, line, col} or omit them to revert the most-recent apply."
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const key = complete && typeof input.file === "string" && typeof input.line === "number" && typeof input.col === "number" ? { file: input.file, line: input.line, col: input.col } : void 0;
|
|
458
|
+
const entry = recent.find(key);
|
|
459
|
+
if (!entry) {
|
|
460
|
+
return {
|
|
461
|
+
ok: false,
|
|
462
|
+
status: 404,
|
|
463
|
+
reason: "no-recent-apply",
|
|
464
|
+
details: key ? `No recent apply found at ${key.file}:${key.line}:${key.col}` : "No recent applies to revert"
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const outcome = await applyToFile(
|
|
468
|
+
{
|
|
469
|
+
file: entry.file,
|
|
470
|
+
line: entry.line,
|
|
471
|
+
col: entry.col,
|
|
472
|
+
before: entry.after,
|
|
473
|
+
// swap
|
|
474
|
+
after: entry.before
|
|
475
|
+
},
|
|
476
|
+
options
|
|
477
|
+
);
|
|
478
|
+
if (outcome.ok) {
|
|
479
|
+
recent.remove(entry);
|
|
480
|
+
}
|
|
481
|
+
return outcome;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/fs/applyCssProperty.ts
|
|
485
|
+
import * as fs2 from "fs/promises";
|
|
486
|
+
import * as path2 from "path";
|
|
487
|
+
import { createPatch as createPatch2 } from "diff";
|
|
488
|
+
|
|
489
|
+
// src/css/cssModule.ts
|
|
490
|
+
import * as recast2 from "recast";
|
|
491
|
+
import babelTsParser2 from "recast/parsers/babel-ts.js";
|
|
492
|
+
import postcss from "postcss";
|
|
493
|
+
function detectCssModule(jsxSource, line, col) {
|
|
494
|
+
let ast;
|
|
495
|
+
try {
|
|
496
|
+
ast = recast2.parse(jsxSource, { parser: babelTsParser2 });
|
|
497
|
+
} catch (err) {
|
|
498
|
+
return {
|
|
499
|
+
ok: false,
|
|
500
|
+
reason: "parse-error",
|
|
501
|
+
details: err.message
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
let found = { objectName: null, propertyName: null, visited: false };
|
|
505
|
+
recast2.visit(ast, {
|
|
506
|
+
visitJSXOpeningElement(path4) {
|
|
507
|
+
const node = path4.node;
|
|
508
|
+
const loc = node.loc;
|
|
509
|
+
if (!loc) {
|
|
510
|
+
this.traverse(path4);
|
|
511
|
+
return void 0;
|
|
512
|
+
}
|
|
513
|
+
if (loc.start.line !== line || loc.start.column !== col) {
|
|
514
|
+
this.traverse(path4);
|
|
515
|
+
return void 0;
|
|
516
|
+
}
|
|
517
|
+
found.visited = true;
|
|
518
|
+
const attrs = node.attributes ?? [];
|
|
519
|
+
const classNameAttr = attrs.find(
|
|
520
|
+
(a) => a.type === "JSXAttribute" && a.name?.type === "JSXIdentifier" && a.name?.name === "className"
|
|
521
|
+
);
|
|
522
|
+
if (!classNameAttr) return false;
|
|
523
|
+
const v = classNameAttr.value;
|
|
524
|
+
if (!v || v.type !== "JSXExpressionContainer") return false;
|
|
525
|
+
const expr = v.expression;
|
|
526
|
+
if (!expr) return false;
|
|
527
|
+
if (expr.type === "MemberExpression" && expr.computed === false && expr.object?.type === "Identifier" && expr.property?.type === "Identifier") {
|
|
528
|
+
found.objectName = expr.object.name ?? null;
|
|
529
|
+
found.propertyName = expr.property.name ?? null;
|
|
530
|
+
}
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
if (!found.visited) {
|
|
535
|
+
return {
|
|
536
|
+
ok: false,
|
|
537
|
+
reason: "no-jsx-at-location",
|
|
538
|
+
details: `No JSXOpeningElement found at ${line}:${col}`
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
if (!found.objectName) {
|
|
542
|
+
return {
|
|
543
|
+
ok: false,
|
|
544
|
+
reason: "dynamic-classname",
|
|
545
|
+
details: "className isn't a `{identifier.property}` member expression. B2 only handles direct `{styles.foo}` access."
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
const importHolder = { value: null };
|
|
549
|
+
recast2.visit(ast, {
|
|
550
|
+
visitImportDeclaration(path4) {
|
|
551
|
+
const node = path4.node;
|
|
552
|
+
const defaultSpec = node.specifiers.find(
|
|
553
|
+
(s) => s.type === "ImportDefaultSpecifier" && s.local?.name === found.objectName
|
|
554
|
+
);
|
|
555
|
+
if (defaultSpec) {
|
|
556
|
+
importHolder.value = node.source.value;
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
this.traverse(path4);
|
|
560
|
+
return void 0;
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
if (!importHolder.value) {
|
|
564
|
+
return {
|
|
565
|
+
ok: false,
|
|
566
|
+
reason: "unresolved-import",
|
|
567
|
+
details: `No default import found for \`${found.objectName}\``
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const importSource = importHolder.value;
|
|
571
|
+
if (!importSource.endsWith(".module.css")) {
|
|
572
|
+
return {
|
|
573
|
+
ok: false,
|
|
574
|
+
reason: "not-a-css-module",
|
|
575
|
+
details: `Import \`${importSource}\` is not a .module.css file`
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
ok: true,
|
|
580
|
+
ref: {
|
|
581
|
+
cssFile: importSource,
|
|
582
|
+
selector: `.${found.propertyName}`
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function mutateCssProperty(cssSource, selector, property, value) {
|
|
587
|
+
if (!/^[a-zA-Z-]+$/.test(property)) {
|
|
588
|
+
return {
|
|
589
|
+
ok: false,
|
|
590
|
+
reason: "invalid-property",
|
|
591
|
+
details: `Property name "${property}" must be alphabetic + hyphens only`
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
let root;
|
|
595
|
+
try {
|
|
596
|
+
root = postcss.parse(cssSource);
|
|
597
|
+
} catch (err) {
|
|
598
|
+
return {
|
|
599
|
+
ok: false,
|
|
600
|
+
reason: "css-parse-error",
|
|
601
|
+
details: err.message
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
let targetRule = null;
|
|
605
|
+
root.walkRules((rule) => {
|
|
606
|
+
if (rule.selector === selector) {
|
|
607
|
+
targetRule = rule;
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
if (!targetRule) {
|
|
612
|
+
return {
|
|
613
|
+
ok: false,
|
|
614
|
+
reason: "selector-not-found",
|
|
615
|
+
details: `No rule matching selector \`${selector}\` in CSS source`
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
let hasComposes = false;
|
|
619
|
+
targetRule.walkDecls("composes", () => {
|
|
620
|
+
hasComposes = true;
|
|
621
|
+
});
|
|
622
|
+
if (hasComposes) {
|
|
623
|
+
return {
|
|
624
|
+
ok: false,
|
|
625
|
+
reason: "composes-chain",
|
|
626
|
+
details: `Rule \`${selector}\` uses \`composes:\`. v0.2 refuses to mutate composes chains because the property change could leak through.`
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
let previousValue = null;
|
|
630
|
+
let updated = false;
|
|
631
|
+
targetRule.walkDecls(property, (decl) => {
|
|
632
|
+
if (decl.parent !== targetRule) return;
|
|
633
|
+
if (!updated) {
|
|
634
|
+
previousValue = decl.value;
|
|
635
|
+
decl.value = value;
|
|
636
|
+
updated = true;
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
if (!updated) {
|
|
640
|
+
targetRule.append({ prop: property, value });
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
ok: true,
|
|
644
|
+
output: root.toString(),
|
|
645
|
+
previousValue
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/fs/applyCssProperty.ts
|
|
650
|
+
async function applyCssProperty(input, options) {
|
|
651
|
+
if (!isValid(input)) {
|
|
652
|
+
return {
|
|
653
|
+
ok: false,
|
|
654
|
+
status: 400,
|
|
655
|
+
reason: "invalid-input",
|
|
656
|
+
details: "Body must be { file: string, line: number, col: number, property: string, value: string }"
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
const jsxAbs = resolveWithinWorkspace(options.workspaceRoot, input.file);
|
|
660
|
+
if (!jsxAbs) {
|
|
661
|
+
return {
|
|
662
|
+
ok: false,
|
|
663
|
+
status: 403,
|
|
664
|
+
reason: "path-outside-workspace",
|
|
665
|
+
details: `JSX file outside workspace: ${input.file}`
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
let jsxSource;
|
|
669
|
+
try {
|
|
670
|
+
jsxSource = await fs2.readFile(jsxAbs, "utf8");
|
|
671
|
+
} catch (err) {
|
|
672
|
+
const e = err;
|
|
673
|
+
if (e.code === "ENOENT") {
|
|
674
|
+
return {
|
|
675
|
+
ok: false,
|
|
676
|
+
status: 404,
|
|
677
|
+
reason: "jsx-file-not-found",
|
|
678
|
+
details: `JSX file not found: ${input.file}`
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
ok: false,
|
|
683
|
+
status: 500,
|
|
684
|
+
reason: "read-failed",
|
|
685
|
+
details: `Read JSX failed: ${e.message}`
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
const detect = detectCssModule(jsxSource, input.line, input.col);
|
|
689
|
+
if (!detect.ok) {
|
|
690
|
+
return {
|
|
691
|
+
ok: false,
|
|
692
|
+
status: 400,
|
|
693
|
+
reason: detect.reason,
|
|
694
|
+
details: detect.details
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const cssAbsolutePath = path2.resolve(
|
|
698
|
+
path2.dirname(jsxAbs),
|
|
699
|
+
detect.ref.cssFile
|
|
700
|
+
);
|
|
701
|
+
const cssRel = path2.relative(options.workspaceRoot, cssAbsolutePath);
|
|
702
|
+
if (cssRel.startsWith("..") || path2.isAbsolute(cssRel)) {
|
|
703
|
+
return {
|
|
704
|
+
ok: false,
|
|
705
|
+
status: 403,
|
|
706
|
+
reason: "path-outside-workspace",
|
|
707
|
+
details: `Resolved CSS file outside workspace: ${cssAbsolutePath}`
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
let cssSource;
|
|
711
|
+
try {
|
|
712
|
+
cssSource = await fs2.readFile(cssAbsolutePath, "utf8");
|
|
713
|
+
} catch (err) {
|
|
714
|
+
const e = err;
|
|
715
|
+
if (e.code === "ENOENT") {
|
|
716
|
+
return {
|
|
717
|
+
ok: false,
|
|
718
|
+
status: 404,
|
|
719
|
+
reason: "css-file-not-found",
|
|
720
|
+
details: `CSS file not found: ${cssAbsolutePath}`
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
ok: false,
|
|
725
|
+
status: 500,
|
|
726
|
+
reason: "read-failed",
|
|
727
|
+
details: `Read CSS failed: ${e.message}`
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const mutation = mutateCssProperty(
|
|
731
|
+
cssSource,
|
|
732
|
+
detect.ref.selector,
|
|
733
|
+
input.property,
|
|
734
|
+
input.value
|
|
735
|
+
);
|
|
736
|
+
if (!mutation.ok) {
|
|
737
|
+
return {
|
|
738
|
+
ok: false,
|
|
739
|
+
status: 400,
|
|
740
|
+
reason: mutation.reason,
|
|
741
|
+
details: mutation.details
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
const diff = createPatch2(cssRel, cssSource, mutation.output, "before", "after");
|
|
745
|
+
if (!options.dryRun) {
|
|
746
|
+
try {
|
|
747
|
+
await fs2.writeFile(cssAbsolutePath, mutation.output, "utf8");
|
|
748
|
+
} catch (err) {
|
|
749
|
+
const e = err;
|
|
750
|
+
return {
|
|
751
|
+
ok: false,
|
|
752
|
+
status: 500,
|
|
753
|
+
reason: "write-failed",
|
|
754
|
+
details: `Write CSS failed: ${e.message}`
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
ok: true,
|
|
760
|
+
diff,
|
|
761
|
+
cssAbsolutePath,
|
|
762
|
+
selector: detect.ref.selector,
|
|
763
|
+
previousValue: mutation.previousValue
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function isValid(x) {
|
|
767
|
+
if (!x || typeof x !== "object") return false;
|
|
768
|
+
const o = x;
|
|
769
|
+
return typeof o.file === "string" && typeof o.line === "number" && typeof o.col === "number" && typeof o.property === "string" && typeof o.value === "string" && Number.isInteger(o.line) && Number.isInteger(o.col) && o.line > 0 && o.col >= 0;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// src/fs/applyStyledProperty.ts
|
|
773
|
+
import * as fs3 from "fs/promises";
|
|
774
|
+
import { createPatch as createPatch3 } from "diff";
|
|
775
|
+
|
|
776
|
+
// src/css/styledComponents.ts
|
|
777
|
+
import * as recast3 from "recast";
|
|
778
|
+
import babelTsParser3 from "recast/parsers/babel-ts.js";
|
|
779
|
+
import postcss2 from "postcss";
|
|
780
|
+
function detectStyledComponent(jsxSource, line, col) {
|
|
781
|
+
let ast;
|
|
782
|
+
try {
|
|
783
|
+
ast = recast3.parse(jsxSource, { parser: babelTsParser3 });
|
|
784
|
+
} catch (err) {
|
|
785
|
+
return {
|
|
786
|
+
ok: false,
|
|
787
|
+
reason: "parse-error",
|
|
788
|
+
details: err.message
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
const jsxFound = {
|
|
792
|
+
tagName: null,
|
|
793
|
+
visited: false
|
|
794
|
+
};
|
|
795
|
+
recast3.visit(ast, {
|
|
796
|
+
visitJSXOpeningElement(path4) {
|
|
797
|
+
const node = path4.node;
|
|
798
|
+
const loc = node.loc;
|
|
799
|
+
if (!loc) {
|
|
800
|
+
this.traverse(path4);
|
|
801
|
+
return void 0;
|
|
802
|
+
}
|
|
803
|
+
if (loc.start.line !== line || loc.start.column !== col) {
|
|
804
|
+
this.traverse(path4);
|
|
805
|
+
return void 0;
|
|
806
|
+
}
|
|
807
|
+
jsxFound.visited = true;
|
|
808
|
+
const name = node.name;
|
|
809
|
+
if (name.type === "JSXIdentifier" && name.name) {
|
|
810
|
+
jsxFound.tagName = name.name;
|
|
811
|
+
}
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
if (!jsxFound.visited) {
|
|
816
|
+
return {
|
|
817
|
+
ok: false,
|
|
818
|
+
reason: "no-jsx-at-location",
|
|
819
|
+
details: `No JSXOpeningElement found at ${line}:${col}`
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
if (!jsxFound.tagName || /^[a-z]/.test(jsxFound.tagName)) {
|
|
823
|
+
return {
|
|
824
|
+
ok: false,
|
|
825
|
+
reason: "not-a-styled-component",
|
|
826
|
+
details: `JSX tag is not a component identifier (got "${jsxFound.tagName ?? "<none>"}")`
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
let foundRef = null;
|
|
830
|
+
let refusedReason = null;
|
|
831
|
+
let refusedDetails = "";
|
|
832
|
+
const program = ast.program;
|
|
833
|
+
for (const stmt of program.body) {
|
|
834
|
+
if (stmt.type !== "VariableDeclaration") continue;
|
|
835
|
+
for (const decl of stmt.declarations ?? []) {
|
|
836
|
+
if (decl.type !== "VariableDeclarator") continue;
|
|
837
|
+
if (decl.id?.type !== "Identifier") continue;
|
|
838
|
+
if (decl.id.name !== jsxFound.tagName) continue;
|
|
839
|
+
const init = decl.init;
|
|
840
|
+
if (!init) continue;
|
|
841
|
+
if (init.type !== "TaggedTemplateExpression") {
|
|
842
|
+
refusedReason = "not-a-styled-component";
|
|
843
|
+
refusedDetails = `\`${jsxFound.tagName}\` is declared but isn't a tagged template`;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const tag = init.tag;
|
|
847
|
+
if (!tag) continue;
|
|
848
|
+
if (tag.type === "MemberExpression" && tag.computed === false && tag.object?.type === "Identifier" && tag.object.name === "styled" && tag.property?.type === "Identifier") {
|
|
849
|
+
if ((init.quasi?.expressions?.length ?? 0) > 0) {
|
|
850
|
+
refusedReason = "styled-with-interpolation";
|
|
851
|
+
refusedDetails = `\`${jsxFound.tagName}\` template has \${\u2026} interpolations \u2014 v0.2 only mutates fully-static templates`;
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
foundRef = {
|
|
855
|
+
componentName: jsxFound.tagName,
|
|
856
|
+
htmlTag: tag.property.name ?? "div"
|
|
857
|
+
};
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
if (tag.type === "CallExpression") {
|
|
861
|
+
const callee = tag;
|
|
862
|
+
const c = callee.callee;
|
|
863
|
+
if (c?.type === "Identifier" && c.name === "styled") {
|
|
864
|
+
refusedReason = "styled-extension-not-supported";
|
|
865
|
+
refusedDetails = `\`styled(...)\` extension form \u2014 v0.2 only handles \`styled.tag\``;
|
|
866
|
+
} else if (c?.type === "MemberExpression" && c.property?.name === "attrs") {
|
|
867
|
+
refusedReason = "styled-attrs-not-supported";
|
|
868
|
+
refusedDetails = `\`.attrs(...)\` chain not supported in v0.2`;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
if (foundRef) break;
|
|
873
|
+
}
|
|
874
|
+
if (foundRef) return { ok: true, ref: foundRef };
|
|
875
|
+
if (refusedReason) {
|
|
876
|
+
return {
|
|
877
|
+
ok: false,
|
|
878
|
+
reason: refusedReason,
|
|
879
|
+
details: refusedDetails
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
return {
|
|
883
|
+
ok: false,
|
|
884
|
+
reason: "cross-file-styled-not-supported",
|
|
885
|
+
details: `\`${jsxFound.tagName}\` isn't declared in this file. v0.2 only resolves same-file styled definitions.`
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function mutateStyledProperty(jsxSource, componentName, property, value) {
|
|
889
|
+
if (!/^[a-zA-Z-]+$/.test(property)) {
|
|
890
|
+
return {
|
|
891
|
+
ok: false,
|
|
892
|
+
reason: "invalid-property",
|
|
893
|
+
details: `Property name "${property}" must be alphabetic + hyphens only`
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
let ast;
|
|
897
|
+
try {
|
|
898
|
+
ast = recast3.parse(jsxSource, { parser: babelTsParser3 });
|
|
899
|
+
} catch (err) {
|
|
900
|
+
return {
|
|
901
|
+
ok: false,
|
|
902
|
+
reason: "parse-error",
|
|
903
|
+
details: err.message
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
const out = {
|
|
907
|
+
found: false,
|
|
908
|
+
interpolated: false,
|
|
909
|
+
previousValue: null,
|
|
910
|
+
error: null
|
|
911
|
+
};
|
|
912
|
+
recast3.visit(ast, {
|
|
913
|
+
visitVariableDeclarator(path4) {
|
|
914
|
+
const node = path4.node;
|
|
915
|
+
if (out.found || out.error) return false;
|
|
916
|
+
if (node.id?.type !== "Identifier" || node.id.name !== componentName) {
|
|
917
|
+
this.traverse(path4);
|
|
918
|
+
return void 0;
|
|
919
|
+
}
|
|
920
|
+
const init = node.init;
|
|
921
|
+
if (!init || init.type !== "TaggedTemplateExpression") return false;
|
|
922
|
+
const tag = init.tag;
|
|
923
|
+
if (!tag || tag.type !== "MemberExpression" || tag.object?.type !== "Identifier" || tag.object.name !== "styled") {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
const quasi = init.quasi;
|
|
927
|
+
if (!quasi) return false;
|
|
928
|
+
if ((quasi.expressions?.length ?? 0) > 0) {
|
|
929
|
+
out.interpolated = true;
|
|
930
|
+
out.error = {
|
|
931
|
+
reason: "styled-with-interpolation",
|
|
932
|
+
details: `\`${componentName}\` template has interpolations \u2014 v0.2 only mutates fully-static templates`
|
|
933
|
+
};
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
const onlyQuasi = quasi.quasis[0];
|
|
937
|
+
if (!onlyQuasi) return false;
|
|
938
|
+
const css = onlyQuasi.value.cooked ?? onlyQuasi.value.raw ?? "";
|
|
939
|
+
const wrapped = `:root {${css}}`;
|
|
940
|
+
let root;
|
|
941
|
+
try {
|
|
942
|
+
root = postcss2.parse(wrapped);
|
|
943
|
+
} catch (err) {
|
|
944
|
+
out.error = {
|
|
945
|
+
reason: "css-parse-error",
|
|
946
|
+
details: err.message
|
|
947
|
+
};
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
const rule = root.first;
|
|
951
|
+
if (!rule) {
|
|
952
|
+
out.error = {
|
|
953
|
+
reason: "css-parse-error",
|
|
954
|
+
details: "Internal: synthetic wrap produced no rule"
|
|
955
|
+
};
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
let updated = false;
|
|
959
|
+
rule.walkDecls(property, (decl) => {
|
|
960
|
+
if (decl.parent !== rule) return;
|
|
961
|
+
if (!updated) {
|
|
962
|
+
out.previousValue = decl.value;
|
|
963
|
+
decl.value = value;
|
|
964
|
+
updated = true;
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
if (!updated) {
|
|
968
|
+
rule.append({ prop: property, value });
|
|
969
|
+
}
|
|
970
|
+
const rebuilt = rule.toString();
|
|
971
|
+
const innerMatch = rebuilt.match(/^[^{]*\{([\s\S]*)\}\s*$/);
|
|
972
|
+
const inner = innerMatch && innerMatch[1] !== void 0 ? innerMatch[1] : rebuilt;
|
|
973
|
+
onlyQuasi.value.cooked = inner;
|
|
974
|
+
onlyQuasi.value.raw = inner;
|
|
975
|
+
out.found = true;
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
if (out.error) {
|
|
980
|
+
return { ok: false, ...out.error };
|
|
981
|
+
}
|
|
982
|
+
if (!out.found) {
|
|
983
|
+
return {
|
|
984
|
+
ok: false,
|
|
985
|
+
reason: "component-not-found",
|
|
986
|
+
details: `No \`const ${componentName} = styled.X\`\u2026\`\` definition found`
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
return {
|
|
990
|
+
ok: true,
|
|
991
|
+
output: recast3.print(ast).code,
|
|
992
|
+
previousValue: out.previousValue
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/fs/applyStyledProperty.ts
|
|
997
|
+
async function applyStyledProperty(input, options) {
|
|
998
|
+
if (!isValid2(input)) {
|
|
999
|
+
return {
|
|
1000
|
+
ok: false,
|
|
1001
|
+
status: 400,
|
|
1002
|
+
reason: "invalid-input",
|
|
1003
|
+
details: "Body must be { file: string, line: number, col: number, property: string, value: string }"
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
const absPath = resolveWithinWorkspace(options.workspaceRoot, input.file);
|
|
1007
|
+
if (!absPath) {
|
|
1008
|
+
return {
|
|
1009
|
+
ok: false,
|
|
1010
|
+
status: 403,
|
|
1011
|
+
reason: "path-outside-workspace",
|
|
1012
|
+
details: `File outside workspace: ${input.file}`
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
let source;
|
|
1016
|
+
try {
|
|
1017
|
+
source = await fs3.readFile(absPath, "utf8");
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
const e = err;
|
|
1020
|
+
if (e.code === "ENOENT") {
|
|
1021
|
+
return {
|
|
1022
|
+
ok: false,
|
|
1023
|
+
status: 404,
|
|
1024
|
+
reason: "file-not-found",
|
|
1025
|
+
details: `File not found: ${input.file}`
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
return {
|
|
1029
|
+
ok: false,
|
|
1030
|
+
status: 500,
|
|
1031
|
+
reason: "read-failed",
|
|
1032
|
+
details: `Read failed: ${e.message}`
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
const detect = detectStyledComponent(source, input.line, input.col);
|
|
1036
|
+
if (!detect.ok) {
|
|
1037
|
+
return {
|
|
1038
|
+
ok: false,
|
|
1039
|
+
status: 400,
|
|
1040
|
+
reason: detect.reason,
|
|
1041
|
+
details: detect.details
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
const mutation = mutateStyledProperty(
|
|
1045
|
+
source,
|
|
1046
|
+
detect.ref.componentName,
|
|
1047
|
+
input.property,
|
|
1048
|
+
input.value
|
|
1049
|
+
);
|
|
1050
|
+
if (!mutation.ok) {
|
|
1051
|
+
return {
|
|
1052
|
+
ok: false,
|
|
1053
|
+
status: 400,
|
|
1054
|
+
reason: mutation.reason,
|
|
1055
|
+
details: mutation.details
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
const diff = createPatch3(input.file, source, mutation.output, "before", "after");
|
|
1059
|
+
if (!options.dryRun) {
|
|
1060
|
+
try {
|
|
1061
|
+
await fs3.writeFile(absPath, mutation.output, "utf8");
|
|
1062
|
+
} catch (err) {
|
|
1063
|
+
const e = err;
|
|
1064
|
+
return {
|
|
1065
|
+
ok: false,
|
|
1066
|
+
status: 500,
|
|
1067
|
+
reason: "write-failed",
|
|
1068
|
+
details: `Write failed: ${e.message}`
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
ok: true,
|
|
1074
|
+
diff,
|
|
1075
|
+
componentName: detect.ref.componentName,
|
|
1076
|
+
previousValue: mutation.previousValue
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function isValid2(x) {
|
|
1080
|
+
if (!x || typeof x !== "object") return false;
|
|
1081
|
+
const o = x;
|
|
1082
|
+
return typeof o.file === "string" && typeof o.line === "number" && typeof o.col === "number" && typeof o.property === "string" && typeof o.value === "string" && Number.isInteger(o.line) && Number.isInteger(o.col) && o.line > 0 && o.col >= 0;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/state/recentApplies.ts
|
|
1086
|
+
import * as fs4 from "fs/promises";
|
|
1087
|
+
var RecentApplies = class {
|
|
1088
|
+
constructor(maxSize = 50) {
|
|
1089
|
+
this.maxSize = maxSize;
|
|
1090
|
+
}
|
|
1091
|
+
maxSize;
|
|
1092
|
+
buffer = [];
|
|
1093
|
+
filePath = null;
|
|
1094
|
+
/**
|
|
1095
|
+
* Wire up disk persistence. If the file exists, its contents become the
|
|
1096
|
+
* initial buffer. If it doesn't, we remember the path for future writes.
|
|
1097
|
+
*/
|
|
1098
|
+
async load(filePath) {
|
|
1099
|
+
this.filePath = filePath;
|
|
1100
|
+
try {
|
|
1101
|
+
const raw = await fs4.readFile(filePath, "utf8");
|
|
1102
|
+
const parsed = JSON.parse(raw);
|
|
1103
|
+
if (Array.isArray(parsed.applies)) {
|
|
1104
|
+
this.buffer = parsed.applies.filter(isApply).slice(-this.maxSize);
|
|
1105
|
+
}
|
|
1106
|
+
} catch {
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
push(apply) {
|
|
1110
|
+
this.buffer.push(apply);
|
|
1111
|
+
if (this.buffer.length > this.maxSize) this.buffer.shift();
|
|
1112
|
+
void this.persistNow();
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Find the most-recent entry matching `key`. If no key is provided, returns
|
|
1116
|
+
* the most-recent entry overall (single-step undo). Returns `null` when no
|
|
1117
|
+
* matching entry is in the buffer.
|
|
1118
|
+
*/
|
|
1119
|
+
find(key) {
|
|
1120
|
+
if (this.buffer.length === 0) return null;
|
|
1121
|
+
if (!key) return this.buffer[this.buffer.length - 1] ?? null;
|
|
1122
|
+
for (let i = this.buffer.length - 1; i >= 0; i--) {
|
|
1123
|
+
const a = this.buffer[i];
|
|
1124
|
+
if (a && a.file === key.file && a.line === key.line && a.col === key.col) {
|
|
1125
|
+
return a;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
remove(apply) {
|
|
1131
|
+
const idx = this.buffer.lastIndexOf(apply);
|
|
1132
|
+
if (idx !== -1) this.buffer.splice(idx, 1);
|
|
1133
|
+
void this.persistNow();
|
|
1134
|
+
}
|
|
1135
|
+
list() {
|
|
1136
|
+
return this.buffer;
|
|
1137
|
+
}
|
|
1138
|
+
clear() {
|
|
1139
|
+
this.buffer = [];
|
|
1140
|
+
void this.persistNow();
|
|
1141
|
+
}
|
|
1142
|
+
get size() {
|
|
1143
|
+
return this.buffer.length;
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* Write the buffer to disk. No-op when no path has been configured.
|
|
1147
|
+
* Safe to `void`-call for fire-and-forget; tests can `await` it for
|
|
1148
|
+
* determinism.
|
|
1149
|
+
*/
|
|
1150
|
+
async persistNow() {
|
|
1151
|
+
if (!this.filePath) return;
|
|
1152
|
+
const json = JSON.stringify(
|
|
1153
|
+
{ applies: this.buffer, savedAt: Date.now() },
|
|
1154
|
+
null,
|
|
1155
|
+
2
|
|
1156
|
+
);
|
|
1157
|
+
try {
|
|
1158
|
+
await fs4.writeFile(this.filePath, json, "utf8");
|
|
1159
|
+
} catch {
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
function isApply(x) {
|
|
1164
|
+
if (!x || typeof x !== "object") return false;
|
|
1165
|
+
const o = x;
|
|
1166
|
+
return typeof o.file === "string" && typeof o.line === "number" && typeof o.col === "number" && typeof o.before === "string" && typeof o.after === "string" && typeof o.appliedAt === "number";
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/state/selection.ts
|
|
1170
|
+
var CurrentSelection = class {
|
|
1171
|
+
current = null;
|
|
1172
|
+
set(s) {
|
|
1173
|
+
this.current = s;
|
|
1174
|
+
}
|
|
1175
|
+
get() {
|
|
1176
|
+
return this.current;
|
|
1177
|
+
}
|
|
1178
|
+
clear() {
|
|
1179
|
+
this.current = null;
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
// src/state/auth.ts
|
|
1184
|
+
import { randomBytes } from "crypto";
|
|
1185
|
+
import * as fs5 from "fs/promises";
|
|
1186
|
+
import * as path3 from "path";
|
|
1187
|
+
var SESSION_DIRNAME = ".visual-editor";
|
|
1188
|
+
var SESSION_FILENAME = "session.json";
|
|
1189
|
+
var SessionToken = class {
|
|
1190
|
+
token = null;
|
|
1191
|
+
filePath = null;
|
|
1192
|
+
async load(workspaceRoot) {
|
|
1193
|
+
const dir = path3.join(workspaceRoot, SESSION_DIRNAME);
|
|
1194
|
+
this.filePath = path3.join(dir, SESSION_FILENAME);
|
|
1195
|
+
try {
|
|
1196
|
+
const existing = JSON.parse(
|
|
1197
|
+
await fs5.readFile(this.filePath, "utf8")
|
|
1198
|
+
);
|
|
1199
|
+
if (typeof existing.token === "string" && existing.token.length >= 16) {
|
|
1200
|
+
this.token = existing.token;
|
|
1201
|
+
return this.token;
|
|
1202
|
+
}
|
|
1203
|
+
} catch {
|
|
1204
|
+
}
|
|
1205
|
+
await fs5.mkdir(dir, { recursive: true });
|
|
1206
|
+
this.token = randomBytes(24).toString("hex");
|
|
1207
|
+
await fs5.writeFile(
|
|
1208
|
+
this.filePath,
|
|
1209
|
+
JSON.stringify({ token: this.token, createdAt: Date.now() }, null, 2),
|
|
1210
|
+
{ encoding: "utf8", mode: 384 }
|
|
1211
|
+
);
|
|
1212
|
+
return this.token;
|
|
1213
|
+
}
|
|
1214
|
+
/** Construct the token in-process without touching disk. Used by tests. */
|
|
1215
|
+
setInMemory(token) {
|
|
1216
|
+
this.token = token;
|
|
1217
|
+
}
|
|
1218
|
+
get() {
|
|
1219
|
+
if (!this.token) {
|
|
1220
|
+
throw new Error("SessionToken not loaded \u2014 call load() first");
|
|
1221
|
+
}
|
|
1222
|
+
return this.token;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Constant-time compare against the bearer string the client sent. Returns
|
|
1226
|
+
* `true` when the lengths match AND every byte is equal. Plain `===` would
|
|
1227
|
+
* leak length information through timing.
|
|
1228
|
+
*/
|
|
1229
|
+
matches(received) {
|
|
1230
|
+
if (!this.token || !received) return false;
|
|
1231
|
+
if (received.length !== this.token.length) return false;
|
|
1232
|
+
let diff = 0;
|
|
1233
|
+
for (let i = 0; i < received.length; i++) {
|
|
1234
|
+
diff |= received.charCodeAt(i) ^ this.token.charCodeAt(i);
|
|
1235
|
+
}
|
|
1236
|
+
return diff === 0;
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
function parseBearer(header) {
|
|
1240
|
+
if (!header) return null;
|
|
1241
|
+
const m = /^Bearer\s+(.+)$/i.exec(header);
|
|
1242
|
+
return m ? m[1].trim() : null;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/http/server.ts
|
|
1246
|
+
import * as http from "http";
|
|
1247
|
+
function createServer2(options) {
|
|
1248
|
+
const ctx = {
|
|
1249
|
+
options,
|
|
1250
|
+
recentApplies: options.recentApplies ?? new RecentApplies(),
|
|
1251
|
+
currentSelection: options.currentSelection ?? new CurrentSelection(),
|
|
1252
|
+
sessionToken: options.sessionToken ?? new SessionToken()
|
|
1253
|
+
};
|
|
1254
|
+
return http.createServer((req, res) => {
|
|
1255
|
+
void handle(req, res, ctx).catch((err) => {
|
|
1256
|
+
writeJson(res, 500, {
|
|
1257
|
+
ok: false,
|
|
1258
|
+
reason: "internal-error",
|
|
1259
|
+
details: err.message
|
|
1260
|
+
});
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
async function handle(req, res, ctx) {
|
|
1265
|
+
const origin = req.headers.origin;
|
|
1266
|
+
const allowed = ctx.options.allowedOrigins ?? [];
|
|
1267
|
+
const originAllowed = allowed.length === 0 || origin && allowed.includes(origin);
|
|
1268
|
+
if (allowed.length === 0) {
|
|
1269
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1270
|
+
} else if (originAllowed && origin) {
|
|
1271
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
1272
|
+
res.setHeader("Vary", "Origin");
|
|
1273
|
+
}
|
|
1274
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
1275
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
1276
|
+
if (req.method === "OPTIONS") {
|
|
1277
|
+
res.writeHead(204);
|
|
1278
|
+
res.end();
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
const url = req.url ?? "/";
|
|
1282
|
+
if (req.method === "GET" && url === "/health") {
|
|
1283
|
+
writeJson(res, 200, { ok: true });
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
if (!originAllowed) {
|
|
1287
|
+
writeJson(res, 403, {
|
|
1288
|
+
ok: false,
|
|
1289
|
+
reason: "origin-not-allowed",
|
|
1290
|
+
details: `Origin ${origin ?? "(missing)"} is not in the allowlist`
|
|
1291
|
+
});
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
if (req.method === "GET" && url === "/token") {
|
|
1295
|
+
writeJson(res, 200, { token: ctx.sessionToken.get() });
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
const bearer = parseBearer(req.headers.authorization);
|
|
1299
|
+
if (!ctx.sessionToken.matches(bearer)) {
|
|
1300
|
+
writeJson(res, 401, {
|
|
1301
|
+
ok: false,
|
|
1302
|
+
reason: "unauthorized",
|
|
1303
|
+
details: "Include `Authorization: Bearer <token>` (fetch the token from GET /token)."
|
|
1304
|
+
});
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (req.method === "GET" && url === "/recent") {
|
|
1308
|
+
writeJson(res, 200, { ok: true, applies: ctx.recentApplies.list() });
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (req.method === "GET" && url === "/assets") {
|
|
1312
|
+
await dispatchAssets(res, ctx);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
if (req.method === "POST" && url === "/apply") {
|
|
1316
|
+
await dispatchMutation(req, res, ctx, false);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (req.method === "POST" && url === "/propose") {
|
|
1320
|
+
await dispatchMutation(req, res, ctx, true);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (req.method === "POST" && url === "/revert") {
|
|
1324
|
+
await dispatchRevert(req, res, ctx);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (req.method === "POST" && url === "/apply-css-prop") {
|
|
1328
|
+
await dispatchCssProperty(req, res, ctx);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (req.method === "POST" && url === "/apply-styled-prop") {
|
|
1332
|
+
await dispatchStyledProperty(req, res, ctx);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
if (req.method === "GET" && url === "/selection") {
|
|
1336
|
+
writeJson(res, 200, {
|
|
1337
|
+
ok: true,
|
|
1338
|
+
selection: ctx.currentSelection.get()
|
|
1339
|
+
});
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
if (req.method === "POST" && url === "/selection") {
|
|
1343
|
+
await dispatchSelection(req, res, ctx);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (req.method === "DELETE" && url === "/selection") {
|
|
1347
|
+
ctx.currentSelection.clear();
|
|
1348
|
+
writeJson(res, 200, { ok: true });
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
writeJson(res, 404, {
|
|
1352
|
+
ok: false,
|
|
1353
|
+
reason: "not-found",
|
|
1354
|
+
details: `No route for ${req.method} ${url}`
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
async function dispatchMutation(req, res, ctx, dryRun) {
|
|
1358
|
+
let body;
|
|
1359
|
+
try {
|
|
1360
|
+
body = await readJsonBody(req);
|
|
1361
|
+
} catch (err) {
|
|
1362
|
+
writeJson(res, 400, {
|
|
1363
|
+
ok: false,
|
|
1364
|
+
reason: "invalid-json",
|
|
1365
|
+
details: err.message
|
|
1366
|
+
});
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const outcome = await applyToFile(body, {
|
|
1370
|
+
workspaceRoot: ctx.options.workspaceRoot,
|
|
1371
|
+
dryRun
|
|
1372
|
+
});
|
|
1373
|
+
if (outcome.ok) {
|
|
1374
|
+
if (!dryRun) {
|
|
1375
|
+
const input = body;
|
|
1376
|
+
const beforeForBuffer = outcome.previousValue ?? input.before ?? "";
|
|
1377
|
+
ctx.recentApplies.push({
|
|
1378
|
+
file: input.file,
|
|
1379
|
+
line: input.line,
|
|
1380
|
+
col: input.col,
|
|
1381
|
+
before: beforeForBuffer,
|
|
1382
|
+
after: input.after,
|
|
1383
|
+
appliedAt: Date.now()
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
writeJson(res, 200, { ok: true, diff: outcome.diff });
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
writeJson(res, outcome.status, {
|
|
1390
|
+
ok: false,
|
|
1391
|
+
reason: outcome.reason,
|
|
1392
|
+
details: outcome.details
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
async function dispatchRevert(req, res, ctx) {
|
|
1396
|
+
let body;
|
|
1397
|
+
try {
|
|
1398
|
+
body = await readJsonBody(req);
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
writeJson(res, 400, {
|
|
1401
|
+
ok: false,
|
|
1402
|
+
reason: "invalid-json",
|
|
1403
|
+
details: err.message
|
|
1404
|
+
});
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
const outcome = await revertToFile(
|
|
1408
|
+
body,
|
|
1409
|
+
{ workspaceRoot: ctx.options.workspaceRoot, dryRun: false },
|
|
1410
|
+
ctx.recentApplies
|
|
1411
|
+
);
|
|
1412
|
+
if (outcome.ok) {
|
|
1413
|
+
writeJson(res, 200, { ok: true, diff: outcome.diff });
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
writeJson(res, outcome.status, {
|
|
1417
|
+
ok: false,
|
|
1418
|
+
reason: outcome.reason,
|
|
1419
|
+
details: outcome.details
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
async function dispatchCssProperty(req, res, ctx) {
|
|
1423
|
+
let body;
|
|
1424
|
+
try {
|
|
1425
|
+
body = await readJsonBody(req);
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
writeJson(res, 400, {
|
|
1428
|
+
ok: false,
|
|
1429
|
+
reason: "invalid-json",
|
|
1430
|
+
details: err.message
|
|
1431
|
+
});
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
const outcome = await applyCssProperty(body, {
|
|
1435
|
+
workspaceRoot: ctx.options.workspaceRoot,
|
|
1436
|
+
dryRun: false
|
|
1437
|
+
});
|
|
1438
|
+
if (outcome.ok) {
|
|
1439
|
+
writeJson(res, 200, {
|
|
1440
|
+
ok: true,
|
|
1441
|
+
diff: outcome.diff,
|
|
1442
|
+
selector: outcome.selector,
|
|
1443
|
+
previousValue: outcome.previousValue
|
|
1444
|
+
});
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
writeJson(res, outcome.status, {
|
|
1448
|
+
ok: false,
|
|
1449
|
+
reason: outcome.reason,
|
|
1450
|
+
details: outcome.details
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
async function dispatchStyledProperty(req, res, ctx) {
|
|
1454
|
+
let body;
|
|
1455
|
+
try {
|
|
1456
|
+
body = await readJsonBody(req);
|
|
1457
|
+
} catch (err) {
|
|
1458
|
+
writeJson(res, 400, {
|
|
1459
|
+
ok: false,
|
|
1460
|
+
reason: "invalid-json",
|
|
1461
|
+
details: err.message
|
|
1462
|
+
});
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const outcome = await applyStyledProperty(body, {
|
|
1466
|
+
workspaceRoot: ctx.options.workspaceRoot,
|
|
1467
|
+
dryRun: false
|
|
1468
|
+
});
|
|
1469
|
+
if (outcome.ok) {
|
|
1470
|
+
writeJson(res, 200, {
|
|
1471
|
+
ok: true,
|
|
1472
|
+
diff: outcome.diff,
|
|
1473
|
+
componentName: outcome.componentName,
|
|
1474
|
+
previousValue: outcome.previousValue
|
|
1475
|
+
});
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
writeJson(res, outcome.status, {
|
|
1479
|
+
ok: false,
|
|
1480
|
+
reason: outcome.reason,
|
|
1481
|
+
details: outcome.details
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
async function dispatchAssets(res, ctx) {
|
|
1485
|
+
const fs6 = await import("fs/promises");
|
|
1486
|
+
const path4 = await import("path");
|
|
1487
|
+
const IMAGE_EXTS = /* @__PURE__ */ new Set([
|
|
1488
|
+
".png",
|
|
1489
|
+
".jpg",
|
|
1490
|
+
".jpeg",
|
|
1491
|
+
".gif",
|
|
1492
|
+
".svg",
|
|
1493
|
+
".webp",
|
|
1494
|
+
".avif"
|
|
1495
|
+
]);
|
|
1496
|
+
const publicDir = path4.join(ctx.options.workspaceRoot, "public");
|
|
1497
|
+
async function walk(dir, rel) {
|
|
1498
|
+
let entries = [];
|
|
1499
|
+
try {
|
|
1500
|
+
entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
1501
|
+
} catch {
|
|
1502
|
+
return [];
|
|
1503
|
+
}
|
|
1504
|
+
const out = [];
|
|
1505
|
+
for (const e of entries) {
|
|
1506
|
+
const child = path4.join(dir, e.name);
|
|
1507
|
+
const childRel = rel ? `${rel}/${e.name}` : e.name;
|
|
1508
|
+
if (e.isDirectory()) {
|
|
1509
|
+
const sub = await walk(child, childRel);
|
|
1510
|
+
out.push(...sub);
|
|
1511
|
+
} else if (e.isFile() && IMAGE_EXTS.has(path4.extname(e.name).toLowerCase())) {
|
|
1512
|
+
out.push(`/${childRel}`);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
return out;
|
|
1516
|
+
}
|
|
1517
|
+
try {
|
|
1518
|
+
const assets = await walk(publicDir, "");
|
|
1519
|
+
assets.sort();
|
|
1520
|
+
writeJson(res, 200, { ok: true, assets });
|
|
1521
|
+
} catch (err) {
|
|
1522
|
+
writeJson(res, 500, {
|
|
1523
|
+
ok: false,
|
|
1524
|
+
reason: "assets-list-failed",
|
|
1525
|
+
details: err.message
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
async function dispatchSelection(req, res, ctx) {
|
|
1530
|
+
let body;
|
|
1531
|
+
try {
|
|
1532
|
+
body = await readJsonBody(req);
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
writeJson(res, 400, {
|
|
1535
|
+
ok: false,
|
|
1536
|
+
reason: "invalid-json",
|
|
1537
|
+
details: err.message
|
|
1538
|
+
});
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
if (!isValidSelection(body)) {
|
|
1542
|
+
writeJson(res, 400, {
|
|
1543
|
+
ok: false,
|
|
1544
|
+
reason: "invalid-input",
|
|
1545
|
+
details: "Body must be { file, line, col, oid, className, tagName, componentName?, instanceCount }"
|
|
1546
|
+
});
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
ctx.currentSelection.set(body);
|
|
1550
|
+
writeJson(res, 200, { ok: true });
|
|
1551
|
+
}
|
|
1552
|
+
function isValidSelection(x) {
|
|
1553
|
+
if (!x || typeof x !== "object") return false;
|
|
1554
|
+
const o = x;
|
|
1555
|
+
return typeof o.file === "string" && typeof o.line === "number" && typeof o.col === "number" && typeof o.oid === "string" && typeof o.className === "string" && typeof o.tagName === "string" && (o.componentName === null || typeof o.componentName === "string") && typeof o.instanceCount === "number";
|
|
1556
|
+
}
|
|
1557
|
+
async function readJsonBody(req) {
|
|
1558
|
+
const chunks = [];
|
|
1559
|
+
for await (const chunk of req) {
|
|
1560
|
+
chunks.push(chunk);
|
|
1561
|
+
if (chunks.reduce((n, c) => n + c.length, 0) > 1024 * 1024) {
|
|
1562
|
+
throw new Error("Request body exceeds 1 MB");
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
1566
|
+
if (raw.length === 0) return {};
|
|
1567
|
+
return JSON.parse(raw);
|
|
1568
|
+
}
|
|
1569
|
+
function writeJson(res, status, payload) {
|
|
1570
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1571
|
+
res.end(JSON.stringify(payload));
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
export {
|
|
1575
|
+
applyToFile,
|
|
1576
|
+
revertToFile,
|
|
1577
|
+
applyCssProperty,
|
|
1578
|
+
applyStyledProperty,
|
|
1579
|
+
RecentApplies,
|
|
1580
|
+
CurrentSelection,
|
|
1581
|
+
SessionToken,
|
|
1582
|
+
createServer2 as createServer
|
|
1583
|
+
};
|
|
1584
|
+
//# sourceMappingURL=chunk-QEI2RGE2.js.map
|