@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.
@@ -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