farmon 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 +163 -0
- package/dist/bin/farmon.js +12 -0
- package/dist/bin/farmon.js.map +1 -0
- package/dist/execute/agents/index.js +19 -0
- package/dist/execute/agents/index.js.map +1 -0
- package/dist/execute/agents/instruction-classifier-agent.js +16 -0
- package/dist/execute/agents/instruction-classifier-agent.js.map +1 -0
- package/dist/execute/agents/mutation-agent.js +272 -0
- package/dist/execute/agents/mutation-agent.js.map +1 -0
- package/dist/execute/agents/query-agent.js +118 -0
- package/dist/execute/agents/query-agent.js.map +1 -0
- package/dist/execute/helpers/analyzers.js +8 -0
- package/dist/execute/helpers/analyzers.js.map +1 -0
- package/dist/execute/helpers/ensurers.js +1053 -0
- package/dist/execute/helpers/ensurers.js.map +1 -0
- package/dist/execute/helpers/finders.js +1454 -0
- package/dist/execute/helpers/finders.js.map +1 -0
- package/dist/execute/helpers/general.js +3736 -0
- package/dist/execute/helpers/general.js.map +1 -0
- package/dist/execute/helpers/import-helpers.js +183 -0
- package/dist/execute/helpers/import-helpers.js.map +1 -0
- package/dist/execute/helpers/parsers.js +840 -0
- package/dist/execute/helpers/parsers.js.map +1 -0
- package/dist/execute/helpers/prompt-maker.js +1163 -0
- package/dist/execute/helpers/prompt-maker.js.map +1 -0
- package/dist/execute/helpers/validators.js +40 -0
- package/dist/execute/helpers/validators.js.map +1 -0
- package/dist/execute/history/history-manager.js +1030 -0
- package/dist/execute/history/history-manager.js.map +1 -0
- package/dist/execute/history/rollback-handlers.js +2524 -0
- package/dist/execute/history/rollback-handlers.js.map +1 -0
- package/dist/execute/index.js +44 -0
- package/dist/execute/index.js.map +1 -0
- package/dist/execute/llm/call.js +103 -0
- package/dist/execute/llm/call.js.map +1 -0
- package/dist/execute/tasks/ast.js +3819 -0
- package/dist/execute/tasks/ast.js.map +1 -0
- package/dist/execute/tasks/generators.js +96 -0
- package/dist/execute/tasks/generators.js.map +1 -0
- package/dist/execute/tasks/index.js +7 -0
- package/dist/execute/tasks/index.js.map +1 -0
- package/dist/execute/tasks/mutations.js +8139 -0
- package/dist/execute/tasks/mutations.js.map +1 -0
- package/dist/execute/tasks/query.js +248 -0
- package/dist/execute/tasks/query.js.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/index.js +15 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/ollama.js +40 -0
- package/dist/providers/ollama.js.map +1 -0
- package/dist/providers/openai-compatible.js +52 -0
- package/dist/providers/openai-compatible.js.map +1 -0
- package/dist/runtime/inject.js +250 -0
- package/dist/runtime/inject.js.map +1 -0
- package/dist/schemas/agent/action.schema.js +935 -0
- package/dist/schemas/agent/action.schema.js.map +1 -0
- package/dist/schemas/agent/index.js +4 -0
- package/dist/schemas/agent/index.js.map +1 -0
- package/dist/schemas/agent/llm.schema.js +16 -0
- package/dist/schemas/agent/llm.schema.js.map +1 -0
- package/dist/schemas/agent/planner.schema.js +17 -0
- package/dist/schemas/agent/planner.schema.js.map +1 -0
- package/dist/schemas/index.js +7 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/project/context.schema.js +2 -0
- package/dist/schemas/project/context.schema.js.map +1 -0
- package/dist/schemas/project/index.js +2 -0
- package/dist/schemas/project/index.js.map +1 -0
- package/dist/schemas/runtime/index.js +4 -0
- package/dist/schemas/runtime/index.js.map +1 -0
- package/dist/schemas/runtime/injector.schema.js +11 -0
- package/dist/schemas/runtime/injector.schema.js.map +1 -0
- package/dist/schemas/runtime/runtime.schema.js +73 -0
- package/dist/schemas/runtime/runtime.schema.js.map +1 -0
- package/dist/schemas/runtime/sse.schema.js +15 -0
- package/dist/schemas/runtime/sse.schema.js.map +1 -0
- package/dist/schemas/system/index.js +2 -0
- package/dist/schemas/system/index.js.map +1 -0
- package/dist/schemas/system/logger.schema.js +56 -0
- package/dist/schemas/system/logger.schema.js.map +1 -0
- package/dist/schemas/task/index.js +9 -0
- package/dist/schemas/task/index.js.map +1 -0
- package/dist/server/app-context.js +254 -0
- package/dist/server/app-context.js.map +1 -0
- package/dist/server/config.js +22 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/error.js +22 -0
- package/dist/server/error.js.map +1 -0
- package/dist/server/event-bus.js +60 -0
- package/dist/server/event-bus.js.map +1 -0
- package/dist/server/logger.js +57 -0
- package/dist/server/logger.js.map +1 -0
- package/dist/server/run.js +265 -0
- package/dist/server/run.js.map +1 -0
- package/dist/server/sse.js +143 -0
- package/dist/server/sse.js.map +1 -0
- package/dist/ui/assets/index-C4ydQSAw.css +2 -0
- package/dist/ui/assets/index-Dzo7S5xs.js +85 -0
- package/dist/ui/favicon.svg +1 -0
- package/dist/ui/icons.svg +24 -0
- package/dist/ui/index.html +14 -0
- package/dist/workers/prettier.js +11 -0
- package/dist/workers/prettier.js.map +1 -0
- package/package.json +114 -0
|
@@ -0,0 +1,1454 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import traverse from "@babel/traverse";
|
|
4
|
+
import t from "@babel/types";
|
|
5
|
+
import { ERROR_CODES } from "../../schemas/index.js";
|
|
6
|
+
import { LoomaError } from "../../server/error.js";
|
|
7
|
+
/*
|
|
8
|
+
|
|
9
|
+
const importPath = findImportDeclaration({
|
|
10
|
+
ast,
|
|
11
|
+
source: "react",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (importPath) {
|
|
15
|
+
console.log(importPath.node);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
*/
|
|
19
|
+
function findImportDeclaration({ ast, source }) {
|
|
20
|
+
let importDeclarationPath = null;
|
|
21
|
+
traverse.default(ast, {
|
|
22
|
+
ImportDeclaration(path) {
|
|
23
|
+
if (path.node.source.value === source) {
|
|
24
|
+
importDeclarationPath = path;
|
|
25
|
+
path.stop();
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
return importDeclarationPath;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Finds the deepest AST node that exists at a specific line number.
|
|
33
|
+
*
|
|
34
|
+
* ------------------------------------------------------------
|
|
35
|
+
* WHAT THIS FUNCTION DOES
|
|
36
|
+
* ------------------------------------------------------------
|
|
37
|
+
*
|
|
38
|
+
* This function searches an AST and returns the node
|
|
39
|
+
* that belongs to a given source-code line.
|
|
40
|
+
*
|
|
41
|
+
* Example:
|
|
42
|
+
*
|
|
43
|
+
* CODE:
|
|
44
|
+
*
|
|
45
|
+
* 1 | import React from 'react';
|
|
46
|
+
* 2 |
|
|
47
|
+
* 3 | function Header() {
|
|
48
|
+
* 4 | return <div>Hello</div>;
|
|
49
|
+
* 5 | }
|
|
50
|
+
*
|
|
51
|
+
* Calling:
|
|
52
|
+
*
|
|
53
|
+
* findNodeByLine({ ast, line: 4 })
|
|
54
|
+
*
|
|
55
|
+
* may return:
|
|
56
|
+
*
|
|
57
|
+
* JSXElement node
|
|
58
|
+
*
|
|
59
|
+
* ------------------------------------------------------------
|
|
60
|
+
* WHY THIS FUNCTION EXISTS
|
|
61
|
+
* ------------------------------------------------------------
|
|
62
|
+
*
|
|
63
|
+
* In AI-powered code editing systems,
|
|
64
|
+
* users often refer to code by location.
|
|
65
|
+
*
|
|
66
|
+
* Examples:
|
|
67
|
+
*
|
|
68
|
+
* - "replace code at line 20"
|
|
69
|
+
* - "insert variable near line 15"
|
|
70
|
+
* - "wrap JSX around line 42"
|
|
71
|
+
* - "delete component at line 10"
|
|
72
|
+
*
|
|
73
|
+
* AST traversal alone is not enough.
|
|
74
|
+
*
|
|
75
|
+
* We also need:
|
|
76
|
+
*
|
|
77
|
+
* source-code location mapping.
|
|
78
|
+
*
|
|
79
|
+
* ------------------------------------------------------------
|
|
80
|
+
* WHAT IS NODE LOCATION?
|
|
81
|
+
* ------------------------------------------------------------
|
|
82
|
+
*
|
|
83
|
+
* Babel AST nodes contain:
|
|
84
|
+
*
|
|
85
|
+
* node.loc.start.line
|
|
86
|
+
* node.loc.end.line
|
|
87
|
+
*
|
|
88
|
+
* Example:
|
|
89
|
+
*
|
|
90
|
+
* function Header() {}
|
|
91
|
+
*
|
|
92
|
+
* may internally store:
|
|
93
|
+
*
|
|
94
|
+
* start.line = 3
|
|
95
|
+
* end.line = 5
|
|
96
|
+
*
|
|
97
|
+
* ------------------------------------------------------------
|
|
98
|
+
* IMPORTANT ARCHITECTURAL IDEA
|
|
99
|
+
* ------------------------------------------------------------
|
|
100
|
+
*
|
|
101
|
+
* findNodeByLine is:
|
|
102
|
+
*
|
|
103
|
+
* INTERNAL ONLY
|
|
104
|
+
*
|
|
105
|
+
* WHY?
|
|
106
|
+
*
|
|
107
|
+
* Users never explicitly ask:
|
|
108
|
+
*
|
|
109
|
+
* "find AST node by line"
|
|
110
|
+
*
|
|
111
|
+
* Instead:
|
|
112
|
+
*
|
|
113
|
+
* higher-level primitives internally use it.
|
|
114
|
+
*
|
|
115
|
+
* ------------------------------------------------------------
|
|
116
|
+
* WHY THIS FUNCTION IS IMPORTANT
|
|
117
|
+
* ------------------------------------------------------------
|
|
118
|
+
*
|
|
119
|
+
* Many editing primitives need precise targeting.
|
|
120
|
+
*
|
|
121
|
+
* Example:
|
|
122
|
+
*
|
|
123
|
+
* insertVariable()
|
|
124
|
+
*
|
|
125
|
+
* may need:
|
|
126
|
+
*
|
|
127
|
+
* "insert near line 20"
|
|
128
|
+
*
|
|
129
|
+
* Example:
|
|
130
|
+
*
|
|
131
|
+
* replaceJSX()
|
|
132
|
+
*
|
|
133
|
+
* may need:
|
|
134
|
+
*
|
|
135
|
+
* "replace JSX around line 55"
|
|
136
|
+
*
|
|
137
|
+
* ------------------------------------------------------------
|
|
138
|
+
* HOW MATCHING WORKS
|
|
139
|
+
* ------------------------------------------------------------
|
|
140
|
+
*
|
|
141
|
+
* A node matches when:
|
|
142
|
+
*
|
|
143
|
+
* targetLine >= start.line
|
|
144
|
+
* AND
|
|
145
|
+
* targetLine <= end.line
|
|
146
|
+
*
|
|
147
|
+
* Example:
|
|
148
|
+
*
|
|
149
|
+
* function Header() {
|
|
150
|
+
* return <div />;
|
|
151
|
+
* }
|
|
152
|
+
*
|
|
153
|
+
* covers:
|
|
154
|
+
*
|
|
155
|
+
* lines 3 → 5
|
|
156
|
+
*
|
|
157
|
+
* ------------------------------------------------------------
|
|
158
|
+
* DEEPEST NODE STRATEGY
|
|
159
|
+
* ------------------------------------------------------------
|
|
160
|
+
*
|
|
161
|
+
* Many nodes overlap same line.
|
|
162
|
+
*
|
|
163
|
+
* Example:
|
|
164
|
+
*
|
|
165
|
+
* function Header() {
|
|
166
|
+
* return <div>Hello</div>;
|
|
167
|
+
* }
|
|
168
|
+
*
|
|
169
|
+
* line 4 belongs to:
|
|
170
|
+
*
|
|
171
|
+
* - FunctionDeclaration
|
|
172
|
+
* - ReturnStatement
|
|
173
|
+
* - JSXElement
|
|
174
|
+
* - JSXText
|
|
175
|
+
*
|
|
176
|
+
* This function returns:
|
|
177
|
+
*
|
|
178
|
+
* deepest matching node.
|
|
179
|
+
*
|
|
180
|
+
* WHY?
|
|
181
|
+
*
|
|
182
|
+
* Deepest node is usually most precise target.
|
|
183
|
+
*
|
|
184
|
+
* Useful in
|
|
185
|
+
* - insertVariable
|
|
186
|
+
* - updateVariable
|
|
187
|
+
* - deleteVariable
|
|
188
|
+
* - insertJSX
|
|
189
|
+
* - replaceJSX
|
|
190
|
+
* - removeJSX
|
|
191
|
+
* - wrapJSX
|
|
192
|
+
* - moveJSX
|
|
193
|
+
* - updateFunction
|
|
194
|
+
* - deleteFunction
|
|
195
|
+
* - updateComponent
|
|
196
|
+
* - deleteComponent
|
|
197
|
+
* - codeReplace
|
|
198
|
+
*
|
|
199
|
+
*
|
|
200
|
+
* High Level Primitive
|
|
201
|
+
↓
|
|
202
|
+
parseAST
|
|
203
|
+
↓
|
|
204
|
+
findNodeByLine
|
|
205
|
+
↓
|
|
206
|
+
AST modification
|
|
207
|
+
↓
|
|
208
|
+
generateCode
|
|
209
|
+
* ------------------------------------------------------------
|
|
210
|
+
* IMPORTANT LIMITATION
|
|
211
|
+
* ------------------------------------------------------------
|
|
212
|
+
*
|
|
213
|
+
* Current implementation:
|
|
214
|
+
*
|
|
215
|
+
* - returns single deepest node
|
|
216
|
+
*
|
|
217
|
+
* Future improvements:
|
|
218
|
+
*
|
|
219
|
+
* - return node path
|
|
220
|
+
* - filter node types
|
|
221
|
+
* - nearest sibling lookup
|
|
222
|
+
* - parent traversal helpers
|
|
223
|
+
*
|
|
224
|
+
* ------------------------------------------------------------
|
|
225
|
+
* PARAMS
|
|
226
|
+
* ------------------------------------------------------------
|
|
227
|
+
*
|
|
228
|
+
* @param {Object} params
|
|
229
|
+
*
|
|
230
|
+
* @param {object} params.ast
|
|
231
|
+
* Babel AST object.
|
|
232
|
+
*
|
|
233
|
+
* @param {number} params.line
|
|
234
|
+
* Target source-code line number.
|
|
235
|
+
*
|
|
236
|
+
* ------------------------------------------------------------
|
|
237
|
+
* RETURNS
|
|
238
|
+
* ------------------------------------------------------------
|
|
239
|
+
*
|
|
240
|
+
* @returns {object|null}
|
|
241
|
+
* Matching AST node or null.
|
|
242
|
+
*
|
|
243
|
+
*/
|
|
244
|
+
function findNodeByLine({ ast, line, }) {
|
|
245
|
+
// ----------------------------------------------------------
|
|
246
|
+
// STEP 1:
|
|
247
|
+
// Store best matching node
|
|
248
|
+
//
|
|
249
|
+
// Initially:
|
|
250
|
+
//
|
|
251
|
+
// no node found.
|
|
252
|
+
// ----------------------------------------------------------
|
|
253
|
+
let matchedNode = null;
|
|
254
|
+
// ----------------------------------------------------------
|
|
255
|
+
// STEP 2:
|
|
256
|
+
// Store current deepest node depth
|
|
257
|
+
//
|
|
258
|
+
// WHY?
|
|
259
|
+
//
|
|
260
|
+
// Multiple nodes may overlap same line.
|
|
261
|
+
//
|
|
262
|
+
// We prefer deepest node.
|
|
263
|
+
// ----------------------------------------------------------
|
|
264
|
+
let deepestDepth = -1;
|
|
265
|
+
// ----------------------------------------------------------
|
|
266
|
+
// STEP 3:
|
|
267
|
+
// Traverse entire AST
|
|
268
|
+
// ----------------------------------------------------------
|
|
269
|
+
traverse.default(ast, {
|
|
270
|
+
enter(path) {
|
|
271
|
+
// ------------------------------------------------------
|
|
272
|
+
// Ignore nodes without source location
|
|
273
|
+
//
|
|
274
|
+
// Some synthetic/generated nodes may not
|
|
275
|
+
// contain loc information.
|
|
276
|
+
// ------------------------------------------------------
|
|
277
|
+
if (!path.node.loc) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// ------------------------------------------------------
|
|
281
|
+
// Extract node line range
|
|
282
|
+
// ------------------------------------------------------
|
|
283
|
+
const startLine = path.node.loc.start.line;
|
|
284
|
+
const endLine = path.node.loc.end.line;
|
|
285
|
+
// ------------------------------------------------------
|
|
286
|
+
// Check whether target line exists inside
|
|
287
|
+
// current node range
|
|
288
|
+
//
|
|
289
|
+
// Example:
|
|
290
|
+
//
|
|
291
|
+
// line 5 matches:
|
|
292
|
+
//
|
|
293
|
+
// start=3
|
|
294
|
+
// end=8
|
|
295
|
+
// ------------------------------------------------------
|
|
296
|
+
const isMatching = line >= startLine && line <= endLine;
|
|
297
|
+
// ------------------------------------------------------
|
|
298
|
+
// Ignore unrelated nodes
|
|
299
|
+
// ------------------------------------------------------
|
|
300
|
+
if (!isMatching) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// ------------------------------------------------------
|
|
304
|
+
// Calculate current traversal depth
|
|
305
|
+
//
|
|
306
|
+
// WHY?
|
|
307
|
+
//
|
|
308
|
+
// Deeper nodes are usually more precise.
|
|
309
|
+
// ------------------------------------------------------
|
|
310
|
+
const currentDepth = path.getAncestry().length;
|
|
311
|
+
// ------------------------------------------------------
|
|
312
|
+
// Replace current match if:
|
|
313
|
+
//
|
|
314
|
+
// - node is deeper
|
|
315
|
+
// ------------------------------------------------------
|
|
316
|
+
if (currentDepth > deepestDepth) {
|
|
317
|
+
matchedNode = path.node;
|
|
318
|
+
deepestDepth = currentDepth;
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
// ----------------------------------------------------------
|
|
323
|
+
// STEP 4:
|
|
324
|
+
// Return best matching node
|
|
325
|
+
// ----------------------------------------------------------
|
|
326
|
+
return matchedNode;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Finds a React component in AST using component name.
|
|
330
|
+
*
|
|
331
|
+
* ------------------------------------------------------------
|
|
332
|
+
* WHAT THIS FUNCTION DOES
|
|
333
|
+
* ------------------------------------------------------------
|
|
334
|
+
*
|
|
335
|
+
* This function searches AST and returns the component node
|
|
336
|
+
* matching a specific component name.
|
|
337
|
+
*
|
|
338
|
+
* Example:
|
|
339
|
+
*
|
|
340
|
+
* CODE:
|
|
341
|
+
*
|
|
342
|
+
* function Header() {
|
|
343
|
+
* return <div>Header</div>;
|
|
344
|
+
* }
|
|
345
|
+
*
|
|
346
|
+
* Calling:
|
|
347
|
+
*
|
|
348
|
+
* findComponentInJsx({
|
|
349
|
+
* ast,
|
|
350
|
+
* componentName: "Header"
|
|
351
|
+
* })
|
|
352
|
+
*
|
|
353
|
+
* returns:
|
|
354
|
+
*
|
|
355
|
+
* FunctionDeclaration node
|
|
356
|
+
*
|
|
357
|
+
* ------------------------------------------------------------
|
|
358
|
+
* WHY THIS FUNCTION EXISTS
|
|
359
|
+
* ------------------------------------------------------------
|
|
360
|
+
*
|
|
361
|
+
* In AI-powered frontend systems,
|
|
362
|
+
* many operations target components by name.
|
|
363
|
+
*
|
|
364
|
+
* Examples:
|
|
365
|
+
*
|
|
366
|
+
* - "update Header component"
|
|
367
|
+
* - "delete Navbar component"
|
|
368
|
+
* - "wrap Sidebar component"
|
|
369
|
+
* - "extract Footer component"
|
|
370
|
+
*
|
|
371
|
+
* AST traversal alone is not enough.
|
|
372
|
+
*
|
|
373
|
+
* We need:
|
|
374
|
+
*
|
|
375
|
+
* semantic component lookup.
|
|
376
|
+
*
|
|
377
|
+
* Usefull in
|
|
378
|
+
* - updateComponent
|
|
379
|
+
* - deleteComponent
|
|
380
|
+
* - renameComponent
|
|
381
|
+
* - wrapComponent
|
|
382
|
+
* - splitComponent
|
|
383
|
+
* - mergeComponents
|
|
384
|
+
* - extractComponent
|
|
385
|
+
* - insertJSX
|
|
386
|
+
* - replaceJSX
|
|
387
|
+
* - removeJSX
|
|
388
|
+
* - moveJSX
|
|
389
|
+
|
|
390
|
+
parseAST
|
|
391
|
+
↓
|
|
392
|
+
findComponentInJsx
|
|
393
|
+
↓
|
|
394
|
+
AST modification
|
|
395
|
+
↓
|
|
396
|
+
generateCode
|
|
397
|
+
* ------------------------------------------------------------
|
|
398
|
+
* IMPORTANT ARCHITECTURAL IDEA
|
|
399
|
+
* ------------------------------------------------------------
|
|
400
|
+
*
|
|
401
|
+
* findComponentInJsx is:
|
|
402
|
+
*
|
|
403
|
+
* INTERNAL ONLY
|
|
404
|
+
*
|
|
405
|
+
* WHY?
|
|
406
|
+
*
|
|
407
|
+
* Users never directly ask:
|
|
408
|
+
*
|
|
409
|
+
* "find AST component node"
|
|
410
|
+
*
|
|
411
|
+
* Instead:
|
|
412
|
+
*
|
|
413
|
+
* higher-level primitives internally use it.
|
|
414
|
+
*
|
|
415
|
+
* ------------------------------------------------------------
|
|
416
|
+
* SUPPORTED COMPONENT TYPES
|
|
417
|
+
* ------------------------------------------------------------
|
|
418
|
+
*
|
|
419
|
+
* Current implementation supports:
|
|
420
|
+
*
|
|
421
|
+
* 1) Function declarations
|
|
422
|
+
*
|
|
423
|
+
* function Header() {}
|
|
424
|
+
*
|
|
425
|
+
* 2) Arrow function components
|
|
426
|
+
*
|
|
427
|
+
* const Header = () => {}
|
|
428
|
+
*
|
|
429
|
+
* 3) Function expression components
|
|
430
|
+
*
|
|
431
|
+
* const Header = function() {}
|
|
432
|
+
*
|
|
433
|
+
* ------------------------------------------------------------
|
|
434
|
+
* HOW COMPONENT DETECTION WORKS
|
|
435
|
+
* ------------------------------------------------------------
|
|
436
|
+
*
|
|
437
|
+
* Current heuristic:
|
|
438
|
+
*
|
|
439
|
+
* - component name must start with uppercase letter
|
|
440
|
+
* - must be function-based
|
|
441
|
+
*
|
|
442
|
+
* WHY?
|
|
443
|
+
*
|
|
444
|
+
* React components conventionally use:
|
|
445
|
+
*
|
|
446
|
+
* PascalCase
|
|
447
|
+
*
|
|
448
|
+
* ------------------------------------------------------------
|
|
449
|
+
* IMPORTANT LIMITATION
|
|
450
|
+
* ------------------------------------------------------------
|
|
451
|
+
*
|
|
452
|
+
* Current implementation:
|
|
453
|
+
*
|
|
454
|
+
* - does not verify JSX return existence
|
|
455
|
+
*
|
|
456
|
+
* Meaning:
|
|
457
|
+
*
|
|
458
|
+
* utility functions with uppercase names
|
|
459
|
+
* may also match.
|
|
460
|
+
*
|
|
461
|
+
* Future improvements:
|
|
462
|
+
*
|
|
463
|
+
* - verify JSX return
|
|
464
|
+
* - support memo()
|
|
465
|
+
* - support forwardRef()
|
|
466
|
+
* - support lazy()
|
|
467
|
+
* - support class components
|
|
468
|
+
*
|
|
469
|
+
* ------------------------------------------------------------
|
|
470
|
+
* RETURN VALUE
|
|
471
|
+
* ------------------------------------------------------------
|
|
472
|
+
*
|
|
473
|
+
* Returns:
|
|
474
|
+
*
|
|
475
|
+
* Babel NodePath
|
|
476
|
+
*
|
|
477
|
+
* instead of plain node.
|
|
478
|
+
*
|
|
479
|
+
* WHY?
|
|
480
|
+
*
|
|
481
|
+
* NodePath provides:
|
|
482
|
+
*
|
|
483
|
+
* - parent traversal
|
|
484
|
+
* - replacement APIs
|
|
485
|
+
* - removal APIs
|
|
486
|
+
* - insertion APIs
|
|
487
|
+
*
|
|
488
|
+
* which are essential for transformations.
|
|
489
|
+
*
|
|
490
|
+
* ------------------------------------------------------------
|
|
491
|
+
* PARAMS
|
|
492
|
+
* ------------------------------------------------------------
|
|
493
|
+
*
|
|
494
|
+
* @param {Object} params
|
|
495
|
+
*
|
|
496
|
+
* @param {object} params.ast
|
|
497
|
+
* Babel AST object.
|
|
498
|
+
*
|
|
499
|
+
* @param {string} params.componentName
|
|
500
|
+
* Component name to search.
|
|
501
|
+
*
|
|
502
|
+
* Example:
|
|
503
|
+
* "Header"
|
|
504
|
+
*
|
|
505
|
+
* ------------------------------------------------------------
|
|
506
|
+
* RETURNS
|
|
507
|
+
* ------------------------------------------------------------
|
|
508
|
+
*
|
|
509
|
+
* @returns {NodePath|null}
|
|
510
|
+
* Matching component path or null.
|
|
511
|
+
*
|
|
512
|
+
*/
|
|
513
|
+
function findComponentInJsx({ ast, componentName }, context) {
|
|
514
|
+
// ----------------------------------------------------------
|
|
515
|
+
// STEP 1:
|
|
516
|
+
// Store matching component path
|
|
517
|
+
//
|
|
518
|
+
// Initially:
|
|
519
|
+
//
|
|
520
|
+
// no component found.
|
|
521
|
+
// ----------------------------------------------------------
|
|
522
|
+
let matchedPath = null;
|
|
523
|
+
// ----------------------------------------------------------
|
|
524
|
+
// STEP 2:
|
|
525
|
+
// Traverse entire AST
|
|
526
|
+
// ----------------------------------------------------------
|
|
527
|
+
traverse.default(ast, {
|
|
528
|
+
// --------------------------------------------------------
|
|
529
|
+
// CASE 1:
|
|
530
|
+
// Function declaration component
|
|
531
|
+
//
|
|
532
|
+
// Example:
|
|
533
|
+
//
|
|
534
|
+
// function Header() {}
|
|
535
|
+
// --------------------------------------------------------
|
|
536
|
+
FunctionDeclaration(path) {
|
|
537
|
+
// ------------------------------------------------------
|
|
538
|
+
// Extract function name
|
|
539
|
+
// ------------------------------------------------------
|
|
540
|
+
const functionName = path.node.id?.name;
|
|
541
|
+
// ------------------------------------------------------
|
|
542
|
+
// Ignore anonymous functions
|
|
543
|
+
// ------------------------------------------------------
|
|
544
|
+
if (!functionName) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
// ------------------------------------------------------
|
|
548
|
+
// Check whether name matches target
|
|
549
|
+
// ------------------------------------------------------
|
|
550
|
+
if (functionName === componentName) {
|
|
551
|
+
matchedPath = path;
|
|
552
|
+
// ----------------------------------------------------
|
|
553
|
+
// Stop traversal once component found
|
|
554
|
+
// ----------------------------------------------------
|
|
555
|
+
path.stop();
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
// --------------------------------------------------------
|
|
559
|
+
// CASE 2:
|
|
560
|
+
// Variable declaration components
|
|
561
|
+
//
|
|
562
|
+
// Examples:
|
|
563
|
+
//
|
|
564
|
+
// const Header = () => {}
|
|
565
|
+
//
|
|
566
|
+
// const Header = function() {}
|
|
567
|
+
// --------------------------------------------------------
|
|
568
|
+
VariableDeclarator(path) {
|
|
569
|
+
// ------------------------------------------------------
|
|
570
|
+
// Extract variable name
|
|
571
|
+
// ------------------------------------------------------
|
|
572
|
+
const variableName = t.isIdentifier(path.node.id)
|
|
573
|
+
? path.node.id.name
|
|
574
|
+
: undefined;
|
|
575
|
+
// ------------------------------------------------------
|
|
576
|
+
// Ignore unrelated variables
|
|
577
|
+
// ------------------------------------------------------
|
|
578
|
+
if (variableName !== componentName) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
// ------------------------------------------------------
|
|
582
|
+
// Extract assigned value
|
|
583
|
+
// ------------------------------------------------------
|
|
584
|
+
const init = path.node.init;
|
|
585
|
+
// ------------------------------------------------------
|
|
586
|
+
// Check whether assigned value is:
|
|
587
|
+
//
|
|
588
|
+
// - ArrowFunctionExpression
|
|
589
|
+
// OR
|
|
590
|
+
// - FunctionExpression
|
|
591
|
+
// ------------------------------------------------------
|
|
592
|
+
const isFunctionComponent = t.isArrowFunctionExpression(init) || t.isFunctionExpression(init);
|
|
593
|
+
// ------------------------------------------------------
|
|
594
|
+
// Ignore non-function assignments
|
|
595
|
+
// ------------------------------------------------------
|
|
596
|
+
if (!isFunctionComponent) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
// ------------------------------------------------------
|
|
600
|
+
// Store matching path
|
|
601
|
+
// ------------------------------------------------------
|
|
602
|
+
matchedPath = path;
|
|
603
|
+
// ------------------------------------------------------
|
|
604
|
+
// Stop traversal once component found
|
|
605
|
+
// ------------------------------------------------------
|
|
606
|
+
path.stop();
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
// ----------------------------------------------------------
|
|
610
|
+
// STEP 3:
|
|
611
|
+
// Return matching component path
|
|
612
|
+
// ----------------------------------------------------------
|
|
613
|
+
return matchedPath;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Finds a JSX element inside AST.
|
|
617
|
+
*
|
|
618
|
+
* ------------------------------------------------------------
|
|
619
|
+
* WHAT THIS FUNCTION DOES
|
|
620
|
+
* ------------------------------------------------------------
|
|
621
|
+
*
|
|
622
|
+
* This function searches AST and returns a JSX element
|
|
623
|
+
* matching specific conditions.
|
|
624
|
+
*
|
|
625
|
+
* Example:
|
|
626
|
+
*
|
|
627
|
+
* CODE:
|
|
628
|
+
*
|
|
629
|
+
* <div>
|
|
630
|
+
* <Header />
|
|
631
|
+
* </div>
|
|
632
|
+
*
|
|
633
|
+
* Calling:
|
|
634
|
+
*
|
|
635
|
+
* findJSXElement({
|
|
636
|
+
* ast,
|
|
637
|
+
* elementName: "Header"
|
|
638
|
+
* })
|
|
639
|
+
*
|
|
640
|
+
* returns:
|
|
641
|
+
*
|
|
642
|
+
* JSXElement path for:
|
|
643
|
+
*
|
|
644
|
+
* <Header />
|
|
645
|
+
*
|
|
646
|
+
* ------------------------------------------------------------
|
|
647
|
+
* WHY THIS FUNCTION EXISTS
|
|
648
|
+
* ------------------------------------------------------------
|
|
649
|
+
*
|
|
650
|
+
* In AI-powered frontend systems,
|
|
651
|
+
* many UI operations target JSX elements.
|
|
652
|
+
*
|
|
653
|
+
* Examples:
|
|
654
|
+
*
|
|
655
|
+
* - "wrap button with div"
|
|
656
|
+
* - "remove Header component usage"
|
|
657
|
+
* - "move sidebar below navbar"
|
|
658
|
+
* - "replace card JSX"
|
|
659
|
+
* - "insert modal inside layout"
|
|
660
|
+
*
|
|
661
|
+
* Raw string manipulation becomes unreliable.
|
|
662
|
+
*
|
|
663
|
+
* We need:
|
|
664
|
+
*
|
|
665
|
+
* semantic JSX lookup.
|
|
666
|
+
*
|
|
667
|
+
* Useful in:
|
|
668
|
+
* - insertJSX
|
|
669
|
+
* - replaceJSX
|
|
670
|
+
* - removeJSX
|
|
671
|
+
* - wrapJSX
|
|
672
|
+
* - moveJSX
|
|
673
|
+
* - splitComponent
|
|
674
|
+
* - mergeComponents
|
|
675
|
+
* - extractComponent
|
|
676
|
+
*
|
|
677
|
+
parseAST
|
|
678
|
+
↓
|
|
679
|
+
findJSXElement
|
|
680
|
+
↓
|
|
681
|
+
JSX modification
|
|
682
|
+
↓
|
|
683
|
+
generateCode
|
|
684
|
+
* ------------------------------------------------------------
|
|
685
|
+
* IMPORTANT ARCHITECTURAL IDEA
|
|
686
|
+
* ------------------------------------------------------------
|
|
687
|
+
*
|
|
688
|
+
* findJSXElement is:
|
|
689
|
+
*
|
|
690
|
+
* INTERNAL ONLY
|
|
691
|
+
*
|
|
692
|
+
* WHY?
|
|
693
|
+
*
|
|
694
|
+
* Users never directly ask:
|
|
695
|
+
*
|
|
696
|
+
* "find JSX AST node"
|
|
697
|
+
*
|
|
698
|
+
* Instead:
|
|
699
|
+
*
|
|
700
|
+
* higher-level primitives internally use it.
|
|
701
|
+
*
|
|
702
|
+
* ------------------------------------------------------------
|
|
703
|
+
* WHAT COUNTS AS JSX ELEMENT?
|
|
704
|
+
* ------------------------------------------------------------
|
|
705
|
+
*
|
|
706
|
+
* Examples:
|
|
707
|
+
*
|
|
708
|
+
* <div />
|
|
709
|
+
* <Header />
|
|
710
|
+
* <Button></Button>
|
|
711
|
+
*
|
|
712
|
+
* All are:
|
|
713
|
+
*
|
|
714
|
+
* JSXElement nodes.
|
|
715
|
+
*
|
|
716
|
+
* ------------------------------------------------------------
|
|
717
|
+
* HOW MATCHING WORKS
|
|
718
|
+
* ------------------------------------------------------------
|
|
719
|
+
*
|
|
720
|
+
* Matching is based on:
|
|
721
|
+
*
|
|
722
|
+
* openingElement.name
|
|
723
|
+
*
|
|
724
|
+
* Example:
|
|
725
|
+
*
|
|
726
|
+
* <Header />
|
|
727
|
+
*
|
|
728
|
+
* name becomes:
|
|
729
|
+
*
|
|
730
|
+
* "Header"
|
|
731
|
+
*
|
|
732
|
+
* ------------------------------------------------------------
|
|
733
|
+
* SUPPORTED ELEMENT TYPES
|
|
734
|
+
* ------------------------------------------------------------
|
|
735
|
+
*
|
|
736
|
+
* Current implementation supports:
|
|
737
|
+
*
|
|
738
|
+
* - HTML elements
|
|
739
|
+
* - React components
|
|
740
|
+
*
|
|
741
|
+
* Examples:
|
|
742
|
+
*
|
|
743
|
+
* div
|
|
744
|
+
* span
|
|
745
|
+
* Header
|
|
746
|
+
* Sidebar
|
|
747
|
+
*
|
|
748
|
+
* ------------------------------------------------------------
|
|
749
|
+
* IMPORTANT LIMITATION
|
|
750
|
+
* ------------------------------------------------------------
|
|
751
|
+
*
|
|
752
|
+
* Current implementation:
|
|
753
|
+
*
|
|
754
|
+
* - supports simple JSX names only
|
|
755
|
+
*
|
|
756
|
+
* NOT:
|
|
757
|
+
*
|
|
758
|
+
* <UI.Header />
|
|
759
|
+
*
|
|
760
|
+
* because:
|
|
761
|
+
*
|
|
762
|
+
* JSXMemberExpression
|
|
763
|
+
* is more complex.
|
|
764
|
+
*
|
|
765
|
+
* Future improvements:
|
|
766
|
+
*
|
|
767
|
+
* - support member expressions
|
|
768
|
+
* - support nested filters
|
|
769
|
+
* - support parent matching
|
|
770
|
+
* - support attribute matching
|
|
771
|
+
* - support nth occurrence selection
|
|
772
|
+
*
|
|
773
|
+
* ------------------------------------------------------------
|
|
774
|
+
* RETURN VALUE
|
|
775
|
+
* ------------------------------------------------------------
|
|
776
|
+
*
|
|
777
|
+
* Returns:
|
|
778
|
+
*
|
|
779
|
+
* Babel NodePath
|
|
780
|
+
*
|
|
781
|
+
* instead of plain node.
|
|
782
|
+
*
|
|
783
|
+
* WHY?
|
|
784
|
+
*
|
|
785
|
+
* NodePath enables:
|
|
786
|
+
*
|
|
787
|
+
* - replaceWith()
|
|
788
|
+
* - remove()
|
|
789
|
+
* - insertBefore()
|
|
790
|
+
* - insertAfter()
|
|
791
|
+
*
|
|
792
|
+
* essential for JSX transformations.
|
|
793
|
+
*
|
|
794
|
+
* ------------------------------------------------------------
|
|
795
|
+
* FIRST MATCH STRATEGY
|
|
796
|
+
* ------------------------------------------------------------
|
|
797
|
+
*
|
|
798
|
+
* Current implementation:
|
|
799
|
+
*
|
|
800
|
+
* - returns first matching JSX element
|
|
801
|
+
*
|
|
802
|
+
* WHY?
|
|
803
|
+
*
|
|
804
|
+
* Simpler deterministic behavior.
|
|
805
|
+
*
|
|
806
|
+
* Future improvement:
|
|
807
|
+
*
|
|
808
|
+
* - return all matches
|
|
809
|
+
* - return exact occurrence
|
|
810
|
+
*
|
|
811
|
+
* ------------------------------------------------------------
|
|
812
|
+
* PARAMS
|
|
813
|
+
* ------------------------------------------------------------
|
|
814
|
+
*
|
|
815
|
+
* @param {Object} params
|
|
816
|
+
*
|
|
817
|
+
* @param {object} params.ast
|
|
818
|
+
* Babel AST object.
|
|
819
|
+
*
|
|
820
|
+
* @param {string} params.elementName
|
|
821
|
+
* JSX tag/component name.
|
|
822
|
+
*
|
|
823
|
+
* Example:
|
|
824
|
+
* "Header"
|
|
825
|
+
*
|
|
826
|
+
* ------------------------------------------------------------
|
|
827
|
+
* RETURNS
|
|
828
|
+
* ------------------------------------------------------------
|
|
829
|
+
*
|
|
830
|
+
* @returns {NodePath|null}
|
|
831
|
+
* Matching JSX path or null.
|
|
832
|
+
*
|
|
833
|
+
*/
|
|
834
|
+
function findJSXElement({ ast, elementName, }) {
|
|
835
|
+
// ----------------------------------------------------------
|
|
836
|
+
// STEP 1:
|
|
837
|
+
// Store matching JSX path
|
|
838
|
+
//
|
|
839
|
+
// Initially:
|
|
840
|
+
//
|
|
841
|
+
// no JSX element found.
|
|
842
|
+
// ----------------------------------------------------------
|
|
843
|
+
let matchedPath = null;
|
|
844
|
+
// ----------------------------------------------------------
|
|
845
|
+
// STEP 2:
|
|
846
|
+
// Traverse entire AST
|
|
847
|
+
// ----------------------------------------------------------
|
|
848
|
+
traverse.default(ast, {
|
|
849
|
+
JSXElement(path) {
|
|
850
|
+
// ------------------------------------------------------
|
|
851
|
+
// Extract opening element
|
|
852
|
+
//
|
|
853
|
+
// Example:
|
|
854
|
+
//
|
|
855
|
+
// <Header />
|
|
856
|
+
//
|
|
857
|
+
// openingElement = Header
|
|
858
|
+
// ------------------------------------------------------
|
|
859
|
+
const openingElement = path.node.openingElement;
|
|
860
|
+
// ------------------------------------------------------
|
|
861
|
+
// Extract JSX tag name node
|
|
862
|
+
// ------------------------------------------------------
|
|
863
|
+
const nameNode = openingElement.name;
|
|
864
|
+
// ------------------------------------------------------
|
|
865
|
+
// Ignore complex JSX names
|
|
866
|
+
//
|
|
867
|
+
// Example:
|
|
868
|
+
//
|
|
869
|
+
// <UI.Header />
|
|
870
|
+
//
|
|
871
|
+
// because it becomes:
|
|
872
|
+
//
|
|
873
|
+
// JSXMemberExpression
|
|
874
|
+
// ------------------------------------------------------
|
|
875
|
+
if (!t.isJSXIdentifier(nameNode)) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
// ------------------------------------------------------
|
|
879
|
+
// Extract actual JSX tag name
|
|
880
|
+
//
|
|
881
|
+
// Example:
|
|
882
|
+
//
|
|
883
|
+
// <Header />
|
|
884
|
+
//
|
|
885
|
+
// becomes:
|
|
886
|
+
//
|
|
887
|
+
// "Header"
|
|
888
|
+
// ------------------------------------------------------
|
|
889
|
+
const currentElementName = nameNode.name;
|
|
890
|
+
// ------------------------------------------------------
|
|
891
|
+
// Ignore unrelated JSX elements
|
|
892
|
+
// ------------------------------------------------------
|
|
893
|
+
if (currentElementName !== elementName) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
// ------------------------------------------------------
|
|
897
|
+
// Store matching JSX path
|
|
898
|
+
// ------------------------------------------------------
|
|
899
|
+
matchedPath = path;
|
|
900
|
+
// ------------------------------------------------------
|
|
901
|
+
// Stop traversal once found
|
|
902
|
+
// ------------------------------------------------------
|
|
903
|
+
path.stop();
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
// ----------------------------------------------------------
|
|
907
|
+
// STEP 3:
|
|
908
|
+
// Return matching JSX path
|
|
909
|
+
// ----------------------------------------------------------
|
|
910
|
+
return matchedPath;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Finds a component directory inside a project.
|
|
914
|
+
*
|
|
915
|
+
* ------------------------------------------------------------
|
|
916
|
+
* WHAT THIS FUNCTION DOES
|
|
917
|
+
* ------------------------------------------------------------
|
|
918
|
+
*
|
|
919
|
+
* This function recursively searches
|
|
920
|
+
* component directories and returns:
|
|
921
|
+
*
|
|
922
|
+
* - matching component path
|
|
923
|
+
* - component metadata
|
|
924
|
+
*
|
|
925
|
+
* Example:
|
|
926
|
+
*
|
|
927
|
+
* Searching:
|
|
928
|
+
*
|
|
929
|
+
* "Header"
|
|
930
|
+
*
|
|
931
|
+
* may return:
|
|
932
|
+
*
|
|
933
|
+
* "./src/components/layout/Header"
|
|
934
|
+
*
|
|
935
|
+
* ------------------------------------------------------------
|
|
936
|
+
* WHY THIS FUNCTION EXISTS
|
|
937
|
+
* ------------------------------------------------------------
|
|
938
|
+
*
|
|
939
|
+
* Runtime AI editing requires:
|
|
940
|
+
*
|
|
941
|
+
* deterministic component lookup.
|
|
942
|
+
*
|
|
943
|
+
* User commands usually reference:
|
|
944
|
+
*
|
|
945
|
+
* logical component names.
|
|
946
|
+
*
|
|
947
|
+
* Example:
|
|
948
|
+
*
|
|
949
|
+
* - "update header"
|
|
950
|
+
* - "make navbar red"
|
|
951
|
+
* - "extract footer"
|
|
952
|
+
*
|
|
953
|
+
* But the backend needs:
|
|
954
|
+
*
|
|
955
|
+
* actual filesystem location.
|
|
956
|
+
*
|
|
957
|
+
* This function acts as:
|
|
958
|
+
*
|
|
959
|
+
* component resolution layer.
|
|
960
|
+
*
|
|
961
|
+
* ------------------------------------------------------------
|
|
962
|
+
* IMPORTANT ARCHITECTURAL DECISION
|
|
963
|
+
* ------------------------------------------------------------
|
|
964
|
+
*
|
|
965
|
+
* Looma should NEVER rely on:
|
|
966
|
+
*
|
|
967
|
+
* fragile path guessing.
|
|
968
|
+
*
|
|
969
|
+
* Component discovery must be:
|
|
970
|
+
*
|
|
971
|
+
* - recursive
|
|
972
|
+
* - deterministic
|
|
973
|
+
* - architecture-aware
|
|
974
|
+
*
|
|
975
|
+
* Eventually this should integrate with:
|
|
976
|
+
*
|
|
977
|
+
* component registry system.
|
|
978
|
+
*
|
|
979
|
+
* ------------------------------------------------------------
|
|
980
|
+
* IMPORTANT NOTE
|
|
981
|
+
* ------------------------------------------------------------
|
|
982
|
+
*
|
|
983
|
+
* Current implementation:
|
|
984
|
+
*
|
|
985
|
+
* performs filesystem traversal.
|
|
986
|
+
*
|
|
987
|
+
* Production version should preferably use:
|
|
988
|
+
*
|
|
989
|
+
* in-memory component registry
|
|
990
|
+
*
|
|
991
|
+
* because:
|
|
992
|
+
*
|
|
993
|
+
* recursive disk traversal becomes expensive
|
|
994
|
+
* in very large projects.
|
|
995
|
+
*
|
|
996
|
+
* ------------------------------------------------------------
|
|
997
|
+
* DEPENDENCIES
|
|
998
|
+
* ------------------------------------------------------------
|
|
999
|
+
*
|
|
1000
|
+
* This function complements:
|
|
1001
|
+
*
|
|
1002
|
+
* - createComponent()
|
|
1003
|
+
* - updateComponent()
|
|
1004
|
+
* - moveComponent()
|
|
1005
|
+
* - renameComponent()
|
|
1006
|
+
* - extractComponent()
|
|
1007
|
+
* - ensureComponentStructure()
|
|
1008
|
+
* - normalizeComponent()
|
|
1009
|
+
*
|
|
1010
|
+
* ------------------------------------------------------------
|
|
1011
|
+
* WHERE THIS FUNCTION IS USEFUL
|
|
1012
|
+
* ------------------------------------------------------------
|
|
1013
|
+
*
|
|
1014
|
+
* Useful in commands like:
|
|
1015
|
+
*
|
|
1016
|
+
* - "update header"
|
|
1017
|
+
* - "delete navbar"
|
|
1018
|
+
* - "move card component"
|
|
1019
|
+
* - "extract footer"
|
|
1020
|
+
* - "rename sidebar"
|
|
1021
|
+
* - "find hero section"
|
|
1022
|
+
*
|
|
1023
|
+
* Usually executed BEFORE:
|
|
1024
|
+
*
|
|
1025
|
+
* - component mutations
|
|
1026
|
+
* - JSX mutations
|
|
1027
|
+
* - CSS synchronization
|
|
1028
|
+
*
|
|
1029
|
+
* ------------------------------------------------------------
|
|
1030
|
+
* PARAMS
|
|
1031
|
+
* ------------------------------------------------------------
|
|
1032
|
+
*
|
|
1033
|
+
* @param {Object} params
|
|
1034
|
+
*
|
|
1035
|
+
* @param {string} params.rootDirectory
|
|
1036
|
+
* Root directory to start searching from.
|
|
1037
|
+
*
|
|
1038
|
+
* Example:
|
|
1039
|
+
*
|
|
1040
|
+
* "./src/components"
|
|
1041
|
+
*
|
|
1042
|
+
* @param {string} params.componentName
|
|
1043
|
+
* Component name to find.
|
|
1044
|
+
*
|
|
1045
|
+
* Example:
|
|
1046
|
+
*
|
|
1047
|
+
* "Header"
|
|
1048
|
+
*
|
|
1049
|
+
* @param {boolean} [params.caseSensitive=false]
|
|
1050
|
+
* Whether search should be case sensitive.
|
|
1051
|
+
*
|
|
1052
|
+
* @param {boolean} [params.returnFirst=true]
|
|
1053
|
+
* Whether search should stop at first match.
|
|
1054
|
+
*
|
|
1055
|
+
* ------------------------------------------------------------
|
|
1056
|
+
* RETURNS
|
|
1057
|
+
* ------------------------------------------------------------
|
|
1058
|
+
*
|
|
1059
|
+
* @returns {Object|null}
|
|
1060
|
+
*
|
|
1061
|
+
* {
|
|
1062
|
+
* componentName,
|
|
1063
|
+
* componentPath,
|
|
1064
|
+
* jsxPath,
|
|
1065
|
+
* cssPath,
|
|
1066
|
+
* exists
|
|
1067
|
+
* }
|
|
1068
|
+
*
|
|
1069
|
+
* OR
|
|
1070
|
+
*
|
|
1071
|
+
* null
|
|
1072
|
+
*
|
|
1073
|
+
*/
|
|
1074
|
+
function findComponentDirectory({
|
|
1075
|
+
// rootDirectory,
|
|
1076
|
+
componentName, caseSensitive = false, returnFirst = true, }, context) {
|
|
1077
|
+
// ----------------------------------------------------------
|
|
1078
|
+
// STEP 1:
|
|
1079
|
+
// Resolve absolute root directory
|
|
1080
|
+
// ----------------------------------------------------------
|
|
1081
|
+
const absoluteRootDirectory = context.project.root;
|
|
1082
|
+
// ----------------------------------------------------------
|
|
1083
|
+
// STEP 2:
|
|
1084
|
+
// Validate root directory existence
|
|
1085
|
+
// ----------------------------------------------------------
|
|
1086
|
+
if (!fs.existsSync(absoluteRootDirectory)) {
|
|
1087
|
+
throw new LoomaError(ERROR_CODES.DIRECTORY_NOT_FOUND, `Root directory does not exist: ${absoluteRootDirectory}`, {
|
|
1088
|
+
payload: {
|
|
1089
|
+
componentName,
|
|
1090
|
+
caseSensitive: false,
|
|
1091
|
+
returnFirst: true,
|
|
1092
|
+
},
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
// ----------------------------------------------------------
|
|
1096
|
+
// STEP 3:
|
|
1097
|
+
// Store search result
|
|
1098
|
+
// ----------------------------------------------------------
|
|
1099
|
+
let foundComponent = null;
|
|
1100
|
+
// ----------------------------------------------------------
|
|
1101
|
+
// STEP 4:
|
|
1102
|
+
// Recursive directory traversal function
|
|
1103
|
+
// ----------------------------------------------------------
|
|
1104
|
+
function traverseDirectory(currentDirectory) {
|
|
1105
|
+
// --------------------------------------------------------
|
|
1106
|
+
// Stop traversal if component already found
|
|
1107
|
+
// --------------------------------------------------------
|
|
1108
|
+
if (foundComponent && returnFirst) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
// --------------------------------------------------------
|
|
1112
|
+
// Read current directory entries
|
|
1113
|
+
// --------------------------------------------------------
|
|
1114
|
+
const entries = fs.readdirSync(currentDirectory, {
|
|
1115
|
+
withFileTypes: true,
|
|
1116
|
+
});
|
|
1117
|
+
// --------------------------------------------------------
|
|
1118
|
+
// Traverse directory entries
|
|
1119
|
+
// --------------------------------------------------------
|
|
1120
|
+
for (const entry of entries) {
|
|
1121
|
+
// ------------------------------------------------------
|
|
1122
|
+
// Build absolute entry path
|
|
1123
|
+
// ------------------------------------------------------
|
|
1124
|
+
const entryPath = path.join(currentDirectory, entry.name);
|
|
1125
|
+
// ------------------------------------------------------
|
|
1126
|
+
// Skip non-directories
|
|
1127
|
+
// ------------------------------------------------------
|
|
1128
|
+
if (!entry.isDirectory()) {
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
// ------------------------------------------------------
|
|
1132
|
+
// Normalize names for comparison
|
|
1133
|
+
// ------------------------------------------------------
|
|
1134
|
+
const normalizedEntryName = caseSensitive
|
|
1135
|
+
? entry.name
|
|
1136
|
+
: entry.name.toLowerCase();
|
|
1137
|
+
const normalizedTargetName = caseSensitive
|
|
1138
|
+
? componentName
|
|
1139
|
+
: componentName.toLowerCase();
|
|
1140
|
+
// ------------------------------------------------------
|
|
1141
|
+
// Check component match
|
|
1142
|
+
// ------------------------------------------------------
|
|
1143
|
+
if (normalizedEntryName === normalizedTargetName) {
|
|
1144
|
+
// ----------------------------------------------------
|
|
1145
|
+
// Build potential JSX file path
|
|
1146
|
+
// ----------------------------------------------------
|
|
1147
|
+
const jsxPath = path.join(entryPath, `${entry.name}.jsx`);
|
|
1148
|
+
// ----------------------------------------------------
|
|
1149
|
+
// Build potential CSS file path
|
|
1150
|
+
// ----------------------------------------------------
|
|
1151
|
+
const cssPath = path.join(entryPath, `${entry.name}.css`);
|
|
1152
|
+
// ----------------------------------------------------
|
|
1153
|
+
// Store component metadata
|
|
1154
|
+
// ----------------------------------------------------
|
|
1155
|
+
foundComponent = {
|
|
1156
|
+
componentName: entry.name,
|
|
1157
|
+
componentPath: entryPath,
|
|
1158
|
+
jsxPath,
|
|
1159
|
+
cssPath,
|
|
1160
|
+
exists: true,
|
|
1161
|
+
};
|
|
1162
|
+
// ----------------------------------------------------
|
|
1163
|
+
// Stop traversal if configured
|
|
1164
|
+
// ----------------------------------------------------
|
|
1165
|
+
if (returnFirst) {
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
// ------------------------------------------------------
|
|
1170
|
+
// Continue recursive traversal
|
|
1171
|
+
// ------------------------------------------------------
|
|
1172
|
+
traverseDirectory(entryPath);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
// ----------------------------------------------------------
|
|
1176
|
+
// STEP 5:
|
|
1177
|
+
// Start recursive traversal
|
|
1178
|
+
// ----------------------------------------------------------
|
|
1179
|
+
traverseDirectory(absoluteRootDirectory);
|
|
1180
|
+
// ----------------------------------------------------------
|
|
1181
|
+
// STEP 6:
|
|
1182
|
+
// Return found component
|
|
1183
|
+
// ----------------------------------------------------------
|
|
1184
|
+
if (!foundComponent) {
|
|
1185
|
+
throw new LoomaError(ERROR_CODES.COMPONENT_NOT_FOUND, `Component ${componentName} not found`);
|
|
1186
|
+
}
|
|
1187
|
+
return { success: true, foundComponent };
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Finds CSS selectors inside a parsed CSS AST.
|
|
1191
|
+
*
|
|
1192
|
+
* ------------------------------------------------------------
|
|
1193
|
+
* WHAT THIS FUNCTION DOES
|
|
1194
|
+
* ------------------------------------------------------------
|
|
1195
|
+
*
|
|
1196
|
+
* This function searches CSS AST
|
|
1197
|
+
* and returns matching selectors.
|
|
1198
|
+
*
|
|
1199
|
+
* Example:
|
|
1200
|
+
*
|
|
1201
|
+
* CSS:
|
|
1202
|
+
*
|
|
1203
|
+
* .header {
|
|
1204
|
+
* color: red;
|
|
1205
|
+
* }
|
|
1206
|
+
*
|
|
1207
|
+
* .header__title {
|
|
1208
|
+
* font-size: 20px;
|
|
1209
|
+
* }
|
|
1210
|
+
*
|
|
1211
|
+
* Searching:
|
|
1212
|
+
*
|
|
1213
|
+
* ".header"
|
|
1214
|
+
*
|
|
1215
|
+
* returns:
|
|
1216
|
+
*
|
|
1217
|
+
* matching PostCSS rule nodes.
|
|
1218
|
+
*
|
|
1219
|
+
* ------------------------------------------------------------
|
|
1220
|
+
* WHY THIS FUNCTION EXISTS
|
|
1221
|
+
* ------------------------------------------------------------
|
|
1222
|
+
*
|
|
1223
|
+
* Most advanced CSS operations require:
|
|
1224
|
+
*
|
|
1225
|
+
* locating selectors safely.
|
|
1226
|
+
*
|
|
1227
|
+
* Example operations:
|
|
1228
|
+
*
|
|
1229
|
+
* - update selector styles
|
|
1230
|
+
* - remove selector
|
|
1231
|
+
* - rename selector
|
|
1232
|
+
* - analyze declarations
|
|
1233
|
+
* - optimize css
|
|
1234
|
+
*
|
|
1235
|
+
* Regex becomes unreliable
|
|
1236
|
+
* for large CSS systems.
|
|
1237
|
+
*
|
|
1238
|
+
* AST selector traversal is safer.
|
|
1239
|
+
*
|
|
1240
|
+
* ------------------------------------------------------------
|
|
1241
|
+
* IMPORTANT ARCHITECTURAL DECISION
|
|
1242
|
+
* ------------------------------------------------------------
|
|
1243
|
+
*
|
|
1244
|
+
* This function works on:
|
|
1245
|
+
*
|
|
1246
|
+
* CSS AST
|
|
1247
|
+
*
|
|
1248
|
+
* NOT raw css strings.
|
|
1249
|
+
*
|
|
1250
|
+
* This enables:
|
|
1251
|
+
*
|
|
1252
|
+
* deterministic CSS tooling.
|
|
1253
|
+
*
|
|
1254
|
+
* ------------------------------------------------------------
|
|
1255
|
+
* DEPENDENCIES
|
|
1256
|
+
* ------------------------------------------------------------
|
|
1257
|
+
*
|
|
1258
|
+
* This function assumes:
|
|
1259
|
+
*
|
|
1260
|
+
* - parseCSS()
|
|
1261
|
+
*
|
|
1262
|
+
* already exists.
|
|
1263
|
+
*
|
|
1264
|
+
* It complements:
|
|
1265
|
+
*
|
|
1266
|
+
* - updateStyles()
|
|
1267
|
+
* - removeStyles()
|
|
1268
|
+
* - renameCssClass()
|
|
1269
|
+
* - syncComponentStyles()
|
|
1270
|
+
*
|
|
1271
|
+
* ------------------------------------------------------------
|
|
1272
|
+
* WHERE THIS FUNCTION IS USEFUL
|
|
1273
|
+
* ------------------------------------------------------------
|
|
1274
|
+
*
|
|
1275
|
+
* Useful in commands like:
|
|
1276
|
+
*
|
|
1277
|
+
* - "make button red"
|
|
1278
|
+
* - "remove card shadow"
|
|
1279
|
+
* - "rename css class"
|
|
1280
|
+
* - "find responsive styles"
|
|
1281
|
+
* - "update navbar styles"
|
|
1282
|
+
* - "cleanup unused css"
|
|
1283
|
+
*
|
|
1284
|
+
* Usually executed BEFORE:
|
|
1285
|
+
*
|
|
1286
|
+
* - css mutations
|
|
1287
|
+
* - selector updates
|
|
1288
|
+
* - declaration analysis
|
|
1289
|
+
*
|
|
1290
|
+
* Example usage:
|
|
1291
|
+
|
|
1292
|
+
const { ast } = parseCSS({
|
|
1293
|
+
cssPath: "./Header.css",
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
const matches =
|
|
1297
|
+
findCssSelector({
|
|
1298
|
+
ast,
|
|
1299
|
+
selector: ".header",
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
console.log(matches);
|
|
1303
|
+
|
|
1304
|
+
or
|
|
1305
|
+
|
|
1306
|
+
findCssSelector({
|
|
1307
|
+
ast,
|
|
1308
|
+
selector: /^\.header/,
|
|
1309
|
+
});
|
|
1310
|
+
* ------------------------------------------------------------
|
|
1311
|
+
* IMPORTANT NOTE
|
|
1312
|
+
* ------------------------------------------------------------
|
|
1313
|
+
*
|
|
1314
|
+
* This function ONLY finds selectors.
|
|
1315
|
+
*
|
|
1316
|
+
* It does NOT:
|
|
1317
|
+
*
|
|
1318
|
+
* - modify css
|
|
1319
|
+
* - remove selectors
|
|
1320
|
+
* - insert styles
|
|
1321
|
+
* - write files
|
|
1322
|
+
*
|
|
1323
|
+
* ------------------------------------------------------------
|
|
1324
|
+
* REQUIRED PACKAGE
|
|
1325
|
+
* ------------------------------------------------------------
|
|
1326
|
+
*
|
|
1327
|
+
* npm install postcss
|
|
1328
|
+
*
|
|
1329
|
+
* ------------------------------------------------------------
|
|
1330
|
+
* PARAMS
|
|
1331
|
+
* ------------------------------------------------------------
|
|
1332
|
+
*
|
|
1333
|
+
* @param {Object} params
|
|
1334
|
+
*
|
|
1335
|
+
* @param {Object} params.ast
|
|
1336
|
+
* Parsed PostCSS AST.
|
|
1337
|
+
*
|
|
1338
|
+
* Usually returned from:
|
|
1339
|
+
*
|
|
1340
|
+
* parseCSS()
|
|
1341
|
+
*
|
|
1342
|
+
* @param {string|RegExp} params.selector
|
|
1343
|
+
* Selector to search.
|
|
1344
|
+
*
|
|
1345
|
+
* Example:
|
|
1346
|
+
*
|
|
1347
|
+
* ".header"
|
|
1348
|
+
*
|
|
1349
|
+
* OR
|
|
1350
|
+
*
|
|
1351
|
+
* /^\.header/
|
|
1352
|
+
*
|
|
1353
|
+
* @param {boolean} [params.findAll=true]
|
|
1354
|
+
* Whether to return all matches.
|
|
1355
|
+
*
|
|
1356
|
+
* ------------------------------------------------------------
|
|
1357
|
+
* RETURNS
|
|
1358
|
+
* ------------------------------------------------------------
|
|
1359
|
+
*
|
|
1360
|
+
* @returns {Object[]}
|
|
1361
|
+
*
|
|
1362
|
+
* Array of matching PostCSS rule nodes.
|
|
1363
|
+
*
|
|
1364
|
+
*/
|
|
1365
|
+
function findCssSelector({ ast, selector, findAll = true, }) {
|
|
1366
|
+
// ----------------------------------------------------------
|
|
1367
|
+
// STEP 1:
|
|
1368
|
+
// Validate AST existence
|
|
1369
|
+
// ----------------------------------------------------------
|
|
1370
|
+
if (!ast) {
|
|
1371
|
+
throw new LoomaError(ERROR_CODES.INTERNAL_ERROR, "CSS AST is required");
|
|
1372
|
+
}
|
|
1373
|
+
// ----------------------------------------------------------
|
|
1374
|
+
// STEP 2:
|
|
1375
|
+
// Store matching selectors
|
|
1376
|
+
// ----------------------------------------------------------
|
|
1377
|
+
const matches = [];
|
|
1378
|
+
// ----------------------------------------------------------
|
|
1379
|
+
// STEP 3:
|
|
1380
|
+
// Traverse all CSS rules
|
|
1381
|
+
//
|
|
1382
|
+
// Example:
|
|
1383
|
+
//
|
|
1384
|
+
// .header {}
|
|
1385
|
+
// .card {}
|
|
1386
|
+
// ----------------------------------------------------------
|
|
1387
|
+
ast.walkRules((rule) => {
|
|
1388
|
+
// --------------------------------------------------------
|
|
1389
|
+
// Extract selector text
|
|
1390
|
+
//
|
|
1391
|
+
// Example:
|
|
1392
|
+
//
|
|
1393
|
+
// ".header"
|
|
1394
|
+
// --------------------------------------------------------
|
|
1395
|
+
const ruleSelector = rule.selector;
|
|
1396
|
+
// --------------------------------------------------------
|
|
1397
|
+
// Handle string selector search
|
|
1398
|
+
// --------------------------------------------------------
|
|
1399
|
+
if (typeof selector === "string") {
|
|
1400
|
+
// ------------------------------------------------------
|
|
1401
|
+
// Exact selector match
|
|
1402
|
+
// ------------------------------------------------------
|
|
1403
|
+
if (ruleSelector === selector) {
|
|
1404
|
+
matches.push(rule);
|
|
1405
|
+
// ----------------------------------------------------
|
|
1406
|
+
// Stop early if only first match needed
|
|
1407
|
+
// ----------------------------------------------------
|
|
1408
|
+
if (!findAll) {
|
|
1409
|
+
return false;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
// --------------------------------------------------------
|
|
1414
|
+
// Handle RegExp selector search
|
|
1415
|
+
// --------------------------------------------------------
|
|
1416
|
+
else if (selector instanceof RegExp) {
|
|
1417
|
+
// ------------------------------------------------------
|
|
1418
|
+
// Match regex against selector
|
|
1419
|
+
// ------------------------------------------------------
|
|
1420
|
+
if (selector.test(ruleSelector)) {
|
|
1421
|
+
matches.push(rule);
|
|
1422
|
+
// ----------------------------------------------------
|
|
1423
|
+
// Stop early if only first match needed
|
|
1424
|
+
// ----------------------------------------------------
|
|
1425
|
+
if (!findAll) {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
// ----------------------------------------------------------
|
|
1432
|
+
// STEP 4:
|
|
1433
|
+
// Return matching selectors
|
|
1434
|
+
// ----------------------------------------------------------
|
|
1435
|
+
if (matches.length === 0) {
|
|
1436
|
+
return { success: false, message: `Selector ${selector} not found` };
|
|
1437
|
+
}
|
|
1438
|
+
return { success: true, matches };
|
|
1439
|
+
}
|
|
1440
|
+
export default {
|
|
1441
|
+
// insertCode,
|
|
1442
|
+
// expose only when planner becomes mature enough to use it directly
|
|
1443
|
+
// ensureImport,
|
|
1444
|
+
// removeImport,
|
|
1445
|
+
// enrichImport,
|
|
1446
|
+
// optimizeImports,
|
|
1447
|
+
// updateComponentImports,
|
|
1448
|
+
findNodeByLine,
|
|
1449
|
+
findComponentInJsx,
|
|
1450
|
+
findJSXElement,
|
|
1451
|
+
findComponentDirectory,
|
|
1452
|
+
findCssSelector,
|
|
1453
|
+
};
|
|
1454
|
+
//# sourceMappingURL=finders.js.map
|