next-ai-editor 0.1.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/LICENSE +21 -0
- package/README.md +213 -0
- package/dist/AIEditorProvider-Bs9zUVrL.cjs +1722 -0
- package/dist/AIEditorProvider-Bs9zUVrL.cjs.map +1 -0
- package/dist/AIEditorProvider-D-w9-GZb.js +1723 -0
- package/dist/AIEditorProvider-D-w9-GZb.js.map +1 -0
- package/dist/client/AIEditorProvider.d.ts +52 -0
- package/dist/client/AIEditorProvider.d.ts.map +1 -0
- package/dist/client/index.d.ts +3 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client.cjs +6 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.js +6 -0
- package/dist/client.js.map +1 -0
- package/dist/index-BFa7H-uO.js +1145 -0
- package/dist/index-BFa7H-uO.js.map +1 -0
- package/dist/index-DnoYi4f8.cjs +1162 -0
- package/dist/index-DnoYi4f8.cjs.map +1 -0
- package/dist/index.cjs +26 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/server/handlers/absolute-path.d.ts +3 -0
- package/dist/server/handlers/absolute-path.d.ts.map +1 -0
- package/dist/server/handlers/edit.d.ts +3 -0
- package/dist/server/handlers/edit.d.ts.map +1 -0
- package/dist/server/handlers/index.d.ts +18 -0
- package/dist/server/handlers/index.d.ts.map +1 -0
- package/dist/server/handlers/read.d.ts +3 -0
- package/dist/server/handlers/read.d.ts.map +1 -0
- package/dist/server/handlers/resolve.d.ts +3 -0
- package/dist/server/handlers/resolve.d.ts.map +1 -0
- package/dist/server/handlers/undo.d.ts +3 -0
- package/dist/server/handlers/undo.d.ts.map +1 -0
- package/dist/server/handlers/validate-session.d.ts +3 -0
- package/dist/server/handlers/validate-session.d.ts.map +1 -0
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/utils/ast.d.ts +39 -0
- package/dist/server/utils/ast.d.ts.map +1 -0
- package/dist/server/utils/file-system.d.ts +24 -0
- package/dist/server/utils/file-system.d.ts.map +1 -0
- package/dist/server/utils/source-map.d.ts +29 -0
- package/dist/server/utils/source-map.d.ts.map +1 -0
- package/dist/server.cjs +24 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.js +24 -0
- package/dist/server.js.map +1 -0
- package/dist/shared/storage.d.ts +53 -0
- package/dist/shared/storage.d.ts.map +1 -0
- package/dist/shared/types.d.ts +44 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/package.json +87 -0
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import * as parser from "@babel/parser";
|
|
4
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
5
|
+
import { SystemMessage, HumanMessage } from "@langchain/core/messages";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import traverse from "@babel/traverse";
|
|
8
|
+
import * as t from "@babel/types";
|
|
9
|
+
import { SourceMapConsumer } from "@jridgewell/source-map";
|
|
10
|
+
import crypto from "crypto";
|
|
11
|
+
function validateDevMode() {
|
|
12
|
+
if (process.env.NODE_ENV !== "development") {
|
|
13
|
+
return NextResponse.json(
|
|
14
|
+
{ success: false, error: "Development mode only" },
|
|
15
|
+
{ status: 403 }
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function normalizePath(filePath) {
|
|
21
|
+
return filePath.replace(/^webpack-internal:\/\/\/\([^)]+\)\/\.?/, "").replace(/^webpack:\/\/[^/]*\//, "").replace(/^\.\//, "").replace(/\?.*$/, "");
|
|
22
|
+
}
|
|
23
|
+
async function resolveFilePath(projectRoot, normalizedPath) {
|
|
24
|
+
const candidates = [
|
|
25
|
+
path.resolve(projectRoot, normalizedPath),
|
|
26
|
+
path.resolve(projectRoot, "src", normalizedPath),
|
|
27
|
+
path.resolve(projectRoot, "app", normalizedPath)
|
|
28
|
+
];
|
|
29
|
+
for (const candidate of candidates) {
|
|
30
|
+
if (!candidate.startsWith(projectRoot)) continue;
|
|
31
|
+
try {
|
|
32
|
+
await fs.access(candidate);
|
|
33
|
+
return candidate;
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function isPathSecure(absolutePath, projectRoot) {
|
|
40
|
+
return absolutePath.startsWith(projectRoot);
|
|
41
|
+
}
|
|
42
|
+
async function fileExists$1(filePath) {
|
|
43
|
+
try {
|
|
44
|
+
await fs.access(filePath);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function parseFile(content) {
|
|
51
|
+
try {
|
|
52
|
+
return parser.parse(content, {
|
|
53
|
+
sourceType: "module",
|
|
54
|
+
plugins: [
|
|
55
|
+
"jsx",
|
|
56
|
+
"typescript",
|
|
57
|
+
"decorators-legacy",
|
|
58
|
+
"classProperties",
|
|
59
|
+
"optionalChaining",
|
|
60
|
+
"nullishCoalescingOperator"
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error("Parse error:", e);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function findTargetElement(ast, fileContent, options) {
|
|
69
|
+
const { componentName, lineNumber, elementContext } = options;
|
|
70
|
+
const matches = [];
|
|
71
|
+
let componentNode = null;
|
|
72
|
+
let componentStart = 0;
|
|
73
|
+
let componentEnd = Infinity;
|
|
74
|
+
let fallbackExportDefault = null;
|
|
75
|
+
traverse(ast, {
|
|
76
|
+
FunctionDeclaration(path2) {
|
|
77
|
+
var _a, _b, _c;
|
|
78
|
+
if (((_a = path2.node.id) == null ? void 0 : _a.name) === componentName) {
|
|
79
|
+
componentNode = path2.node;
|
|
80
|
+
componentStart = ((_b = path2.node.loc) == null ? void 0 : _b.start.line) || 0;
|
|
81
|
+
componentEnd = ((_c = path2.node.loc) == null ? void 0 : _c.end.line) || Infinity;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
VariableDeclarator(path2) {
|
|
85
|
+
var _a, _b;
|
|
86
|
+
if (t.isIdentifier(path2.node.id) && path2.node.id.name === componentName) {
|
|
87
|
+
componentNode = path2.node;
|
|
88
|
+
const parent = path2.parentPath.parent;
|
|
89
|
+
componentStart = ((_a = parent == null ? void 0 : parent.loc) == null ? void 0 : _a.start.line) || 0;
|
|
90
|
+
componentEnd = ((_b = parent == null ? void 0 : parent.loc) == null ? void 0 : _b.end.line) || Infinity;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
ExportDefaultDeclaration(path2) {
|
|
94
|
+
var _a, _b, _c, _d, _e;
|
|
95
|
+
if (t.isFunctionDeclaration(path2.node.declaration)) {
|
|
96
|
+
const funcName = (_a = path2.node.declaration.id) == null ? void 0 : _a.name;
|
|
97
|
+
if (funcName === componentName || !funcName) {
|
|
98
|
+
componentNode = path2.node;
|
|
99
|
+
componentStart = ((_b = path2.node.loc) == null ? void 0 : _b.start.line) || 0;
|
|
100
|
+
componentEnd = ((_c = path2.node.loc) == null ? void 0 : _c.end.line) || Infinity;
|
|
101
|
+
} else if (!componentNode) {
|
|
102
|
+
fallbackExportDefault = {
|
|
103
|
+
node: path2.node,
|
|
104
|
+
start: ((_d = path2.node.loc) == null ? void 0 : _d.start.line) || 0,
|
|
105
|
+
end: ((_e = path2.node.loc) == null ? void 0 : _e.end.line) || Infinity
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
if (!componentNode && fallbackExportDefault !== null) {
|
|
112
|
+
const fallback = fallbackExportDefault;
|
|
113
|
+
console.log(
|
|
114
|
+
`⚠️ Component "${componentName}" not found, using export default function as fallback`
|
|
115
|
+
);
|
|
116
|
+
componentNode = fallback.node;
|
|
117
|
+
componentStart = fallback.start;
|
|
118
|
+
componentEnd = fallback.end;
|
|
119
|
+
}
|
|
120
|
+
const allElementsByTag = /* @__PURE__ */ new Map();
|
|
121
|
+
traverse(ast, {
|
|
122
|
+
JSXElement(path2) {
|
|
123
|
+
const loc = path2.node.loc;
|
|
124
|
+
if (!loc) return;
|
|
125
|
+
const startLine = loc.start.line;
|
|
126
|
+
const endLine = loc.end.line;
|
|
127
|
+
if (startLine < componentStart || endLine > componentEnd) return;
|
|
128
|
+
const opening = path2.node.openingElement;
|
|
129
|
+
if (t.isJSXIdentifier(opening.name)) {
|
|
130
|
+
const tagName = opening.name.name;
|
|
131
|
+
if (!allElementsByTag.has(tagName)) {
|
|
132
|
+
allElementsByTag.set(tagName, []);
|
|
133
|
+
}
|
|
134
|
+
allElementsByTag.get(tagName).push({ node: path2.node, startLine, endLine, score: 0 });
|
|
135
|
+
}
|
|
136
|
+
if (elementContext) {
|
|
137
|
+
const score = scoreElementMatch(path2.node, elementContext, fileContent);
|
|
138
|
+
if (score > 0) {
|
|
139
|
+
matches.push({ node: path2.node, startLine, endLine, score });
|
|
140
|
+
}
|
|
141
|
+
} else if (Math.abs(startLine - lineNumber) <= 5) {
|
|
142
|
+
matches.push({
|
|
143
|
+
node: path2.node,
|
|
144
|
+
startLine,
|
|
145
|
+
endLine,
|
|
146
|
+
score: 100 - Math.abs(startLine - lineNumber)
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
if (matches.length === 0) {
|
|
152
|
+
if (componentNode && componentStart > 0) {
|
|
153
|
+
return {
|
|
154
|
+
startLine: componentStart,
|
|
155
|
+
endLine: componentEnd,
|
|
156
|
+
componentStart,
|
|
157
|
+
componentEnd
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
if ((elementContext == null ? void 0 : elementContext.nthOfType) && elementContext.tagName) {
|
|
163
|
+
const allOfTag = allElementsByTag.get(elementContext.tagName);
|
|
164
|
+
if (allOfTag && allOfTag.length >= elementContext.nthOfType) {
|
|
165
|
+
const target = allOfTag[elementContext.nthOfType - 1];
|
|
166
|
+
console.log(
|
|
167
|
+
` Using nthOfType=${elementContext.nthOfType}: found ${allOfTag.length} <${elementContext.tagName}> elements`
|
|
168
|
+
);
|
|
169
|
+
return {
|
|
170
|
+
startLine: target.startLine,
|
|
171
|
+
endLine: target.endLine,
|
|
172
|
+
componentStart,
|
|
173
|
+
componentEnd
|
|
174
|
+
};
|
|
175
|
+
} else {
|
|
176
|
+
console.log(
|
|
177
|
+
` nthOfType=${elementContext.nthOfType} but only found ${(allOfTag == null ? void 0 : allOfTag.length) || 0} <${elementContext.tagName}> elements`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
matches.sort((a, b) => b.score - a.score);
|
|
182
|
+
const best = matches[0];
|
|
183
|
+
return {
|
|
184
|
+
startLine: best.startLine,
|
|
185
|
+
endLine: best.endLine,
|
|
186
|
+
componentStart,
|
|
187
|
+
componentEnd
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function scoreElementMatch(node, context, fileContent) {
|
|
191
|
+
let score = 0;
|
|
192
|
+
const opening = node.openingElement;
|
|
193
|
+
if (t.isJSXIdentifier(opening.name)) {
|
|
194
|
+
if (opening.name.name === context.tagName) {
|
|
195
|
+
score += 50;
|
|
196
|
+
} else {
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
} else if (t.isJSXMemberExpression(opening.name)) {
|
|
200
|
+
const fullName = getJSXMemberName(opening.name);
|
|
201
|
+
if (fullName === context.tagName || fullName.endsWith(`.${context.tagName}`)) {
|
|
202
|
+
score += 50;
|
|
203
|
+
} else {
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (context.className) {
|
|
208
|
+
const classAttr = opening.attributes.find(
|
|
209
|
+
(attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === "className"
|
|
210
|
+
);
|
|
211
|
+
if (classAttr && t.isJSXAttribute(classAttr)) {
|
|
212
|
+
const classValue = getAttributeValue(classAttr);
|
|
213
|
+
if (classValue && context.className.split(/\s+/).some((c) => classValue.includes(c))) {
|
|
214
|
+
score += 20;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (context.textContent && node.loc) {
|
|
219
|
+
const elementCode = fileContent.split("\n").slice(node.loc.start.line - 1, node.loc.end.line).join("\n");
|
|
220
|
+
const normalizedContent = context.textContent.toLowerCase().trim();
|
|
221
|
+
const normalizedElement = elementCode.toLowerCase();
|
|
222
|
+
if (normalizedElement.includes(normalizedContent)) {
|
|
223
|
+
score += 30;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (context.props) {
|
|
227
|
+
for (const [key, value] of Object.entries(context.props)) {
|
|
228
|
+
if (key.startsWith("_")) continue;
|
|
229
|
+
const attr = opening.attributes.find(
|
|
230
|
+
(a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === key
|
|
231
|
+
);
|
|
232
|
+
if (attr) {
|
|
233
|
+
score += 5;
|
|
234
|
+
if (typeof value === "string" && t.isJSXAttribute(attr)) {
|
|
235
|
+
const attrValue = getAttributeValue(attr);
|
|
236
|
+
if (attrValue === value) score += 10;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return score;
|
|
242
|
+
}
|
|
243
|
+
function getJSXMemberName(node) {
|
|
244
|
+
if (t.isJSXIdentifier(node.object)) {
|
|
245
|
+
return `${node.object.name}.${node.property.name}`;
|
|
246
|
+
}
|
|
247
|
+
if (t.isJSXMemberExpression(node.object)) {
|
|
248
|
+
return `${getJSXMemberName(node.object)}.${node.property.name}`;
|
|
249
|
+
}
|
|
250
|
+
return node.property.name;
|
|
251
|
+
}
|
|
252
|
+
function getAttributeValue(attr) {
|
|
253
|
+
if (!attr.value) return null;
|
|
254
|
+
if (t.isStringLiteral(attr.value)) return attr.value.value;
|
|
255
|
+
if (t.isJSXExpressionContainer(attr.value) && t.isStringLiteral(attr.value.expression)) {
|
|
256
|
+
return attr.value.expression.value;
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
async function handleEdit(req) {
|
|
261
|
+
var _a;
|
|
262
|
+
const devModeError = validateDevMode();
|
|
263
|
+
if (devModeError) return devModeError;
|
|
264
|
+
try {
|
|
265
|
+
const body = await req.json();
|
|
266
|
+
const {
|
|
267
|
+
filePath,
|
|
268
|
+
lineNumber,
|
|
269
|
+
componentName,
|
|
270
|
+
suggestion,
|
|
271
|
+
elementContext,
|
|
272
|
+
editHistory
|
|
273
|
+
} = body;
|
|
274
|
+
const projectRoot = process.cwd();
|
|
275
|
+
const normalizedPath = normalizePath(filePath);
|
|
276
|
+
const absolutePath = await resolveFilePath(projectRoot, normalizedPath);
|
|
277
|
+
if (!absolutePath) {
|
|
278
|
+
return NextResponse.json(
|
|
279
|
+
{ success: false, error: `File not found: ${normalizedPath}` },
|
|
280
|
+
{ status: 404 }
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const fileContent = await fs.readFile(absolutePath, "utf-8");
|
|
284
|
+
console.log(`[/edit] componentName="${componentName}", filePath="${filePath}"`);
|
|
285
|
+
const ast = parseFile(fileContent);
|
|
286
|
+
if (!ast) {
|
|
287
|
+
return NextResponse.json(
|
|
288
|
+
{ success: false, error: "Failed to parse file" },
|
|
289
|
+
{ status: 400 }
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
const target = findTargetElement(ast, fileContent, {
|
|
293
|
+
componentName,
|
|
294
|
+
lineNumber,
|
|
295
|
+
elementContext
|
|
296
|
+
});
|
|
297
|
+
if (!target) {
|
|
298
|
+
return NextResponse.json(
|
|
299
|
+
{ success: false, error: "Could not locate target element" },
|
|
300
|
+
{ status: 400 }
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
console.log(
|
|
304
|
+
`📍 Found element <${(elementContext == null ? void 0 : elementContext.tagName) || "component"}> at lines ${target.startLine}-${target.endLine} (component: ${target.componentStart}-${target.componentEnd})`
|
|
305
|
+
);
|
|
306
|
+
console.log(
|
|
307
|
+
` Element context: tagName=${elementContext == null ? void 0 : elementContext.tagName}, nthOfType=${elementContext == null ? void 0 : elementContext.nthOfType}, textContent="${elementContext == null ? void 0 : elementContext.textContent}"`
|
|
308
|
+
);
|
|
309
|
+
const lines = fileContent.split("\n");
|
|
310
|
+
const foundElementCode = lines.slice(target.startLine - 1, Math.min(target.startLine + 2, target.endLine)).join("\n");
|
|
311
|
+
console.log(` Found element preview:
|
|
312
|
+
${foundElementCode}`);
|
|
313
|
+
const targetCode = lines.slice(target.startLine - 1, target.endLine).join("\n");
|
|
314
|
+
const baseIndentation = ((_a = lines[target.startLine - 1].match(/^(\s*)/)) == null ? void 0 : _a[1]) || "";
|
|
315
|
+
if (target.componentStart <= 0 || target.componentEnd === Infinity) {
|
|
316
|
+
console.error("❌ Invalid component bounds detected");
|
|
317
|
+
return NextResponse.json({
|
|
318
|
+
success: false,
|
|
319
|
+
error: `Could not determine component bounds. Component: ${target.componentStart}-${target.componentEnd}`
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
const componentLines = lines.slice(
|
|
323
|
+
target.componentStart - 1,
|
|
324
|
+
target.componentEnd
|
|
325
|
+
);
|
|
326
|
+
const annotatedLines = componentLines.map((line, idx) => {
|
|
327
|
+
const lineNum = target.componentStart + idx;
|
|
328
|
+
const isTargetStart = lineNum === target.startLine;
|
|
329
|
+
const isTargetEnd = lineNum === target.endLine;
|
|
330
|
+
const isWithinTarget = lineNum >= target.startLine && lineNum <= target.endLine;
|
|
331
|
+
let annotation = "";
|
|
332
|
+
if (isTargetStart && isTargetEnd) {
|
|
333
|
+
annotation = " // ← CLICKED ELEMENT (single line)";
|
|
334
|
+
} else if (isTargetStart) {
|
|
335
|
+
annotation = " // ← CLICKED ELEMENT STARTS";
|
|
336
|
+
} else if (isTargetEnd) {
|
|
337
|
+
annotation = " // ← CLICKED ELEMENT ENDS";
|
|
338
|
+
} else if (isWithinTarget) {
|
|
339
|
+
annotation = " // ← (clicked element)";
|
|
340
|
+
}
|
|
341
|
+
return line + annotation;
|
|
342
|
+
});
|
|
343
|
+
const fullComponentCode = annotatedLines.join("\n");
|
|
344
|
+
const newCode = await generateEdit({
|
|
345
|
+
targetCode,
|
|
346
|
+
fullComponentCode,
|
|
347
|
+
suggestion,
|
|
348
|
+
elementContext,
|
|
349
|
+
baseIndentation,
|
|
350
|
+
targetLine: target.startLine,
|
|
351
|
+
targetEndLine: target.endLine,
|
|
352
|
+
componentStart: target.componentStart,
|
|
353
|
+
componentEnd: target.componentEnd,
|
|
354
|
+
editHistory: editHistory || []
|
|
355
|
+
});
|
|
356
|
+
if (!newCode) {
|
|
357
|
+
return NextResponse.json({
|
|
358
|
+
success: false,
|
|
359
|
+
error: "AI failed to generate valid edit"
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
console.log("Raw AI response length:", newCode.length);
|
|
363
|
+
console.log("Looking for // FULL_COMPONENT marker...");
|
|
364
|
+
const fullComponentMatch = newCode.match(/\/\/ FULL_COMPONENT\s*\n([\s\S]+)/);
|
|
365
|
+
let codeToApply = newCode;
|
|
366
|
+
let startLineToReplace = target.startLine;
|
|
367
|
+
let endLineToReplace = target.endLine;
|
|
368
|
+
if (fullComponentMatch) {
|
|
369
|
+
console.log("Found // FULL_COMPONENT marker, extracting full component code");
|
|
370
|
+
codeToApply = fullComponentMatch[1].trim();
|
|
371
|
+
startLineToReplace = target.componentStart;
|
|
372
|
+
endLineToReplace = target.componentEnd;
|
|
373
|
+
console.log(
|
|
374
|
+
`🔄 AI returned full component modification (lines ${startLineToReplace}-${endLineToReplace})`
|
|
375
|
+
);
|
|
376
|
+
console.log("Extracted component code length:", codeToApply.length);
|
|
377
|
+
console.log("Extracted component code (first 300 chars):", codeToApply.substring(0, 300));
|
|
378
|
+
} else {
|
|
379
|
+
console.log("No // FULL_COMPONENT marker found, treating as target element modification");
|
|
380
|
+
console.log(
|
|
381
|
+
`🔄 AI returned target element modification (lines ${startLineToReplace}-${endLineToReplace})`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
console.log("Code to apply (first 200 chars):", codeToApply.substring(0, 200));
|
|
385
|
+
if (!validateGeneratedCode(codeToApply, targetCode, fileContent)) {
|
|
386
|
+
console.error("❌ Generated code failed validation");
|
|
387
|
+
console.error("=== Generated code START ===");
|
|
388
|
+
console.error(codeToApply);
|
|
389
|
+
console.error("=== Generated code END ===");
|
|
390
|
+
console.error(`Length: ${codeToApply.length} chars, ${codeToApply.split("\n").length} lines`);
|
|
391
|
+
return NextResponse.json({
|
|
392
|
+
success: false,
|
|
393
|
+
error: "Generated code is invalid - check server logs for details"
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
const newLines = [...lines];
|
|
397
|
+
newLines.splice(
|
|
398
|
+
startLineToReplace - 1,
|
|
399
|
+
endLineToReplace - startLineToReplace + 1,
|
|
400
|
+
...codeToApply.split("\n")
|
|
401
|
+
);
|
|
402
|
+
await fs.writeFile(absolutePath, newLines.join("\n"), "utf-8");
|
|
403
|
+
console.log(`✅ Updated ${normalizedPath}`);
|
|
404
|
+
return NextResponse.json({
|
|
405
|
+
success: true,
|
|
406
|
+
fileSnapshot: fileContent,
|
|
407
|
+
// Original file content for undo
|
|
408
|
+
generatedCode: codeToApply,
|
|
409
|
+
// AI-generated code
|
|
410
|
+
modifiedLines: {
|
|
411
|
+
start: startLineToReplace,
|
|
412
|
+
end: endLineToReplace
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.error("AI Editor error:", error);
|
|
417
|
+
return NextResponse.json(
|
|
418
|
+
{ success: false, error: String(error) },
|
|
419
|
+
{ status: 500 }
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async function generateEdit(options) {
|
|
424
|
+
const {
|
|
425
|
+
fullComponentCode,
|
|
426
|
+
suggestion,
|
|
427
|
+
baseIndentation,
|
|
428
|
+
editHistory
|
|
429
|
+
} = options;
|
|
430
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
431
|
+
if (!apiKey) throw new Error("ANTHROPIC_API_KEY not set");
|
|
432
|
+
const model = new ChatAnthropic({
|
|
433
|
+
apiKey,
|
|
434
|
+
modelName: "claude-sonnet-4-5-20250929",
|
|
435
|
+
maxTokens: 4096,
|
|
436
|
+
temperature: 0
|
|
437
|
+
});
|
|
438
|
+
const systemPrompt = `You are a precise code editor for React/JSX components.
|
|
439
|
+
|
|
440
|
+
WHAT YOU'LL SEE:
|
|
441
|
+
- Full component code with line annotations
|
|
442
|
+
- The clicked element is marked with "// ← CLICKED ELEMENT STARTS" and "// ← CLICKED ELEMENT ENDS"
|
|
443
|
+
- Lines within the element have "// ← (clicked element)"
|
|
444
|
+
- These are just annotations to help you - they are NOT part of the actual code
|
|
445
|
+
|
|
446
|
+
YOUR TASK:
|
|
447
|
+
Modify ONLY the marked element (unless the request requires changing its parent/wrapper).
|
|
448
|
+
|
|
449
|
+
OUTPUT:
|
|
450
|
+
- Just the modified element code (WITHOUT the annotation comments)
|
|
451
|
+
- OR if broader changes needed: "// FULL_COMPONENT\\n" + complete modified component (WITHOUT annotation comments)
|
|
452
|
+
|
|
453
|
+
RULES:
|
|
454
|
+
- Output ONLY code, no explanations
|
|
455
|
+
- No markdown fences
|
|
456
|
+
- Do NOT include the "// ← CLICKED ELEMENT" annotation comments in your output
|
|
457
|
+
- Preserve indentation
|
|
458
|
+
- Make minimal changes
|
|
459
|
+
- Do NOT modify unrelated elements`;
|
|
460
|
+
let userPrompt = "";
|
|
461
|
+
if (editHistory.length > 0) {
|
|
462
|
+
userPrompt += "Previous edits made to this element:\n";
|
|
463
|
+
editHistory.forEach((item, idx) => {
|
|
464
|
+
userPrompt += `${idx + 1}. ${item.suggestion} ${item.success ? "(✓ applied)" : "(✗ failed)"}
|
|
465
|
+
`;
|
|
466
|
+
});
|
|
467
|
+
userPrompt += "\n";
|
|
468
|
+
}
|
|
469
|
+
userPrompt += `Component (clicked element is annotated with "// ← CLICKED ELEMENT" comments):
|
|
470
|
+
|
|
471
|
+
\`\`\`jsx
|
|
472
|
+
${fullComponentCode}
|
|
473
|
+
\`\`\`
|
|
474
|
+
|
|
475
|
+
User request: "${suggestion}"
|
|
476
|
+
${editHistory.length > 0 ? "\n(Build upon previous changes)" : ""}
|
|
477
|
+
|
|
478
|
+
Modify the annotated element to fulfill this request. Remember: do NOT include the annotation comments in your output.`;
|
|
479
|
+
try {
|
|
480
|
+
const response = await model.invoke([
|
|
481
|
+
new SystemMessage(systemPrompt),
|
|
482
|
+
new HumanMessage(userPrompt)
|
|
483
|
+
]);
|
|
484
|
+
let code = typeof response.content === "string" ? response.content : String(response.content);
|
|
485
|
+
code = cleanGeneratedCode(code, baseIndentation);
|
|
486
|
+
return code || null;
|
|
487
|
+
} catch (e) {
|
|
488
|
+
console.error("AI generation error:", e);
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function cleanGeneratedCode(code, baseIndentation) {
|
|
493
|
+
var _a, _b, _c;
|
|
494
|
+
const isFullComponent = code.trim().startsWith("// FULL_COMPONENT");
|
|
495
|
+
let marker = "";
|
|
496
|
+
if (isFullComponent) {
|
|
497
|
+
marker = "// FULL_COMPONENT\n";
|
|
498
|
+
code = code.replace(/^\/\/ FULL_COMPONENT\n?/, "");
|
|
499
|
+
}
|
|
500
|
+
code = code.replace(/^```[\w]*\n?/gm, "").replace(/\n?```$/gm, "").trim();
|
|
501
|
+
code = code.replace(/^(Here'?s?|Here is|Modified|Output|Result)[:\s]*/i, "").replace(/\s*(Done|Complete|That'?s? it)\.?$/i, "").trim();
|
|
502
|
+
code = code.replace(/\s*\/\/ ← CLICKED ELEMENT STARTS/g, "").replace(/\s*\/\/ ← CLICKED ELEMENT ENDS/g, "").replace(/\s*\/\/ ← CLICKED ELEMENT \(single line\)/g, "").replace(/\s*\/\/ ← \(clicked element\)/g, "").trim();
|
|
503
|
+
if (!code) return "";
|
|
504
|
+
if (!isFullComponent) {
|
|
505
|
+
const lines = code.split("\n");
|
|
506
|
+
const firstLineIndent = ((_b = (_a = lines[0]) == null ? void 0 : _a.match(/^(\s*)/)) == null ? void 0 : _b[1]) || "";
|
|
507
|
+
const indentDiff = baseIndentation.length - firstLineIndent.length;
|
|
508
|
+
if (indentDiff !== 0) {
|
|
509
|
+
for (let i = 0; i < lines.length; i++) {
|
|
510
|
+
if (lines[i].trim()) {
|
|
511
|
+
const currentIndent = ((_c = lines[i].match(/^(\s*)/)) == null ? void 0 : _c[1]) || "";
|
|
512
|
+
const newIndentLength = Math.max(
|
|
513
|
+
0,
|
|
514
|
+
currentIndent.length + indentDiff
|
|
515
|
+
);
|
|
516
|
+
lines[i] = " ".repeat(newIndentLength) + lines[i].trimStart();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
code = lines.join("\n");
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return marker + code;
|
|
523
|
+
}
|
|
524
|
+
function validateGeneratedCode(newCode, originalCode, fileContent) {
|
|
525
|
+
try {
|
|
526
|
+
const isFullComponent = /^(export\s+)?(default\s+)?function\s+\w+/.test(newCode.trim()) || /^(export\s+)?(default\s+)?const\s+\w+\s*=/.test(newCode.trim());
|
|
527
|
+
if (isFullComponent) {
|
|
528
|
+
let codeToValidate = newCode;
|
|
529
|
+
if (fileContent) {
|
|
530
|
+
const interfaceMatches = fileContent.match(/^(interface|type)\s+\w+[^}]*\}/gm);
|
|
531
|
+
if (interfaceMatches) {
|
|
532
|
+
codeToValidate = interfaceMatches.join("\n\n") + "\n\n" + newCode;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
parser.parse(codeToValidate, {
|
|
536
|
+
sourceType: "module",
|
|
537
|
+
plugins: ["jsx", "typescript"]
|
|
538
|
+
});
|
|
539
|
+
} else {
|
|
540
|
+
const wrapped = `function _() { return (${newCode}); }`;
|
|
541
|
+
parser.parse(wrapped, {
|
|
542
|
+
sourceType: "module",
|
|
543
|
+
plugins: ["jsx", "typescript"]
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
} catch (e) {
|
|
547
|
+
console.error("Generated code parse error:", e);
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
const origBraces = (originalCode.match(/[{}]/g) || []).length;
|
|
551
|
+
const newBraces = (newCode.match(/[{}]/g) || []).length;
|
|
552
|
+
const origTags = (originalCode.match(/[<>]/g) || []).length;
|
|
553
|
+
const newTags = (newCode.match(/[<>]/g) || []).length;
|
|
554
|
+
if (Math.abs(origBraces - newBraces) > 4 || Math.abs(origTags - newTags) > 4) {
|
|
555
|
+
console.warn(
|
|
556
|
+
`Structure changed significantly: braces ${origBraces}->${newBraces}, tags ${origTags}->${newTags}`
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
function parseDebugStack(stack) {
|
|
562
|
+
if (!stack) return null;
|
|
563
|
+
const stackStr = typeof stack === "string" ? stack : stack.stack || String(stack);
|
|
564
|
+
const frames = stackStr.split("\n");
|
|
565
|
+
const skipPatterns = [
|
|
566
|
+
"node_modules",
|
|
567
|
+
"SegmentViewNode",
|
|
568
|
+
"LayoutRouter",
|
|
569
|
+
"ErrorBoundary",
|
|
570
|
+
"fakeJSXCallSite"
|
|
571
|
+
];
|
|
572
|
+
for (const frame of frames) {
|
|
573
|
+
if (skipPatterns.some((p) => frame.includes(p))) continue;
|
|
574
|
+
const match = frame.match(/at\s+(\w+)\s+\((.+?):(\d+):(\d+)\)?$/);
|
|
575
|
+
if (match) {
|
|
576
|
+
let filePath = match[2];
|
|
577
|
+
const line = parseInt(match[3], 10);
|
|
578
|
+
const column = parseInt(match[4], 10);
|
|
579
|
+
let chunkId;
|
|
580
|
+
const chunkMatch = filePath.match(/\?([^:]+)$/);
|
|
581
|
+
if (chunkMatch) {
|
|
582
|
+
chunkId = chunkMatch[1];
|
|
583
|
+
filePath = filePath.replace(/\?[^:]*$/, "");
|
|
584
|
+
}
|
|
585
|
+
filePath = cleanPathTurbopack(filePath);
|
|
586
|
+
if (!shouldSkip(filePath)) {
|
|
587
|
+
console.log("parseDebugStack extracted:", { filePath, line, column });
|
|
588
|
+
return { filePath, line, column, chunkId };
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
console.log(
|
|
593
|
+
"parseDebugStack: no valid frame found in stack:",
|
|
594
|
+
stackStr.substring(0, 200)
|
|
595
|
+
);
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
function cleanPathTurbopack(p) {
|
|
599
|
+
let cleaned = p;
|
|
600
|
+
cleaned = cleaned.replace(/^about:\/\/React\/Server\//, "");
|
|
601
|
+
if (cleaned.startsWith("file://")) {
|
|
602
|
+
cleaned = cleaned.replace(/^file:\/\//, "");
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
if (cleaned.includes("%")) {
|
|
606
|
+
cleaned = decodeURIComponent(cleaned);
|
|
607
|
+
}
|
|
608
|
+
} catch (e) {
|
|
609
|
+
console.warn("Failed to decode URL-encoded path:", cleaned, e);
|
|
610
|
+
}
|
|
611
|
+
cleaned = cleaned.replace(/^webpack-internal:\/\/\/\([^)]+\)\/\.\//, "").replace(/^webpack-internal:\/\/\/\([^)]+\)\//, "").replace(/^webpack-internal:\/\//, "").replace(/^webpack:\/\/[^/]*\//, "").replace(/^\([^)]+\)\//, "").replace(/\?.*$/, "");
|
|
612
|
+
if (cleaned.startsWith(".next/") || path.isAbsolute(cleaned)) {
|
|
613
|
+
return cleaned;
|
|
614
|
+
}
|
|
615
|
+
return cleaned;
|
|
616
|
+
}
|
|
617
|
+
function cleanPath(p) {
|
|
618
|
+
return cleanPathTurbopack(p);
|
|
619
|
+
}
|
|
620
|
+
function normalizeSourcePath(source, projectRoot) {
|
|
621
|
+
let cleaned = cleanPath(source);
|
|
622
|
+
cleaned = cleaned.replace(/\\/g, "/");
|
|
623
|
+
if (cleaned.startsWith(projectRoot)) {
|
|
624
|
+
cleaned = cleaned.substring(projectRoot.length + 1);
|
|
625
|
+
}
|
|
626
|
+
return cleaned.replace(/^\/+/, "");
|
|
627
|
+
}
|
|
628
|
+
function shouldSkip(p) {
|
|
629
|
+
if (!p) return true;
|
|
630
|
+
return ["node_modules", "next/dist", "react-dom"].some((s) => p.includes(s));
|
|
631
|
+
}
|
|
632
|
+
async function resolveOriginalPosition(compiledPos, projectRoot) {
|
|
633
|
+
try {
|
|
634
|
+
console.log("resolveOriginalPosition called with:", compiledPos);
|
|
635
|
+
let compiledFilePath = compiledPos.filePath;
|
|
636
|
+
compiledFilePath = cleanPathTurbopack(compiledFilePath);
|
|
637
|
+
console.log("After cleanPathTurbopack:", compiledFilePath);
|
|
638
|
+
if (compiledFilePath.startsWith("http://") || compiledFilePath.startsWith("https://")) {
|
|
639
|
+
const url = new URL(compiledFilePath);
|
|
640
|
+
const pathname = url.pathname;
|
|
641
|
+
if (pathname.startsWith("/_next/")) {
|
|
642
|
+
const relativePath = pathname.substring("/_next/".length);
|
|
643
|
+
compiledFilePath = path.join(projectRoot, ".next", "dev", relativePath);
|
|
644
|
+
} else {
|
|
645
|
+
console.warn("Unexpected HTTP URL path:", pathname);
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
} else if (!path.isAbsolute(compiledFilePath)) {
|
|
649
|
+
if (compiledFilePath.startsWith(".next/")) {
|
|
650
|
+
compiledFilePath = path.resolve(projectRoot, compiledFilePath);
|
|
651
|
+
} else {
|
|
652
|
+
const resolved = path.resolve(projectRoot, compiledFilePath);
|
|
653
|
+
if (await fileExists(resolved)) {
|
|
654
|
+
compiledFilePath = resolved;
|
|
655
|
+
} else {
|
|
656
|
+
const possiblePaths = [
|
|
657
|
+
path.join(projectRoot, ".next", "dev", compiledFilePath),
|
|
658
|
+
path.join(projectRoot, compiledFilePath)
|
|
659
|
+
];
|
|
660
|
+
for (const tryPath of possiblePaths) {
|
|
661
|
+
if (await fileExists(tryPath)) {
|
|
662
|
+
compiledFilePath = tryPath;
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
} else {
|
|
669
|
+
compiledFilePath = path.normalize(compiledFilePath);
|
|
670
|
+
}
|
|
671
|
+
console.log("Normalized compiled file path:", compiledFilePath);
|
|
672
|
+
const compiledExists = await fileExists(compiledFilePath);
|
|
673
|
+
console.log(
|
|
674
|
+
"Compiled file exists:",
|
|
675
|
+
compiledExists,
|
|
676
|
+
"at:",
|
|
677
|
+
compiledFilePath
|
|
678
|
+
);
|
|
679
|
+
if (!compiledExists) {
|
|
680
|
+
console.error(
|
|
681
|
+
`Compiled file not found: ${compiledFilePath} (from parsed: ${compiledPos.filePath})`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
const sourceMapPath = compiledFilePath + ".map";
|
|
685
|
+
console.log("Looking for source map at:", sourceMapPath);
|
|
686
|
+
const sourceMapExists = await fileExists(sourceMapPath);
|
|
687
|
+
console.log("Source map exists:", sourceMapExists);
|
|
688
|
+
if (!sourceMapExists) {
|
|
689
|
+
console.error(
|
|
690
|
+
`Source map not found: ${sourceMapPath} (from compiled: ${compiledPos.filePath}, normalized: ${compiledFilePath})`
|
|
691
|
+
);
|
|
692
|
+
const altPaths = [
|
|
693
|
+
compiledFilePath.replace(/\.js$/, ".map"),
|
|
694
|
+
path.join(
|
|
695
|
+
path.dirname(compiledFilePath),
|
|
696
|
+
path.basename(compiledFilePath) + ".map"
|
|
697
|
+
)
|
|
698
|
+
];
|
|
699
|
+
console.log("Trying alternative paths:", altPaths);
|
|
700
|
+
for (const altPath of altPaths) {
|
|
701
|
+
if (await fileExists(altPath)) {
|
|
702
|
+
console.log("Found source map at alternative path:", altPath);
|
|
703
|
+
return await resolveFromSourceMap(altPath, compiledPos, projectRoot);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
return await resolveFromSourceMap(sourceMapPath, compiledPos, projectRoot);
|
|
709
|
+
} catch (error) {
|
|
710
|
+
console.error("Error resolving source map:", error);
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
async function resolveFromSourceMap(sourceMapPath, compiledPos, projectRoot) {
|
|
715
|
+
try {
|
|
716
|
+
const sourceMapContent = await fs.readFile(sourceMapPath, "utf-8");
|
|
717
|
+
const sourceMap = JSON.parse(sourceMapContent);
|
|
718
|
+
if ((!sourceMap.sources || sourceMap.sources.length === 0) && (!sourceMap.sections || sourceMap.sections.length === 0)) {
|
|
719
|
+
console.warn(
|
|
720
|
+
"Empty source map detected, cannot resolve position:",
|
|
721
|
+
sourceMapPath
|
|
722
|
+
);
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
if (sourceMap.sections) {
|
|
726
|
+
let matchingSection = null;
|
|
727
|
+
for (let i = sourceMap.sections.length - 1; i >= 0; i--) {
|
|
728
|
+
const section = sourceMap.sections[i];
|
|
729
|
+
const offset = section.offset;
|
|
730
|
+
const sectionStartLine1Indexed = offset.line + 1;
|
|
731
|
+
if (compiledPos.line > sectionStartLine1Indexed || compiledPos.line === sectionStartLine1Indexed && compiledPos.column >= offset.column) {
|
|
732
|
+
matchingSection = section;
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (matchingSection && matchingSection.map) {
|
|
737
|
+
const sectionMap = matchingSection.map;
|
|
738
|
+
const offset = matchingSection.offset;
|
|
739
|
+
let consumer2;
|
|
740
|
+
try {
|
|
741
|
+
consumer2 = await new SourceMapConsumer(sectionMap);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
console.error("Error creating SourceMapConsumer:", error);
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
const adjustedLine = compiledPos.line - offset.line;
|
|
748
|
+
const adjustedColumn = compiledPos.line === offset.line + 1 ? compiledPos.column - offset.column : compiledPos.column;
|
|
749
|
+
const originalPos = consumer2.originalPositionFor({
|
|
750
|
+
line: adjustedLine,
|
|
751
|
+
column: adjustedColumn
|
|
752
|
+
});
|
|
753
|
+
if (originalPos.source && originalPos.line !== null) {
|
|
754
|
+
const source = normalizeSourcePath(
|
|
755
|
+
originalPos.source || "",
|
|
756
|
+
projectRoot
|
|
757
|
+
);
|
|
758
|
+
return {
|
|
759
|
+
source,
|
|
760
|
+
line: originalPos.line,
|
|
761
|
+
column: originalPos.column ?? 0
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
} finally {
|
|
765
|
+
consumer2.destroy();
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
const consumer = await new SourceMapConsumer(sourceMap);
|
|
771
|
+
try {
|
|
772
|
+
const originalPos = consumer.originalPositionFor({
|
|
773
|
+
line: compiledPos.line,
|
|
774
|
+
column: compiledPos.column
|
|
775
|
+
});
|
|
776
|
+
if (originalPos.source && originalPos.line !== null) {
|
|
777
|
+
const source = normalizeSourcePath(
|
|
778
|
+
originalPos.source || "",
|
|
779
|
+
projectRoot
|
|
780
|
+
);
|
|
781
|
+
return {
|
|
782
|
+
source,
|
|
783
|
+
line: originalPos.line,
|
|
784
|
+
column: originalPos.column ?? 0
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
} finally {
|
|
788
|
+
consumer.destroy();
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.error("Error resolving source map:", error);
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
async function fileExists(filePath) {
|
|
797
|
+
try {
|
|
798
|
+
await fs.access(filePath);
|
|
799
|
+
return true;
|
|
800
|
+
} catch {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
async function getOriginalPositionFromDebugStack(debugStack, projectRoot) {
|
|
805
|
+
const compiledPos = parseDebugStack(debugStack);
|
|
806
|
+
if (!compiledPos) return null;
|
|
807
|
+
return await resolveOriginalPosition(compiledPos, projectRoot);
|
|
808
|
+
}
|
|
809
|
+
async function handleRead(req) {
|
|
810
|
+
var _a;
|
|
811
|
+
const devModeError = validateDevMode();
|
|
812
|
+
if (devModeError) return devModeError;
|
|
813
|
+
const { searchParams } = new URL(req.url);
|
|
814
|
+
let filePath = searchParams.get("path") || "";
|
|
815
|
+
let lineNumber = parseInt(searchParams.get("line") || "1");
|
|
816
|
+
const debugStack = searchParams.get("debugStack") || "";
|
|
817
|
+
const tagName = searchParams.get("tagName") || "";
|
|
818
|
+
const nthOfType = parseInt(searchParams.get("nthOfType") || "0");
|
|
819
|
+
const textContent = searchParams.get("textContent") || "";
|
|
820
|
+
const className = searchParams.get("className") || "";
|
|
821
|
+
const elementContext = tagName ? {
|
|
822
|
+
tagName,
|
|
823
|
+
nthOfType: nthOfType > 0 ? nthOfType : void 0,
|
|
824
|
+
textContent: textContent || void 0,
|
|
825
|
+
className: className || void 0
|
|
826
|
+
} : void 0;
|
|
827
|
+
const projectRoot = process.cwd();
|
|
828
|
+
if (debugStack) {
|
|
829
|
+
const compiledPos = parseDebugStack(debugStack);
|
|
830
|
+
if (compiledPos) {
|
|
831
|
+
const originalPos = await resolveOriginalPosition(
|
|
832
|
+
compiledPos,
|
|
833
|
+
projectRoot
|
|
834
|
+
);
|
|
835
|
+
if (originalPos) {
|
|
836
|
+
filePath = originalPos.source;
|
|
837
|
+
lineNumber = originalPos.line;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
const normalizedPath = normalizePath(filePath);
|
|
842
|
+
const absolutePath = await resolveFilePath(projectRoot, normalizedPath);
|
|
843
|
+
if (!absolutePath) {
|
|
844
|
+
return NextResponse.json(
|
|
845
|
+
{ success: false, error: "File not found" },
|
|
846
|
+
{ status: 404 }
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
const content = await fs.readFile(absolutePath, "utf-8");
|
|
850
|
+
const ast = parseFile(content);
|
|
851
|
+
if (!ast) {
|
|
852
|
+
return NextResponse.json(
|
|
853
|
+
{ success: false, error: "Failed to parse file" },
|
|
854
|
+
{ status: 400 }
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
let componentName = "";
|
|
858
|
+
traverse(ast, {
|
|
859
|
+
ExportDefaultDeclaration(path2) {
|
|
860
|
+
var _a2;
|
|
861
|
+
if (t.isFunctionDeclaration(path2.node.declaration)) {
|
|
862
|
+
componentName = ((_a2 = path2.node.declaration.id) == null ? void 0 : _a2.name) || "";
|
|
863
|
+
}
|
|
864
|
+
},
|
|
865
|
+
ExportNamedDeclaration(path2) {
|
|
866
|
+
var _a2;
|
|
867
|
+
if (t.isFunctionDeclaration(path2.node.declaration)) {
|
|
868
|
+
componentName = ((_a2 = path2.node.declaration.id) == null ? void 0 : _a2.name) || "";
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
if (!componentName) {
|
|
873
|
+
return NextResponse.json({
|
|
874
|
+
success: true,
|
|
875
|
+
content,
|
|
876
|
+
lineStart: 1,
|
|
877
|
+
lineEnd: content.split("\n").length
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
const target = findTargetElement(ast, content, {
|
|
881
|
+
componentName,
|
|
882
|
+
lineNumber,
|
|
883
|
+
elementContext
|
|
884
|
+
});
|
|
885
|
+
if (!target) {
|
|
886
|
+
return NextResponse.json({
|
|
887
|
+
success: true,
|
|
888
|
+
content,
|
|
889
|
+
lineStart: 1,
|
|
890
|
+
lineEnd: content.split("\n").length
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
console.log(`[/read] Found element:`);
|
|
894
|
+
console.log(
|
|
895
|
+
` Component: ${componentName} (lines ${target.componentStart}-${target.componentEnd})`
|
|
896
|
+
);
|
|
897
|
+
console.log(` Target element: lines ${target.startLine}-${target.endLine}`);
|
|
898
|
+
console.log(
|
|
899
|
+
` Element context: tagName=${elementContext == null ? void 0 : elementContext.tagName}, nthOfType=${elementContext == null ? void 0 : elementContext.nthOfType}`
|
|
900
|
+
);
|
|
901
|
+
const foundLines = content.split("\n").slice(
|
|
902
|
+
target.startLine - 1,
|
|
903
|
+
Math.min(target.startLine + 2, target.endLine)
|
|
904
|
+
);
|
|
905
|
+
console.log(` Preview: ${(_a = foundLines[0]) == null ? void 0 : _a.trim()}`);
|
|
906
|
+
console.log(
|
|
907
|
+
` textContent="${elementContext == null ? void 0 : elementContext.textContent}", className="${elementContext == null ? void 0 : elementContext.className}"`
|
|
908
|
+
);
|
|
909
|
+
const lines = content.split("\n");
|
|
910
|
+
const componentLines = lines.slice(
|
|
911
|
+
target.componentStart - 1,
|
|
912
|
+
target.componentEnd
|
|
913
|
+
);
|
|
914
|
+
const preview = componentLines.join("\n");
|
|
915
|
+
return NextResponse.json({
|
|
916
|
+
success: true,
|
|
917
|
+
content: preview,
|
|
918
|
+
lineStart: target.componentStart,
|
|
919
|
+
lineEnd: target.componentEnd,
|
|
920
|
+
targetStartLine: target.startLine,
|
|
921
|
+
targetEndLine: target.endLine,
|
|
922
|
+
componentName
|
|
923
|
+
// Return the actual component name parsed from code
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
async function handleUndo(req) {
|
|
927
|
+
const devModeError = validateDevMode();
|
|
928
|
+
if (devModeError) return devModeError;
|
|
929
|
+
try {
|
|
930
|
+
const body = await req.json();
|
|
931
|
+
const { filePath, content } = body;
|
|
932
|
+
if (!filePath || typeof content !== "string") {
|
|
933
|
+
return NextResponse.json(
|
|
934
|
+
{
|
|
935
|
+
success: false,
|
|
936
|
+
error: "Invalid request: filePath and content required"
|
|
937
|
+
},
|
|
938
|
+
{ status: 400 }
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
const projectRoot = process.cwd();
|
|
942
|
+
const normalizedPath = normalizePath(filePath);
|
|
943
|
+
const absolutePath = await resolveFilePath(projectRoot, normalizedPath);
|
|
944
|
+
if (!absolutePath) {
|
|
945
|
+
return NextResponse.json(
|
|
946
|
+
{ success: false, error: `File not found: ${normalizedPath}` },
|
|
947
|
+
{ status: 404 }
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
if (!isPathSecure(absolutePath, projectRoot)) {
|
|
951
|
+
return NextResponse.json(
|
|
952
|
+
{ success: false, error: "Access denied: File outside project root" },
|
|
953
|
+
{ status: 403 }
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
await fs.writeFile(absolutePath, content, "utf-8");
|
|
957
|
+
console.log(`✅ Undo: Restored ${normalizedPath}`);
|
|
958
|
+
return NextResponse.json({ success: true });
|
|
959
|
+
} catch (error) {
|
|
960
|
+
console.error("Undo error:", error);
|
|
961
|
+
return NextResponse.json(
|
|
962
|
+
{ success: false, error: String(error) },
|
|
963
|
+
{ status: 500 }
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
async function handleResolve(req) {
|
|
968
|
+
const devModeError = validateDevMode();
|
|
969
|
+
if (devModeError) return devModeError;
|
|
970
|
+
try {
|
|
971
|
+
const body = await req.json();
|
|
972
|
+
const debugStack = body == null ? void 0 : body.debugStack;
|
|
973
|
+
if (!debugStack || typeof debugStack !== "string") {
|
|
974
|
+
return NextResponse.json(
|
|
975
|
+
{ success: false, error: "Missing debugStack" },
|
|
976
|
+
{ status: 400 }
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
const compiledPos = parseDebugStack(debugStack);
|
|
980
|
+
if (!compiledPos) {
|
|
981
|
+
console.error("Could not parse debug stack:", debugStack);
|
|
982
|
+
return NextResponse.json(
|
|
983
|
+
{ success: false, error: "Could not parse stack" },
|
|
984
|
+
{ status: 422 }
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
console.log("Parsed compiled position:", compiledPos);
|
|
988
|
+
const originalPos = await resolveOriginalPosition(
|
|
989
|
+
compiledPos,
|
|
990
|
+
process.cwd()
|
|
991
|
+
);
|
|
992
|
+
if (!originalPos) {
|
|
993
|
+
console.error(
|
|
994
|
+
"Source map lookup failed for:",
|
|
995
|
+
compiledPos.filePath,
|
|
996
|
+
"at line",
|
|
997
|
+
compiledPos.line
|
|
998
|
+
);
|
|
999
|
+
return NextResponse.json(
|
|
1000
|
+
{ success: false, error: "Source map lookup failed" },
|
|
1001
|
+
{ status: 404 }
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
console.log("Resolved original position:", originalPos);
|
|
1005
|
+
return NextResponse.json({
|
|
1006
|
+
success: true,
|
|
1007
|
+
filePath: originalPos.source,
|
|
1008
|
+
lineNumber: originalPos.line,
|
|
1009
|
+
columnNumber: originalPos.column ?? 0
|
|
1010
|
+
});
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
console.error("Source resolve error:", error);
|
|
1013
|
+
return NextResponse.json(
|
|
1014
|
+
{ success: false, error: "Internal error" },
|
|
1015
|
+
{ status: 500 }
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
async function handleAbsolutePath(req) {
|
|
1020
|
+
const devModeError = validateDevMode();
|
|
1021
|
+
if (devModeError) return devModeError;
|
|
1022
|
+
const { searchParams } = new URL(req.url);
|
|
1023
|
+
const filePath = searchParams.get("path") || "";
|
|
1024
|
+
if (!filePath) {
|
|
1025
|
+
return NextResponse.json(
|
|
1026
|
+
{ success: false, error: "Missing path" },
|
|
1027
|
+
{ status: 400 }
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
const projectRoot = process.cwd();
|
|
1031
|
+
const normalizedPath = normalizePath(filePath);
|
|
1032
|
+
const absolutePath = await resolveFilePath(projectRoot, normalizedPath);
|
|
1033
|
+
if (!absolutePath) {
|
|
1034
|
+
return NextResponse.json(
|
|
1035
|
+
{ success: false, error: "File not found" },
|
|
1036
|
+
{ status: 404 }
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
return NextResponse.json({
|
|
1040
|
+
success: true,
|
|
1041
|
+
absolutePath
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
async function handleValidateSession(req) {
|
|
1045
|
+
const devModeError = validateDevMode();
|
|
1046
|
+
if (devModeError) return devModeError;
|
|
1047
|
+
try {
|
|
1048
|
+
const body = await req.json();
|
|
1049
|
+
const { filePath, lastKnownHash, lastKnownSize } = body;
|
|
1050
|
+
if (!filePath || !lastKnownHash || lastKnownSize === void 0) {
|
|
1051
|
+
return NextResponse.json(
|
|
1052
|
+
{ success: false, error: "Missing required fields" },
|
|
1053
|
+
{ status: 400 }
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
const projectRoot = process.cwd();
|
|
1057
|
+
const normalizedPath = normalizePath(filePath);
|
|
1058
|
+
const absolutePath = await resolveFilePath(projectRoot, normalizedPath);
|
|
1059
|
+
if (!absolutePath) {
|
|
1060
|
+
return NextResponse.json(
|
|
1061
|
+
{ success: false, error: "File not found" },
|
|
1062
|
+
{ status: 404 }
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
if (!isPathSecure(absolutePath, projectRoot)) {
|
|
1066
|
+
return NextResponse.json(
|
|
1067
|
+
{ success: false, error: "Access denied" },
|
|
1068
|
+
{ status: 403 }
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
const content = await fs.readFile(absolutePath, "utf-8");
|
|
1072
|
+
const hash = crypto.createHash("sha256").update(content).digest("hex");
|
|
1073
|
+
const stats = await fs.stat(absolutePath);
|
|
1074
|
+
const isValid = hash === lastKnownHash && content.length === lastKnownSize;
|
|
1075
|
+
console.log(`[validate-session] ${filePath}:`);
|
|
1076
|
+
console.log(` Hash match: ${hash === lastKnownHash}`);
|
|
1077
|
+
console.log(` Size match: ${content.length === lastKnownSize}`);
|
|
1078
|
+
console.log(` Is valid: ${isValid}`);
|
|
1079
|
+
return NextResponse.json({
|
|
1080
|
+
success: true,
|
|
1081
|
+
isValid,
|
|
1082
|
+
currentHash: hash,
|
|
1083
|
+
currentSize: content.length,
|
|
1084
|
+
lastModifiedTime: stats.mtimeMs
|
|
1085
|
+
});
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
console.error("Validation error:", error);
|
|
1088
|
+
return NextResponse.json(
|
|
1089
|
+
{ success: false, error: String(error) },
|
|
1090
|
+
{ status: 500 }
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
async function handleAIEditorRequest(req, context) {
|
|
1095
|
+
const { path: path2 } = await context.params;
|
|
1096
|
+
const endpoint = path2[0];
|
|
1097
|
+
const method = req.method;
|
|
1098
|
+
switch (endpoint) {
|
|
1099
|
+
case "edit":
|
|
1100
|
+
if (method === "POST") return handleEdit(req);
|
|
1101
|
+
break;
|
|
1102
|
+
case "read":
|
|
1103
|
+
if (method === "GET") return handleRead(req);
|
|
1104
|
+
break;
|
|
1105
|
+
case "undo":
|
|
1106
|
+
if (method === "POST") return handleUndo(req);
|
|
1107
|
+
break;
|
|
1108
|
+
case "resolve":
|
|
1109
|
+
if (method === "POST") return handleResolve(req);
|
|
1110
|
+
break;
|
|
1111
|
+
case "absolute-path":
|
|
1112
|
+
if (method === "GET") return handleAbsolutePath(req);
|
|
1113
|
+
break;
|
|
1114
|
+
case "validate-session":
|
|
1115
|
+
if (method === "POST") return handleValidateSession(req);
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
return NextResponse.json(
|
|
1119
|
+
{ error: `Unknown endpoint: ${endpoint}` },
|
|
1120
|
+
{ status: 404 }
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
export {
|
|
1124
|
+
handleEdit as a,
|
|
1125
|
+
handleRead as b,
|
|
1126
|
+
handleUndo as c,
|
|
1127
|
+
handleResolve as d,
|
|
1128
|
+
handleAbsolutePath as e,
|
|
1129
|
+
handleValidateSession as f,
|
|
1130
|
+
fileExists$1 as g,
|
|
1131
|
+
handleAIEditorRequest as h,
|
|
1132
|
+
isPathSecure as i,
|
|
1133
|
+
findTargetElement as j,
|
|
1134
|
+
getJSXMemberName as k,
|
|
1135
|
+
getAttributeValue as l,
|
|
1136
|
+
parseDebugStack as m,
|
|
1137
|
+
normalizePath as n,
|
|
1138
|
+
resolveOriginalPosition as o,
|
|
1139
|
+
parseFile as p,
|
|
1140
|
+
getOriginalPositionFromDebugStack as q,
|
|
1141
|
+
resolveFilePath as r,
|
|
1142
|
+
scoreElementMatch as s,
|
|
1143
|
+
validateDevMode as v
|
|
1144
|
+
};
|
|
1145
|
+
//# sourceMappingURL=index-BFa7H-uO.js.map
|