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,3736 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
Helper tasks: Discovery, parsing,
|
|
4
|
+
normalization, analysis, and other utilities that support the main tasks.
|
|
5
|
+
------------
|
|
6
|
+
ensureLibrary
|
|
7
|
+
ensureImport
|
|
8
|
+
removeImport
|
|
9
|
+
enrichImport
|
|
10
|
+
optimizeImports
|
|
11
|
+
resolveImportConflicts
|
|
12
|
+
findComponentDirectory
|
|
13
|
+
findNodeByLine
|
|
14
|
+
findComponentByName
|
|
15
|
+
findJSXElement
|
|
16
|
+
ensureComponentStructure
|
|
17
|
+
updateComponentImports
|
|
18
|
+
normalizeComponent
|
|
19
|
+
inferComponentName
|
|
20
|
+
ensureStyleFile
|
|
21
|
+
renameCssClass
|
|
22
|
+
generateClassNames
|
|
23
|
+
syncComponentStyles
|
|
24
|
+
findCssSelector
|
|
25
|
+
resolveCssClassConflicts
|
|
26
|
+
resolveStyleDependencies
|
|
27
|
+
|
|
28
|
+
*/
|
|
29
|
+
import fs from "fs";
|
|
30
|
+
import path from "path";
|
|
31
|
+
import traverse from "@babel/traverse";
|
|
32
|
+
import { parse } from "@babel/parser";
|
|
33
|
+
import { generate } from "@babel/generator";
|
|
34
|
+
import t from "@babel/types";
|
|
35
|
+
import postcss from "postcss";
|
|
36
|
+
import crypto from "crypto";
|
|
37
|
+
import { createSyncFn } from "synckit";
|
|
38
|
+
import { ERROR_CODES, TaskRegistry } from "../../schemas/index.js";
|
|
39
|
+
import parsers from "./parsers.js";
|
|
40
|
+
import { LoomaError } from "../../server/error.js";
|
|
41
|
+
import { getInitializationContext } from "../../server/app-context.js";
|
|
42
|
+
// function informUser({ message }) {
|
|
43
|
+
// sse.emitInfo(message);
|
|
44
|
+
// }
|
|
45
|
+
/**
|
|
46
|
+
* Removes a library from package.json.
|
|
47
|
+
*
|
|
48
|
+
* ------------------------------------------------------------
|
|
49
|
+
* WHAT THIS FUNCTION DOES
|
|
50
|
+
* ------------------------------------------------------------
|
|
51
|
+
*
|
|
52
|
+
* This function removes a package from:
|
|
53
|
+
*
|
|
54
|
+
* - dependencies
|
|
55
|
+
* OR
|
|
56
|
+
* - devDependencies
|
|
57
|
+
*
|
|
58
|
+
* If the package does not exist:
|
|
59
|
+
* - nothing changes
|
|
60
|
+
*
|
|
61
|
+
* ------------------------------------------------------------
|
|
62
|
+
* IMPORTANT NOTE
|
|
63
|
+
* ------------------------------------------------------------
|
|
64
|
+
*
|
|
65
|
+
* This function ONLY updates package.json.
|
|
66
|
+
*
|
|
67
|
+
* It DOES NOT:
|
|
68
|
+
* - run npm uninstall
|
|
69
|
+
* - remove node_modules
|
|
70
|
+
* - remove imports from source files
|
|
71
|
+
*
|
|
72
|
+
* Those responsibilities should remain separate.
|
|
73
|
+
*
|
|
74
|
+
*
|
|
75
|
+
* Useful commands:
|
|
76
|
+
* - remove redux: removeLibrary("@reduxjs/toolkit")
|
|
77
|
+
* - remove tailwind: removeLibrary("tailwindcss")
|
|
78
|
+
* - remove charts: removeLibrary("recharts")
|
|
79
|
+
* - remove icons package: removeLibrary("lucide-react")
|
|
80
|
+
* - stop using router: removeLibrary("react-router-dom")
|
|
81
|
+
*
|
|
82
|
+
* ------------------------------------------------------------
|
|
83
|
+
* WHY THIS SEPARATION IS IMPORTANT
|
|
84
|
+
* ------------------------------------------------------------
|
|
85
|
+
*
|
|
86
|
+
* Dependency management and source-code cleanup are different
|
|
87
|
+
* architectural concerns.
|
|
88
|
+
*
|
|
89
|
+
* Example:
|
|
90
|
+
*
|
|
91
|
+
* removeLibrary()
|
|
92
|
+
* -> package.json mutation
|
|
93
|
+
*
|
|
94
|
+
* removeImport()
|
|
95
|
+
* -> source code mutation
|
|
96
|
+
*
|
|
97
|
+
* optimizeImports()
|
|
98
|
+
* -> dead import cleanup
|
|
99
|
+
*
|
|
100
|
+
* ------------------------------------------------------------
|
|
101
|
+
* EXAMPLE
|
|
102
|
+
* ------------------------------------------------------------
|
|
103
|
+
*
|
|
104
|
+
* removeLibrary({
|
|
105
|
+
* projectPath: "/my-app",
|
|
106
|
+
* libraryName: "lodash"
|
|
107
|
+
* });
|
|
108
|
+
*
|
|
109
|
+
* ------------------------------------------------------------
|
|
110
|
+
* PARAMS
|
|
111
|
+
* ------------------------------------------------------------
|
|
112
|
+
*
|
|
113
|
+
* @param {Object} params
|
|
114
|
+
*
|
|
115
|
+
* @param {string} params.projectPath
|
|
116
|
+
* Path to project root directory.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} params.libraryName
|
|
119
|
+
* Name of package to remove.
|
|
120
|
+
*
|
|
121
|
+
* ------------------------------------------------------------
|
|
122
|
+
* RETURNS
|
|
123
|
+
* ------------------------------------------------------------
|
|
124
|
+
*
|
|
125
|
+
* @returns {{
|
|
126
|
+
* modified: boolean,
|
|
127
|
+
* removedFrom: string|null,
|
|
128
|
+
* packageJson: Object
|
|
129
|
+
* }}
|
|
130
|
+
*
|
|
131
|
+
* modified:
|
|
132
|
+
* true -> package removed
|
|
133
|
+
* false -> package not found
|
|
134
|
+
*
|
|
135
|
+
* removedFrom:
|
|
136
|
+
* "dependencies"
|
|
137
|
+
* "devDependencies"
|
|
138
|
+
* null
|
|
139
|
+
*
|
|
140
|
+
*/
|
|
141
|
+
// function removeLibrary({ projectPath, libraryName }) {
|
|
142
|
+
// // ----------------------------------------------------------
|
|
143
|
+
// // STEP 1:
|
|
144
|
+
// // Build package.json absolute path
|
|
145
|
+
// // ----------------------------------------------------------
|
|
146
|
+
// const packageJsonPath = path.join(projectPath, "package.json");
|
|
147
|
+
// // ----------------------------------------------------------
|
|
148
|
+
// // STEP 2:
|
|
149
|
+
// // Ensure package.json exists
|
|
150
|
+
// // ----------------------------------------------------------
|
|
151
|
+
// if (!fs.existsSync(packageJsonPath)) {
|
|
152
|
+
// throw new Error(`package.json not found at: ${packageJsonPath}`);
|
|
153
|
+
// }
|
|
154
|
+
// // ----------------------------------------------------------
|
|
155
|
+
// // STEP 3:
|
|
156
|
+
// // Read package.json as text
|
|
157
|
+
// // ----------------------------------------------------------
|
|
158
|
+
// const packageJsonContent = fs.readFileSync(packageJsonPath, "utf-8");
|
|
159
|
+
// // ----------------------------------------------------------
|
|
160
|
+
// // STEP 4:
|
|
161
|
+
// // Convert JSON string into JS object
|
|
162
|
+
// // ----------------------------------------------------------
|
|
163
|
+
// const packageJson = JSON.parse(packageJsonContent);
|
|
164
|
+
// // ----------------------------------------------------------
|
|
165
|
+
// // STEP 5:
|
|
166
|
+
// // Track where library was removed from
|
|
167
|
+
// // ----------------------------------------------------------
|
|
168
|
+
// let removedFrom = null;
|
|
169
|
+
// // ----------------------------------------------------------
|
|
170
|
+
// // STEP 6:
|
|
171
|
+
// // Check dependencies
|
|
172
|
+
// //
|
|
173
|
+
// // Example:
|
|
174
|
+
// //
|
|
175
|
+
// // {
|
|
176
|
+
// // dependencies: {
|
|
177
|
+
// // react: "^19.0.0"
|
|
178
|
+
// // }
|
|
179
|
+
// // }
|
|
180
|
+
// // ----------------------------------------------------------
|
|
181
|
+
// if (packageJson.dependencies && packageJson.dependencies[libraryName]) {
|
|
182
|
+
// // --------------------------------------------------------
|
|
183
|
+
// // Remove package
|
|
184
|
+
// // --------------------------------------------------------
|
|
185
|
+
// delete packageJson.dependencies[libraryName];
|
|
186
|
+
// // --------------------------------------------------------
|
|
187
|
+
// // Track removal source
|
|
188
|
+
// // --------------------------------------------------------
|
|
189
|
+
// removedFrom = "dependencies";
|
|
190
|
+
// }
|
|
191
|
+
// // ----------------------------------------------------------
|
|
192
|
+
// // STEP 7:
|
|
193
|
+
// // Check devDependencies
|
|
194
|
+
// // ----------------------------------------------------------
|
|
195
|
+
// if (packageJson.devDependencies && packageJson.devDependencies[libraryName]) {
|
|
196
|
+
// // --------------------------------------------------------
|
|
197
|
+
// // Remove package
|
|
198
|
+
// // --------------------------------------------------------
|
|
199
|
+
// delete packageJson.devDependencies[libraryName];
|
|
200
|
+
// // --------------------------------------------------------
|
|
201
|
+
// // Track removal source
|
|
202
|
+
// // --------------------------------------------------------
|
|
203
|
+
// removedFrom = "devDependencies";
|
|
204
|
+
// }
|
|
205
|
+
// // ----------------------------------------------------------
|
|
206
|
+
// // STEP 8:
|
|
207
|
+
// // If package was not found:
|
|
208
|
+
// // return unchanged
|
|
209
|
+
// // ----------------------------------------------------------
|
|
210
|
+
// if (!removedFrom) {
|
|
211
|
+
// return {
|
|
212
|
+
// modified: false,
|
|
213
|
+
// removedFrom: null,
|
|
214
|
+
// packageJson,
|
|
215
|
+
// };
|
|
216
|
+
// }
|
|
217
|
+
// // ----------------------------------------------------------
|
|
218
|
+
// // STEP 9:
|
|
219
|
+
// // Sort dependencies alphabetically
|
|
220
|
+
// //
|
|
221
|
+
// // WHY?
|
|
222
|
+
// //
|
|
223
|
+
// // Stable ordering:
|
|
224
|
+
// // - cleaner git diffs
|
|
225
|
+
// // - deterministic formatting
|
|
226
|
+
// // ----------------------------------------------------------
|
|
227
|
+
// if (packageJson.dependencies) {
|
|
228
|
+
// packageJson.dependencies = Object.fromEntries(
|
|
229
|
+
// Object.entries(packageJson.dependencies).sort(([a], [b]) =>
|
|
230
|
+
// a.localeCompare(b)
|
|
231
|
+
// )
|
|
232
|
+
// );
|
|
233
|
+
// }
|
|
234
|
+
// // ----------------------------------------------------------
|
|
235
|
+
// // STEP 10:
|
|
236
|
+
// // Sort devDependencies alphabetically
|
|
237
|
+
// // ----------------------------------------------------------
|
|
238
|
+
// if (packageJson.devDependencies) {
|
|
239
|
+
// packageJson.devDependencies = Object.fromEntries(
|
|
240
|
+
// Object.entries(packageJson.devDependencies).sort(([a], [b]) =>
|
|
241
|
+
// a.localeCompare(b)
|
|
242
|
+
// )
|
|
243
|
+
// );
|
|
244
|
+
// }
|
|
245
|
+
// // ----------------------------------------------------------
|
|
246
|
+
// // STEP 11:
|
|
247
|
+
// // Convert JS object back into formatted JSON string
|
|
248
|
+
// // ----------------------------------------------------------
|
|
249
|
+
// const updatedContent = JSON.stringify(packageJson, null, 2);
|
|
250
|
+
// // ----------------------------------------------------------
|
|
251
|
+
// // STEP 12:
|
|
252
|
+
// // Write updated package.json back to disk
|
|
253
|
+
// // ----------------------------------------------------------
|
|
254
|
+
// fs.writeFileSync(packageJsonPath, `${updatedContent}\n`, "utf-8");
|
|
255
|
+
// // ----------------------------------------------------------
|
|
256
|
+
// // STEP 13:
|
|
257
|
+
// // Return success metadata
|
|
258
|
+
// // ----------------------------------------------------------
|
|
259
|
+
// return {
|
|
260
|
+
// modified: true,
|
|
261
|
+
// removedFrom,
|
|
262
|
+
// packageJson,
|
|
263
|
+
// };
|
|
264
|
+
// }
|
|
265
|
+
/**
|
|
266
|
+
* this function updates the version of a library in package.json. it looks for the library in all dependency sections and updates the version. if the library is not found, it throws an error.
|
|
267
|
+
* @param pkgPath
|
|
268
|
+
* @param param1
|
|
269
|
+
*/
|
|
270
|
+
// function updateLibraryVersion({ pkgPath, packageName, newVersion }) {
|
|
271
|
+
// const pkgRaw = fs.readFileSync(pkgPath, "utf-8");
|
|
272
|
+
// const pkg = JSON.parse(pkgRaw);
|
|
273
|
+
// let updated = false;
|
|
274
|
+
// const sections = [
|
|
275
|
+
// "dependencies",
|
|
276
|
+
// "devDependencies",
|
|
277
|
+
// "peerDependencies",
|
|
278
|
+
// "optionalDependencies",
|
|
279
|
+
// ];
|
|
280
|
+
// for (const section of sections) {
|
|
281
|
+
// if (pkg[section] && pkg[section][packageName]) {
|
|
282
|
+
// pkg[section][packageName] = newVersion;
|
|
283
|
+
// updated = true;
|
|
284
|
+
// }
|
|
285
|
+
// }
|
|
286
|
+
// if (!updated) {
|
|
287
|
+
// throw new Error(`Package "${packageName}" not found in dependencies`);
|
|
288
|
+
// }
|
|
289
|
+
// fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
290
|
+
// }
|
|
291
|
+
/**
|
|
292
|
+
* Resolves import naming conflicts inside a file.
|
|
293
|
+
*
|
|
294
|
+
* ------------------------------------------------------------
|
|
295
|
+
* WHAT THIS FUNCTION DOES
|
|
296
|
+
* ------------------------------------------------------------
|
|
297
|
+
*
|
|
298
|
+
* This function detects conflicting import identifiers
|
|
299
|
+
* and automatically renames imports safely.
|
|
300
|
+
*
|
|
301
|
+
* Example:
|
|
302
|
+
*
|
|
303
|
+
* BEFORE:
|
|
304
|
+
*
|
|
305
|
+
* import Button from "./Button";
|
|
306
|
+
* import Button from "./UI/Button";
|
|
307
|
+
*
|
|
308
|
+
* AFTER:
|
|
309
|
+
*
|
|
310
|
+
* import Button from "./Button";
|
|
311
|
+
* import UIButton from "./UI/Button";
|
|
312
|
+
*
|
|
313
|
+
* ------------------------------------------------------------
|
|
314
|
+
* WHY THIS FUNCTION EXISTS
|
|
315
|
+
* ------------------------------------------------------------
|
|
316
|
+
*
|
|
317
|
+
* In AI-powered frontend systems,
|
|
318
|
+
* imports are often generated dynamically.
|
|
319
|
+
*
|
|
320
|
+
* Example:
|
|
321
|
+
*
|
|
322
|
+
* User says:
|
|
323
|
+
*
|
|
324
|
+
* "add button component"
|
|
325
|
+
*
|
|
326
|
+
* LLM may generate:
|
|
327
|
+
*
|
|
328
|
+
* import Button from "./Button";
|
|
329
|
+
*
|
|
330
|
+
* But:
|
|
331
|
+
*
|
|
332
|
+
* Button may already exist.
|
|
333
|
+
*
|
|
334
|
+
* This creates:
|
|
335
|
+
*
|
|
336
|
+
* duplicate identifier errors.
|
|
337
|
+
*
|
|
338
|
+
* ------------------------------------------------------------
|
|
339
|
+
* WHAT IS IMPORT CONFLICT?
|
|
340
|
+
* ------------------------------------------------------------
|
|
341
|
+
*
|
|
342
|
+
* Conflict occurs when:
|
|
343
|
+
*
|
|
344
|
+
* two imports introduce same variable name.
|
|
345
|
+
*
|
|
346
|
+
* Example:
|
|
347
|
+
*
|
|
348
|
+
* import Card from "./Card";
|
|
349
|
+
* import Card from "./UI/Card";
|
|
350
|
+
*
|
|
351
|
+
* Both define:
|
|
352
|
+
*
|
|
353
|
+
* Card
|
|
354
|
+
*
|
|
355
|
+
* in same scope.
|
|
356
|
+
*
|
|
357
|
+
* Useful in:
|
|
358
|
+
* - ensureImport
|
|
359
|
+
* - includeImport
|
|
360
|
+
* - optimizeImports
|
|
361
|
+
* - createComponent
|
|
362
|
+
* - insertJSX
|
|
363
|
+
* - mergeComponents
|
|
364
|
+
* - codeReplace
|
|
365
|
+
* parseAST
|
|
366
|
+
↓
|
|
367
|
+
resolveImportConflicts
|
|
368
|
+
↓
|
|
369
|
+
generateCode
|
|
370
|
+
* ------------------------------------------------------------
|
|
371
|
+
* IMPORTANT ARCHITECTURAL IDEA
|
|
372
|
+
* ------------------------------------------------------------
|
|
373
|
+
*
|
|
374
|
+
* resolveImportConflicts is:
|
|
375
|
+
*
|
|
376
|
+
* INTERNAL ONLY
|
|
377
|
+
*
|
|
378
|
+
* WHY?
|
|
379
|
+
*
|
|
380
|
+
* Users never directly ask:
|
|
381
|
+
*
|
|
382
|
+
* "resolve import conflicts"
|
|
383
|
+
*
|
|
384
|
+
* Instead:
|
|
385
|
+
*
|
|
386
|
+
* higher-level primitives internally use it.
|
|
387
|
+
*
|
|
388
|
+
* ------------------------------------------------------------
|
|
389
|
+
* WHAT THIS FUNCTION HANDLES
|
|
390
|
+
* ------------------------------------------------------------
|
|
391
|
+
*
|
|
392
|
+
* Current implementation handles:
|
|
393
|
+
*
|
|
394
|
+
* - duplicate default imports
|
|
395
|
+
* - duplicate named imports
|
|
396
|
+
* - duplicate namespace imports
|
|
397
|
+
*
|
|
398
|
+
* ------------------------------------------------------------
|
|
399
|
+
* CONFLICT RESOLUTION STRATEGY
|
|
400
|
+
* ------------------------------------------------------------
|
|
401
|
+
*
|
|
402
|
+
* If identifier already exists:
|
|
403
|
+
*
|
|
404
|
+
* generate unique alias.
|
|
405
|
+
*
|
|
406
|
+
* Example:
|
|
407
|
+
*
|
|
408
|
+
* Button
|
|
409
|
+
* Button1
|
|
410
|
+
* Button2
|
|
411
|
+
* Button3
|
|
412
|
+
*
|
|
413
|
+
* until unique identifier found.
|
|
414
|
+
*
|
|
415
|
+
* ------------------------------------------------------------
|
|
416
|
+
* IMPORTANT NOTE
|
|
417
|
+
* ------------------------------------------------------------
|
|
418
|
+
*
|
|
419
|
+
* This function updates:
|
|
420
|
+
*
|
|
421
|
+
* import declarations only.
|
|
422
|
+
*
|
|
423
|
+
* It does NOT automatically rename usages.
|
|
424
|
+
*
|
|
425
|
+
* WHY?
|
|
426
|
+
*
|
|
427
|
+
* Usage updates belong to:
|
|
428
|
+
*
|
|
429
|
+
* renameVariable
|
|
430
|
+
* or
|
|
431
|
+
* identifier refactor utilities.
|
|
432
|
+
*
|
|
433
|
+
* ------------------------------------------------------------
|
|
434
|
+
* IMPORTANT LIMITATION
|
|
435
|
+
* ------------------------------------------------------------
|
|
436
|
+
*
|
|
437
|
+
* Current implementation:
|
|
438
|
+
*
|
|
439
|
+
* - only checks import declarations
|
|
440
|
+
*
|
|
441
|
+
* NOT:
|
|
442
|
+
*
|
|
443
|
+
* - local variables
|
|
444
|
+
* - function params
|
|
445
|
+
* - component names
|
|
446
|
+
*
|
|
447
|
+
* Future improvements:
|
|
448
|
+
*
|
|
449
|
+
* - scope-aware resolution
|
|
450
|
+
* - full identifier graph
|
|
451
|
+
* - automatic usage rewrites
|
|
452
|
+
* - TS type import support
|
|
453
|
+
*
|
|
454
|
+
* ------------------------------------------------------------
|
|
455
|
+
* PARAMS
|
|
456
|
+
* ------------------------------------------------------------
|
|
457
|
+
*
|
|
458
|
+
* @param {Object} params
|
|
459
|
+
*
|
|
460
|
+
* @param {object} params.ast
|
|
461
|
+
* Babel AST object.
|
|
462
|
+
*
|
|
463
|
+
* ------------------------------------------------------------
|
|
464
|
+
* RETURNS
|
|
465
|
+
* ------------------------------------------------------------
|
|
466
|
+
*
|
|
467
|
+
* @returns {{
|
|
468
|
+
* ast: object,
|
|
469
|
+
* code: string
|
|
470
|
+
* }}
|
|
471
|
+
*
|
|
472
|
+
*/
|
|
473
|
+
// function resolveImportConflicts({
|
|
474
|
+
// ast,
|
|
475
|
+
// }: TaskPayload<any>): TaskResponse<TaskReturn<any>> {
|
|
476
|
+
// // ----------------------------------------------------------
|
|
477
|
+
// // STEP 1:
|
|
478
|
+
// // Store already-used identifiers
|
|
479
|
+
// //
|
|
480
|
+
// // Example:
|
|
481
|
+
// //
|
|
482
|
+
// // Button
|
|
483
|
+
// // Header
|
|
484
|
+
// // React
|
|
485
|
+
// // ----------------------------------------------------------
|
|
486
|
+
// const usedIdentifiers = new Set();
|
|
487
|
+
// // ----------------------------------------------------------
|
|
488
|
+
// // STEP 2:
|
|
489
|
+
// // Traverse all import declarations
|
|
490
|
+
// // ----------------------------------------------------------
|
|
491
|
+
// traverse.default(ast, {
|
|
492
|
+
// ImportDeclaration(path) {
|
|
493
|
+
// // ------------------------------------------------------
|
|
494
|
+
// // Iterate all import specifiers
|
|
495
|
+
// //
|
|
496
|
+
// // Examples:
|
|
497
|
+
// //
|
|
498
|
+
// // import React from "react"
|
|
499
|
+
// // import { useState } from "react"
|
|
500
|
+
// // import * as UI from "./ui"
|
|
501
|
+
// // ------------------------------------------------------
|
|
502
|
+
// path.node.specifiers.forEach((specifier) => {
|
|
503
|
+
// // --------------------------------------------------
|
|
504
|
+
// // Extract local identifier name
|
|
505
|
+
// //
|
|
506
|
+
// // Example:
|
|
507
|
+
// //
|
|
508
|
+
// // import Button from "./Button"
|
|
509
|
+
// //
|
|
510
|
+
// // local.name = Button
|
|
511
|
+
// // --------------------------------------------------
|
|
512
|
+
// const localName = specifier.local.name;
|
|
513
|
+
// // --------------------------------------------------
|
|
514
|
+
// // If identifier not used yet:
|
|
515
|
+
// // store it safely
|
|
516
|
+
// // --------------------------------------------------
|
|
517
|
+
// if (!usedIdentifiers.has(localName)) {
|
|
518
|
+
// usedIdentifiers.add(localName);
|
|
519
|
+
// return;
|
|
520
|
+
// }
|
|
521
|
+
// // --------------------------------------------------
|
|
522
|
+
// // CONFLICT DETECTED
|
|
523
|
+
// //
|
|
524
|
+
// // Need unique replacement name.
|
|
525
|
+
// // --------------------------------------------------
|
|
526
|
+
// let counter = 1;
|
|
527
|
+
// // --------------------------------------------------
|
|
528
|
+
// // Start with:
|
|
529
|
+
// //
|
|
530
|
+
// // Button1
|
|
531
|
+
// // --------------------------------------------------
|
|
532
|
+
// let newName = `${localName}${counter}`;
|
|
533
|
+
// // --------------------------------------------------
|
|
534
|
+
// // Keep generating until unique name found
|
|
535
|
+
// // --------------------------------------------------
|
|
536
|
+
// while (usedIdentifiers.has(newName)) {
|
|
537
|
+
// counter++;
|
|
538
|
+
// newName = `${localName}${counter}`;
|
|
539
|
+
// }
|
|
540
|
+
// // --------------------------------------------------
|
|
541
|
+
// // Mark new name as used
|
|
542
|
+
// // --------------------------------------------------
|
|
543
|
+
// usedIdentifiers.add(newName);
|
|
544
|
+
// // --------------------------------------------------
|
|
545
|
+
// // Replace conflicting identifier
|
|
546
|
+
// //
|
|
547
|
+
// // Example:
|
|
548
|
+
// //
|
|
549
|
+
// // Button -> Button1
|
|
550
|
+
// // --------------------------------------------------
|
|
551
|
+
// specifier.local = t.identifier(newName);
|
|
552
|
+
// });
|
|
553
|
+
// },
|
|
554
|
+
// });
|
|
555
|
+
// // ----------------------------------------------------------
|
|
556
|
+
// // STEP 3:
|
|
557
|
+
// // Generate updated source code
|
|
558
|
+
// // ----------------------------------------------------------
|
|
559
|
+
// const output = generate(ast, {
|
|
560
|
+
// retainLines: true,
|
|
561
|
+
// });
|
|
562
|
+
// // ----------------------------------------------------------
|
|
563
|
+
// // STEP 4:
|
|
564
|
+
// // Return updated AST + code
|
|
565
|
+
// // ----------------------------------------------------------
|
|
566
|
+
// return {
|
|
567
|
+
// ast,
|
|
568
|
+
// code: output.code,
|
|
569
|
+
// };
|
|
570
|
+
// }
|
|
571
|
+
/**
|
|
572
|
+
*
|
|
573
|
+
* Updates import statements inside a component file.
|
|
574
|
+
* ------------------------------------------------------------
|
|
575
|
+
* WHAT THIS FUNCTION DOES
|
|
576
|
+
* ------------------------------------------------------------
|
|
577
|
+
*
|
|
578
|
+
* This function safely manages:
|
|
579
|
+
*
|
|
580
|
+
* - adding imports
|
|
581
|
+
* - removing imports
|
|
582
|
+
* - replacing imports
|
|
583
|
+
* - fixing import paths
|
|
584
|
+
* - deduplicating imports
|
|
585
|
+
*
|
|
586
|
+
* inside a component file.
|
|
587
|
+
*
|
|
588
|
+
* Example:
|
|
589
|
+
*
|
|
590
|
+
* BEFORE:
|
|
591
|
+
*
|
|
592
|
+
* import Button from "./Button";
|
|
593
|
+
*
|
|
594
|
+
* AFTER:
|
|
595
|
+
*
|
|
596
|
+
* import Button from "../ui/Button";
|
|
597
|
+
*
|
|
598
|
+
* ------------------------------------------------------------
|
|
599
|
+
* WHY THIS FUNCTION EXISTS
|
|
600
|
+
* ------------------------------------------------------------
|
|
601
|
+
*
|
|
602
|
+
* AI-generated projects frequently suffer from:
|
|
603
|
+
*
|
|
604
|
+
* - duplicate imports
|
|
605
|
+
* - broken relative paths
|
|
606
|
+
* - stale imports
|
|
607
|
+
* - missing imports
|
|
608
|
+
* - inconsistent ordering
|
|
609
|
+
*
|
|
610
|
+
* Runtime component manipulation
|
|
611
|
+
* becomes impossible without:
|
|
612
|
+
*
|
|
613
|
+
* deterministic import management.
|
|
614
|
+
*
|
|
615
|
+
* This function acts as:
|
|
616
|
+
*
|
|
617
|
+
* import synchronization layer.
|
|
618
|
+
*
|
|
619
|
+
* ------------------------------------------------------------
|
|
620
|
+
* IMPORTANT ARCHITECTURAL DECISION
|
|
621
|
+
* ------------------------------------------------------------
|
|
622
|
+
*
|
|
623
|
+
* Looma should NEVER:
|
|
624
|
+
*
|
|
625
|
+
* blindly append imports.
|
|
626
|
+
*
|
|
627
|
+
* Imports should always remain:
|
|
628
|
+
*
|
|
629
|
+
* - normalized
|
|
630
|
+
* - deduplicated
|
|
631
|
+
* - deterministic
|
|
632
|
+
* - architecture-safe
|
|
633
|
+
*
|
|
634
|
+
* ------------------------------------------------------------
|
|
635
|
+
* IMPORTANT NOTE
|
|
636
|
+
* ------------------------------------------------------------
|
|
637
|
+
*
|
|
638
|
+
* updateComponentImports only removes imports it does not update JSX
|
|
639
|
+
*
|
|
640
|
+
*
|
|
641
|
+
* Current implementation:
|
|
642
|
+
*
|
|
643
|
+
* uses string-based parsing.
|
|
644
|
+
*
|
|
645
|
+
* Production version should use:
|
|
646
|
+
*
|
|
647
|
+
* Babel AST import manipulation.
|
|
648
|
+
*
|
|
649
|
+
* because regex/string parsing becomes fragile
|
|
650
|
+
* for:
|
|
651
|
+
*
|
|
652
|
+
* - multiline imports
|
|
653
|
+
* - namespace imports
|
|
654
|
+
* - mixed imports
|
|
655
|
+
* - comments
|
|
656
|
+
* - dynamic imports
|
|
657
|
+
*
|
|
658
|
+
* ------------------------------------------------------------
|
|
659
|
+
* DEPENDENCIES
|
|
660
|
+
* ------------------------------------------------------------
|
|
661
|
+
*
|
|
662
|
+
* This function complements:
|
|
663
|
+
*
|
|
664
|
+
* - createComponent()
|
|
665
|
+
* - updateComponent()
|
|
666
|
+
* - renameComponent()
|
|
667
|
+
* - moveComponent()
|
|
668
|
+
* - extractComponent()
|
|
669
|
+
* - ensureComponentStructure()
|
|
670
|
+
* - ensureImport()
|
|
671
|
+
* - enrichImports()
|
|
672
|
+
* - removeImport()
|
|
673
|
+
*
|
|
674
|
+
* ------------------------------------------------------------
|
|
675
|
+
* WHERE THIS FUNCTION IS USEFUL
|
|
676
|
+
* ------------------------------------------------------------
|
|
677
|
+
*
|
|
678
|
+
* Useful in commands like:
|
|
679
|
+
*
|
|
680
|
+
* - "extract component"
|
|
681
|
+
* - "move component"
|
|
682
|
+
* - "rename component"
|
|
683
|
+
* - "replace button"
|
|
684
|
+
* - "add modal"
|
|
685
|
+
* - "cleanup imports"
|
|
686
|
+
* - "fix broken imports"
|
|
687
|
+
*
|
|
688
|
+
* Usually executed AFTER:
|
|
689
|
+
*
|
|
690
|
+
* - component mutations
|
|
691
|
+
* - file movements
|
|
692
|
+
* - JSX updates
|
|
693
|
+
*
|
|
694
|
+
* ------------------------------------------------------------
|
|
695
|
+
* PARAMS
|
|
696
|
+
* ------------------------------------------------------------
|
|
697
|
+
*
|
|
698
|
+
* @param {Object} params
|
|
699
|
+
*
|
|
700
|
+
* @param {string} params.componentPath
|
|
701
|
+
* JSX component file path.
|
|
702
|
+
*
|
|
703
|
+
* Example:
|
|
704
|
+
*
|
|
705
|
+
* "./src/components/Home/Home.jsx"
|
|
706
|
+
*
|
|
707
|
+
* @param {Array<Object>} params.operations
|
|
708
|
+
* Import operations.
|
|
709
|
+
*
|
|
710
|
+
* Example:
|
|
711
|
+
*
|
|
712
|
+
* [
|
|
713
|
+
* {
|
|
714
|
+
* type: "add",
|
|
715
|
+
* importName: "Button",
|
|
716
|
+
* importPath: "../ui/Button"
|
|
717
|
+
* }
|
|
718
|
+
* ]
|
|
719
|
+
*
|
|
720
|
+
* Supported operation types:
|
|
721
|
+
*
|
|
722
|
+
* - add
|
|
723
|
+
* - remove
|
|
724
|
+
* - replace
|
|
725
|
+
*
|
|
726
|
+
* ------------------------------------------------------------
|
|
727
|
+
* OPERATION STRUCTURE
|
|
728
|
+
* ------------------------------------------------------------
|
|
729
|
+
*
|
|
730
|
+
* ADD:
|
|
731
|
+
*
|
|
732
|
+
* {
|
|
733
|
+
* type: "add",
|
|
734
|
+
* importName,
|
|
735
|
+
* importPath
|
|
736
|
+
* }
|
|
737
|
+
*
|
|
738
|
+
* REMOVE:
|
|
739
|
+
*
|
|
740
|
+
* {
|
|
741
|
+
* type: "remove",
|
|
742
|
+
* importName
|
|
743
|
+
* }
|
|
744
|
+
*
|
|
745
|
+
* REPLACE:
|
|
746
|
+
*
|
|
747
|
+
* {
|
|
748
|
+
* type: "replace",
|
|
749
|
+
* oldImportName,
|
|
750
|
+
* newImportName,
|
|
751
|
+
* newImportPath
|
|
752
|
+
* }
|
|
753
|
+
*
|
|
754
|
+
* ------------------------------------------------------------
|
|
755
|
+
* RETURNS
|
|
756
|
+
* ------------------------------------------------------------
|
|
757
|
+
*
|
|
758
|
+
* @returns {Object}
|
|
759
|
+
*
|
|
760
|
+
* {
|
|
761
|
+
* updated: boolean,
|
|
762
|
+
* operationsApplied: number
|
|
763
|
+
* }
|
|
764
|
+
*
|
|
765
|
+
*/
|
|
766
|
+
// function updateComponentImports({
|
|
767
|
+
// componentPath,
|
|
768
|
+
// operations = [],
|
|
769
|
+
// }: TaskPayload<"updateComponentImports">): TaskResponse<
|
|
770
|
+
// TaskReturn<"updateComponentImports">
|
|
771
|
+
// > {
|
|
772
|
+
// // ----------------------------------------------------------
|
|
773
|
+
// // STEP 1:
|
|
774
|
+
// // Resolve absolute component path
|
|
775
|
+
// // ----------------------------------------------------------
|
|
776
|
+
// const absoluteComponentPath = path.resolve(componentPath);
|
|
777
|
+
// // ----------------------------------------------------------
|
|
778
|
+
// // STEP 2:
|
|
779
|
+
// // Validate component existence
|
|
780
|
+
// // ----------------------------------------------------------
|
|
781
|
+
// if (!fs.existsSync(absoluteComponentPath)) {
|
|
782
|
+
// throw new Error(`Component file does not exist: ${absoluteComponentPath}`);
|
|
783
|
+
// }
|
|
784
|
+
// // ----------------------------------------------------------
|
|
785
|
+
// // STEP 3:
|
|
786
|
+
// // Read component source code
|
|
787
|
+
// // ----------------------------------------------------------
|
|
788
|
+
// let componentCode = fs.readFileSync(absoluteComponentPath, "utf8");
|
|
789
|
+
// // ----------------------------------------------------------
|
|
790
|
+
// // STEP 4:
|
|
791
|
+
// // Track successful operations
|
|
792
|
+
// // ----------------------------------------------------------
|
|
793
|
+
// let operationsApplied = 0;
|
|
794
|
+
// // ----------------------------------------------------------
|
|
795
|
+
// // STEP 5:
|
|
796
|
+
// // Process each import operation
|
|
797
|
+
// // ----------------------------------------------------------
|
|
798
|
+
// operations.forEach((operation) => {
|
|
799
|
+
// // ------------------------------------------------------
|
|
800
|
+
// // ADD IMPORT
|
|
801
|
+
// // ------------------------------------------------------
|
|
802
|
+
// if (operation.type === "add") {
|
|
803
|
+
// // ----------------------------------------------------
|
|
804
|
+
// // Build import statement
|
|
805
|
+
// // ----------------------------------------------------
|
|
806
|
+
// const importStatement = `import ${operation.importName} from "${operation.importPath}";`;
|
|
807
|
+
// // ----------------------------------------------------
|
|
808
|
+
// // Prevent duplicate imports
|
|
809
|
+
// // ----------------------------------------------------
|
|
810
|
+
// if (componentCode.includes(importStatement)) {
|
|
811
|
+
// return;
|
|
812
|
+
// }
|
|
813
|
+
// // ----------------------------------------------------
|
|
814
|
+
// // Insert import at top of file
|
|
815
|
+
// // ----------------------------------------------------
|
|
816
|
+
// componentCode = importStatement + "\n" + componentCode;
|
|
817
|
+
// operationsApplied++;
|
|
818
|
+
// }
|
|
819
|
+
// // ------------------------------------------------------
|
|
820
|
+
// // REMOVE IMPORT
|
|
821
|
+
// // ------------------------------------------------------
|
|
822
|
+
// else if (operation.type === "remove") {
|
|
823
|
+
// // ----------------------------------------------------
|
|
824
|
+
// // Build regex for import removal
|
|
825
|
+
// // ----------------------------------------------------
|
|
826
|
+
// const importRegex = new RegExp(
|
|
827
|
+
// `import\\s+${operation.importName}\\s+from\\s+["'][^"']+["'];?\\n?`,
|
|
828
|
+
// "g"
|
|
829
|
+
// );
|
|
830
|
+
// // ----------------------------------------------------
|
|
831
|
+
// // Detect whether import exists
|
|
832
|
+
// // ----------------------------------------------------
|
|
833
|
+
// const hasImport = importRegex.test(componentCode);
|
|
834
|
+
// // ----------------------------------------------------
|
|
835
|
+
// // Remove import
|
|
836
|
+
// // ----------------------------------------------------
|
|
837
|
+
// componentCode = componentCode.replace(importRegex, "");
|
|
838
|
+
// // ----------------------------------------------------
|
|
839
|
+
// // Track successful removal
|
|
840
|
+
// // ----------------------------------------------------
|
|
841
|
+
// if (hasImport) {
|
|
842
|
+
// operationsApplied++;
|
|
843
|
+
// }
|
|
844
|
+
// }
|
|
845
|
+
// // ------------------------------------------------------
|
|
846
|
+
// // REPLACE IMPORT
|
|
847
|
+
// // ------------------------------------------------------
|
|
848
|
+
// else if (operation.type === "replace") {
|
|
849
|
+
// // ----------------------------------------------------
|
|
850
|
+
// // Build regex for old import
|
|
851
|
+
// // ----------------------------------------------------
|
|
852
|
+
// const oldImportRegex = new RegExp(
|
|
853
|
+
// `import\\s+${operation.oldImportName}\\s+from\\s+["'][^"']+["'];?`,
|
|
854
|
+
// "g"
|
|
855
|
+
// );
|
|
856
|
+
// // ----------------------------------------------------
|
|
857
|
+
// // Build new import statement
|
|
858
|
+
// // ----------------------------------------------------
|
|
859
|
+
// const newImportStatement = `import ${operation.newImportName} from "${operation.newImportPath}";`;
|
|
860
|
+
// // ----------------------------------------------------
|
|
861
|
+
// // Detect old import existence
|
|
862
|
+
// // ----------------------------------------------------
|
|
863
|
+
// const hasImport = oldImportRegex.test(componentCode);
|
|
864
|
+
// // ----------------------------------------------------
|
|
865
|
+
// // Replace old import
|
|
866
|
+
// // ----------------------------------------------------
|
|
867
|
+
// componentCode = componentCode.replace(oldImportRegex, newImportStatement);
|
|
868
|
+
// // ----------------------------------------------------
|
|
869
|
+
// // Track successful replacement
|
|
870
|
+
// // ----------------------------------------------------
|
|
871
|
+
// if (hasImport) {
|
|
872
|
+
// operationsApplied++;
|
|
873
|
+
// }
|
|
874
|
+
// }
|
|
875
|
+
// });
|
|
876
|
+
// // ----------------------------------------------------------
|
|
877
|
+
// // STEP 6:
|
|
878
|
+
// // Cleanup excessive blank lines
|
|
879
|
+
// // ----------------------------------------------------------
|
|
880
|
+
// componentCode = componentCode.replace(/\n{3,}/g, "\n\n");
|
|
881
|
+
// // ----------------------------------------------------------
|
|
882
|
+
// // STEP 7:
|
|
883
|
+
// // Write updated component source
|
|
884
|
+
// // ----------------------------------------------------------
|
|
885
|
+
// fs.writeFileSync(absoluteComponentPath, componentCode.trim(), "utf8");
|
|
886
|
+
// // ----------------------------------------------------------
|
|
887
|
+
// // STEP 8:
|
|
888
|
+
// // Return operation summary
|
|
889
|
+
// // ----------------------------------------------------------
|
|
890
|
+
// return {
|
|
891
|
+
// success: operationsApplied > 0,
|
|
892
|
+
// operationsApplied,
|
|
893
|
+
// };
|
|
894
|
+
// }
|
|
895
|
+
/**
|
|
896
|
+
* Infers the most appropriate component name
|
|
897
|
+
* from various inputs.
|
|
898
|
+
*
|
|
899
|
+
* ------------------------------------------------------------
|
|
900
|
+
* WHAT THIS FUNCTION DOES
|
|
901
|
+
* ------------------------------------------------------------
|
|
902
|
+
*
|
|
903
|
+
* This function intelligently derives
|
|
904
|
+
* a React component name from:
|
|
905
|
+
*
|
|
906
|
+
* - user commands
|
|
907
|
+
* - file paths
|
|
908
|
+
* - JSX content
|
|
909
|
+
* - DOM labels
|
|
910
|
+
* - filenames
|
|
911
|
+
* - directory names
|
|
912
|
+
*
|
|
913
|
+
* Example:
|
|
914
|
+
*
|
|
915
|
+
* INPUT:
|
|
916
|
+
*
|
|
917
|
+
* "make navbar red"
|
|
918
|
+
*
|
|
919
|
+
* OUTPUT:
|
|
920
|
+
*
|
|
921
|
+
* "Navbar"
|
|
922
|
+
*
|
|
923
|
+
* ------------------------------------------------------------
|
|
924
|
+
* WHY THIS FUNCTION EXISTS
|
|
925
|
+
* ------------------------------------------------------------
|
|
926
|
+
*
|
|
927
|
+
* Users rarely speak using:
|
|
928
|
+
*
|
|
929
|
+
* exact filesystem names.
|
|
930
|
+
*
|
|
931
|
+
* Example:
|
|
932
|
+
*
|
|
933
|
+
* User says:
|
|
934
|
+
*
|
|
935
|
+
* "update hero section"
|
|
936
|
+
*
|
|
937
|
+
* but filesystem may contain:
|
|
938
|
+
*
|
|
939
|
+
* HeroSection/
|
|
940
|
+
*
|
|
941
|
+
* AI systems require:
|
|
942
|
+
*
|
|
943
|
+
* normalized deterministic names.
|
|
944
|
+
*
|
|
945
|
+
* This function acts as:
|
|
946
|
+
*
|
|
947
|
+
* semantic naming bridge.
|
|
948
|
+
*
|
|
949
|
+
* ------------------------------------------------------------
|
|
950
|
+
* IMPORTANT ARCHITECTURAL DECISION
|
|
951
|
+
* ------------------------------------------------------------
|
|
952
|
+
*
|
|
953
|
+
* Looma should NEVER depend entirely on:
|
|
954
|
+
*
|
|
955
|
+
* exact naming matches.
|
|
956
|
+
*
|
|
957
|
+
* Natural language interaction requires:
|
|
958
|
+
*
|
|
959
|
+
* fuzzy semantic inference.
|
|
960
|
+
*
|
|
961
|
+
* because users naturally say:
|
|
962
|
+
*
|
|
963
|
+
* - navbar
|
|
964
|
+
* - nav bar
|
|
965
|
+
* - navigation
|
|
966
|
+
* - top header
|
|
967
|
+
*
|
|
968
|
+
* while project may contain:
|
|
969
|
+
*
|
|
970
|
+
* NavigationBar
|
|
971
|
+
*
|
|
972
|
+
* ------------------------------------------------------------
|
|
973
|
+
* IMPORTANT NOTE
|
|
974
|
+
* ------------------------------------------------------------
|
|
975
|
+
*
|
|
976
|
+
* Current implementation uses:
|
|
977
|
+
*
|
|
978
|
+
* heuristic inference.
|
|
979
|
+
*
|
|
980
|
+
* Production version should eventually use:
|
|
981
|
+
*
|
|
982
|
+
* - component registry
|
|
983
|
+
* - semantic embeddings
|
|
984
|
+
* - AST metadata
|
|
985
|
+
* - runtime DOM context
|
|
986
|
+
*
|
|
987
|
+
* for better accuracy.
|
|
988
|
+
*
|
|
989
|
+
* ------------------------------------------------------------
|
|
990
|
+
* DEPENDENCIES
|
|
991
|
+
* ------------------------------------------------------------
|
|
992
|
+
*
|
|
993
|
+
* This function complements:
|
|
994
|
+
*
|
|
995
|
+
* - findComponentDirectory()
|
|
996
|
+
* - createComponent()
|
|
997
|
+
* - renameComponent()
|
|
998
|
+
* - normalizeComponent()
|
|
999
|
+
* - extractComponent()
|
|
1000
|
+
*
|
|
1001
|
+
* ------------------------------------------------------------
|
|
1002
|
+
* WHERE THIS FUNCTION IS USEFUL
|
|
1003
|
+
* ------------------------------------------------------------
|
|
1004
|
+
*
|
|
1005
|
+
* Useful in commands like:
|
|
1006
|
+
*
|
|
1007
|
+
* - "make navbar sticky"
|
|
1008
|
+
* - "update hero"
|
|
1009
|
+
* - "extract footer"
|
|
1010
|
+
* - "move login form"
|
|
1011
|
+
* - "delete sidebar"
|
|
1012
|
+
* - "rename top section"
|
|
1013
|
+
*
|
|
1014
|
+
* Usually executed BEFORE:
|
|
1015
|
+
*
|
|
1016
|
+
* - component lookup
|
|
1017
|
+
* - component mutation
|
|
1018
|
+
* - DOM synchronization
|
|
1019
|
+
*
|
|
1020
|
+
* ------------------------------------------------------------
|
|
1021
|
+
* PARAMS
|
|
1022
|
+
* ------------------------------------------------------------
|
|
1023
|
+
*
|
|
1024
|
+
* @param {Object} params
|
|
1025
|
+
*
|
|
1026
|
+
* @param {string} [params.userInput]
|
|
1027
|
+
* Natural language user command.
|
|
1028
|
+
*
|
|
1029
|
+
* Example:
|
|
1030
|
+
*
|
|
1031
|
+
* "update navbar styles"
|
|
1032
|
+
*
|
|
1033
|
+
* @param {string} [params.filePath]
|
|
1034
|
+
* File or directory path.
|
|
1035
|
+
*
|
|
1036
|
+
* Example:
|
|
1037
|
+
*
|
|
1038
|
+
* "./src/components/Navbar/Navbar.jsx"
|
|
1039
|
+
*
|
|
1040
|
+
* @param {string} [params.jsxCode]
|
|
1041
|
+
* JSX source code.
|
|
1042
|
+
*
|
|
1043
|
+
* Example:
|
|
1044
|
+
*
|
|
1045
|
+
* function Navbar() {}
|
|
1046
|
+
*
|
|
1047
|
+
* @param {string[]} [params.stopWords]
|
|
1048
|
+
* Words to ignore during inference.
|
|
1049
|
+
*
|
|
1050
|
+
* ------------------------------------------------------------
|
|
1051
|
+
* RETURNS
|
|
1052
|
+
* ------------------------------------------------------------
|
|
1053
|
+
*
|
|
1054
|
+
* @returns {Object}
|
|
1055
|
+
*
|
|
1056
|
+
* {
|
|
1057
|
+
* componentName,
|
|
1058
|
+
* inferredFrom
|
|
1059
|
+
* }
|
|
1060
|
+
*
|
|
1061
|
+
*/
|
|
1062
|
+
// function inferComponentName({
|
|
1063
|
+
// userInput = "",
|
|
1064
|
+
// filePath = "",
|
|
1065
|
+
// jsxCode = "",
|
|
1066
|
+
// stopWords = [
|
|
1067
|
+
// "make",
|
|
1068
|
+
// "update",
|
|
1069
|
+
// "delete",
|
|
1070
|
+
// "move",
|
|
1071
|
+
// "rename",
|
|
1072
|
+
// "extract",
|
|
1073
|
+
// "component",
|
|
1074
|
+
// "section",
|
|
1075
|
+
// "add",
|
|
1076
|
+
// "remove",
|
|
1077
|
+
// "create",
|
|
1078
|
+
// "change",
|
|
1079
|
+
// "modify",
|
|
1080
|
+
// "the",
|
|
1081
|
+
// "a",
|
|
1082
|
+
// "an",
|
|
1083
|
+
// ],
|
|
1084
|
+
// }: TaskPayload<"inferComponentName">): TaskResponse<
|
|
1085
|
+
// TaskReturn<"inferComponentName">
|
|
1086
|
+
// > {
|
|
1087
|
+
// // ----------------------------------------------------------
|
|
1088
|
+
// // STEP 1:
|
|
1089
|
+
// // Helper function to convert strings
|
|
1090
|
+
// // into PascalCase component names
|
|
1091
|
+
// //
|
|
1092
|
+
// // Example:
|
|
1093
|
+
// //
|
|
1094
|
+
// // "hero section"
|
|
1095
|
+
// // →
|
|
1096
|
+
// // "HeroSection"
|
|
1097
|
+
// // ----------------------------------------------------------
|
|
1098
|
+
// function toPascalCase(value) {
|
|
1099
|
+
// return value
|
|
1100
|
+
// .split(/[\s-_]+/)
|
|
1101
|
+
// .filter(Boolean)
|
|
1102
|
+
// .map((word) => {
|
|
1103
|
+
// return word.charAt(0).toUpperCase() + word.slice(1);
|
|
1104
|
+
// })
|
|
1105
|
+
// .join("");
|
|
1106
|
+
// }
|
|
1107
|
+
// // ----------------------------------------------------------
|
|
1108
|
+
// // STEP 2:
|
|
1109
|
+
// // Try inferring from JSX code
|
|
1110
|
+
// //
|
|
1111
|
+
// // Highest confidence source.
|
|
1112
|
+
// // ----------------------------------------------------------
|
|
1113
|
+
// if (jsxCode) {
|
|
1114
|
+
// // --------------------------------------------------------
|
|
1115
|
+
// // Match:
|
|
1116
|
+
// //
|
|
1117
|
+
// // function Header()
|
|
1118
|
+
// // --------------------------------------------------------
|
|
1119
|
+
// const functionMatch = jsxCode.match(/function\s+([A-Z][A-Za-z0-9_]*)\s*\(/);
|
|
1120
|
+
// // --------------------------------------------------------
|
|
1121
|
+
// // Return detected component name
|
|
1122
|
+
// // --------------------------------------------------------
|
|
1123
|
+
// if (functionMatch) {
|
|
1124
|
+
// return {
|
|
1125
|
+
// componentName: functionMatch[1],
|
|
1126
|
+
// inferredFrom: "jsx-function",
|
|
1127
|
+
// };
|
|
1128
|
+
// }
|
|
1129
|
+
// // --------------------------------------------------------
|
|
1130
|
+
// // Match:
|
|
1131
|
+
// //
|
|
1132
|
+
// // const Header = () =>
|
|
1133
|
+
// // --------------------------------------------------------
|
|
1134
|
+
// const arrowMatch = jsxCode.match(/const\s+([A-Z][A-Za-z0-9_]*)\s*=\s*\(/);
|
|
1135
|
+
// // --------------------------------------------------------
|
|
1136
|
+
// // Return detected arrow component
|
|
1137
|
+
// // --------------------------------------------------------
|
|
1138
|
+
// if (arrowMatch) {
|
|
1139
|
+
// return {
|
|
1140
|
+
// success: true,
|
|
1141
|
+
// componentName: arrowMatch[1],
|
|
1142
|
+
// inferredFrom: "jsx-arrow-function",
|
|
1143
|
+
// };
|
|
1144
|
+
// }
|
|
1145
|
+
// }
|
|
1146
|
+
// // ----------------------------------------------------------
|
|
1147
|
+
// // STEP 3:
|
|
1148
|
+
// // Try inferring from file path
|
|
1149
|
+
// // ----------------------------------------------------------
|
|
1150
|
+
// if (filePath) {
|
|
1151
|
+
// // --------------------------------------------------------
|
|
1152
|
+
// // Extract filename/folder name
|
|
1153
|
+
// // --------------------------------------------------------
|
|
1154
|
+
// const parsedPath = path.parse(filePath);
|
|
1155
|
+
// // --------------------------------------------------------
|
|
1156
|
+
// // Prefer directory name if available
|
|
1157
|
+
// // --------------------------------------------------------
|
|
1158
|
+
// const directoryName = path.basename(path.dirname(filePath));
|
|
1159
|
+
// // --------------------------------------------------------
|
|
1160
|
+
// // Use filename if meaningful
|
|
1161
|
+
// // --------------------------------------------------------
|
|
1162
|
+
// const candidateName = parsedPath.name || directoryName;
|
|
1163
|
+
// // --------------------------------------------------------
|
|
1164
|
+
// // Normalize component name
|
|
1165
|
+
// // --------------------------------------------------------
|
|
1166
|
+
// const normalizedName = toPascalCase(candidateName);
|
|
1167
|
+
// // --------------------------------------------------------
|
|
1168
|
+
// // Return inferred name
|
|
1169
|
+
// // --------------------------------------------------------
|
|
1170
|
+
// if (normalizedName) {
|
|
1171
|
+
// return {
|
|
1172
|
+
// success: true,
|
|
1173
|
+
// componentName: normalizedName,
|
|
1174
|
+
// inferredFrom: "file-path",
|
|
1175
|
+
// };
|
|
1176
|
+
// }
|
|
1177
|
+
// }
|
|
1178
|
+
// // ----------------------------------------------------------
|
|
1179
|
+
// // STEP 4:
|
|
1180
|
+
// // Try inferring from user input
|
|
1181
|
+
// // ----------------------------------------------------------
|
|
1182
|
+
// if (userInput) {
|
|
1183
|
+
// // --------------------------------------------------------
|
|
1184
|
+
// // Normalize input
|
|
1185
|
+
// // --------------------------------------------------------
|
|
1186
|
+
// const normalizedInput = userInput
|
|
1187
|
+
// .toLowerCase()
|
|
1188
|
+
// .replace(/[^a-z0-9\s-_]/g, "");
|
|
1189
|
+
// // --------------------------------------------------------
|
|
1190
|
+
// // Split words
|
|
1191
|
+
// // --------------------------------------------------------
|
|
1192
|
+
// const words = normalizedInput.split(/\s+/);
|
|
1193
|
+
// // --------------------------------------------------------
|
|
1194
|
+
// // Remove stop words
|
|
1195
|
+
// // --------------------------------------------------------
|
|
1196
|
+
// const filteredWords = words.filter((word) => {
|
|
1197
|
+
// return word && !stopWords.includes(word);
|
|
1198
|
+
// });
|
|
1199
|
+
// // --------------------------------------------------------
|
|
1200
|
+
// // Convert to PascalCase
|
|
1201
|
+
// // --------------------------------------------------------
|
|
1202
|
+
// const inferredName = toPascalCase(filteredWords.join(" "));
|
|
1203
|
+
// // --------------------------------------------------------
|
|
1204
|
+
// // Return inferred component name
|
|
1205
|
+
// // --------------------------------------------------------
|
|
1206
|
+
// if (inferredName) {
|
|
1207
|
+
// return {
|
|
1208
|
+
// success: true,
|
|
1209
|
+
// componentName: inferredName,
|
|
1210
|
+
// inferredFrom: "user-input",
|
|
1211
|
+
// };
|
|
1212
|
+
// }
|
|
1213
|
+
// }
|
|
1214
|
+
// // ----------------------------------------------------------
|
|
1215
|
+
// // STEP 5:
|
|
1216
|
+
// // Fallback component name
|
|
1217
|
+
// // ----------------------------------------------------------
|
|
1218
|
+
// return {
|
|
1219
|
+
// success: true,
|
|
1220
|
+
// componentName: "UnnamedComponent",
|
|
1221
|
+
// inferredFrom: "fallback",
|
|
1222
|
+
// };
|
|
1223
|
+
// }
|
|
1224
|
+
/**
|
|
1225
|
+
* Generates deterministic CSS class names
|
|
1226
|
+
* for Looma components.
|
|
1227
|
+
*
|
|
1228
|
+
* ------------------------------------------------------------
|
|
1229
|
+
* WHAT THIS FUNCTION DOES
|
|
1230
|
+
* ------------------------------------------------------------
|
|
1231
|
+
*
|
|
1232
|
+
* This function creates predictable,
|
|
1233
|
+
* collision-safe css class names.
|
|
1234
|
+
*
|
|
1235
|
+
* Example:
|
|
1236
|
+
*
|
|
1237
|
+
* INPUT:
|
|
1238
|
+
*
|
|
1239
|
+
* componentName: "Header"
|
|
1240
|
+
* elementName: "title"
|
|
1241
|
+
*
|
|
1242
|
+
* OUTPUT:
|
|
1243
|
+
*
|
|
1244
|
+
* "header__title"
|
|
1245
|
+
*
|
|
1246
|
+
* ------------------------------------------------------------
|
|
1247
|
+
* WHY THIS FUNCTION EXISTS
|
|
1248
|
+
* ------------------------------------------------------------
|
|
1249
|
+
*
|
|
1250
|
+
* AI-generated code can quickly become messy
|
|
1251
|
+
* if class naming is inconsistent.
|
|
1252
|
+
*
|
|
1253
|
+
* Problems without naming strategy:
|
|
1254
|
+
*
|
|
1255
|
+
* - duplicate class names
|
|
1256
|
+
* - style leakage
|
|
1257
|
+
* - unreadable css
|
|
1258
|
+
* - impossible refactors
|
|
1259
|
+
* - component collisions
|
|
1260
|
+
*
|
|
1261
|
+
* This function enforces:
|
|
1262
|
+
*
|
|
1263
|
+
* deterministic naming architecture.
|
|
1264
|
+
*
|
|
1265
|
+
* ------------------------------------------------------------
|
|
1266
|
+
* ARCHITECTURE STRATEGY
|
|
1267
|
+
* ------------------------------------------------------------
|
|
1268
|
+
*
|
|
1269
|
+
* This implementation follows:
|
|
1270
|
+
*
|
|
1271
|
+
* BEM-inspired naming.
|
|
1272
|
+
*
|
|
1273
|
+
* Format:
|
|
1274
|
+
*
|
|
1275
|
+
* component__element--modifier
|
|
1276
|
+
*
|
|
1277
|
+
* Examples:
|
|
1278
|
+
*
|
|
1279
|
+
* header
|
|
1280
|
+
* header__title
|
|
1281
|
+
* header__button
|
|
1282
|
+
* header__button--active
|
|
1283
|
+
*
|
|
1284
|
+
* ------------------------------------------------------------
|
|
1285
|
+
* IMPORTANT DESIGN DECISION
|
|
1286
|
+
* ------------------------------------------------------------
|
|
1287
|
+
*
|
|
1288
|
+
* Class names are:
|
|
1289
|
+
*
|
|
1290
|
+
* - deterministic
|
|
1291
|
+
* - readable
|
|
1292
|
+
* - component scoped
|
|
1293
|
+
* - collision resistant
|
|
1294
|
+
*
|
|
1295
|
+
* We NEVER generate:
|
|
1296
|
+
*
|
|
1297
|
+
* random class names.
|
|
1298
|
+
*
|
|
1299
|
+
* Example of BAD naming:
|
|
1300
|
+
*
|
|
1301
|
+
* x12ab
|
|
1302
|
+
* card991
|
|
1303
|
+
*
|
|
1304
|
+
* ------------------------------------------------------------
|
|
1305
|
+
* WHERE THIS FUNCTION IS USEFUL
|
|
1306
|
+
* ------------------------------------------------------------
|
|
1307
|
+
*
|
|
1308
|
+
* Useful in commands like:
|
|
1309
|
+
*
|
|
1310
|
+
* - "create card component"
|
|
1311
|
+
* - "add button"
|
|
1312
|
+
* - "generate styles"
|
|
1313
|
+
* - "extract section"
|
|
1314
|
+
* - "create navbar"
|
|
1315
|
+
* - "rename styles"
|
|
1316
|
+
* - "normalize css"
|
|
1317
|
+
*
|
|
1318
|
+
* This function is usually called before:
|
|
1319
|
+
*
|
|
1320
|
+
* - insertStyles()
|
|
1321
|
+
* - updateStyles()
|
|
1322
|
+
* - renameCssClass()
|
|
1323
|
+
* - createComponent()
|
|
1324
|
+
*
|
|
1325
|
+
* ------------------------------------------------------------
|
|
1326
|
+
* PARAMS
|
|
1327
|
+
* ------------------------------------------------------------
|
|
1328
|
+
*
|
|
1329
|
+
* @param {Object} params
|
|
1330
|
+
*
|
|
1331
|
+
* @param {string} params.componentName
|
|
1332
|
+
* Component name.
|
|
1333
|
+
*
|
|
1334
|
+
* Example:
|
|
1335
|
+
*
|
|
1336
|
+
* "Header"
|
|
1337
|
+
*
|
|
1338
|
+
* @param {string} [params.elementName]
|
|
1339
|
+
* Optional child element name.
|
|
1340
|
+
*
|
|
1341
|
+
* Example:
|
|
1342
|
+
*
|
|
1343
|
+
* "title"
|
|
1344
|
+
*
|
|
1345
|
+
* @param {string} [params.modifier]
|
|
1346
|
+
* Optional modifier name.
|
|
1347
|
+
*
|
|
1348
|
+
* Example:
|
|
1349
|
+
*
|
|
1350
|
+
* "active"
|
|
1351
|
+
*
|
|
1352
|
+
* @param {boolean} [params.useKebabCase=true]
|
|
1353
|
+
* Whether generated names should use kebab-case.
|
|
1354
|
+
*
|
|
1355
|
+
* ------------------------------------------------------------
|
|
1356
|
+
* RETURNS
|
|
1357
|
+
* ------------------------------------------------------------
|
|
1358
|
+
*
|
|
1359
|
+
* @returns {string}
|
|
1360
|
+
*
|
|
1361
|
+
* Example outputs:
|
|
1362
|
+
*
|
|
1363
|
+
* "header"
|
|
1364
|
+
* "header__title"
|
|
1365
|
+
* "header__button--active"
|
|
1366
|
+
*
|
|
1367
|
+
*/
|
|
1368
|
+
// function generateClassNames({
|
|
1369
|
+
// componentName,
|
|
1370
|
+
// elementName,
|
|
1371
|
+
// modifier,
|
|
1372
|
+
// useKebabCase = true,
|
|
1373
|
+
// }) {
|
|
1374
|
+
// // ----------------------------------------------------------
|
|
1375
|
+
// // STEP 1:
|
|
1376
|
+
// // Helper function to normalize names
|
|
1377
|
+
// //
|
|
1378
|
+
// // Converts:
|
|
1379
|
+
// //
|
|
1380
|
+
// // HeaderTitle
|
|
1381
|
+
// // →
|
|
1382
|
+
// // header-title
|
|
1383
|
+
// // ----------------------------------------------------------
|
|
1384
|
+
// function normalize(value) {
|
|
1385
|
+
// // --------------------------------------------------------
|
|
1386
|
+
// // Return empty string for invalid values
|
|
1387
|
+
// // --------------------------------------------------------
|
|
1388
|
+
// if (!value) {
|
|
1389
|
+
// return "";
|
|
1390
|
+
// }
|
|
1391
|
+
// // --------------------------------------------------------
|
|
1392
|
+
// // Convert camelCase/PascalCase to kebab-case
|
|
1393
|
+
// // --------------------------------------------------------
|
|
1394
|
+
// let normalized = value.replace(/([a-z])([A-Z])/g, "$1-$2");
|
|
1395
|
+
// // --------------------------------------------------------
|
|
1396
|
+
// // Replace spaces/underscores
|
|
1397
|
+
// // --------------------------------------------------------
|
|
1398
|
+
// normalized = normalized.replace(/[\s_]+/g, "-");
|
|
1399
|
+
// // --------------------------------------------------------
|
|
1400
|
+
// // Lowercase final result
|
|
1401
|
+
// // --------------------------------------------------------
|
|
1402
|
+
// normalized = normalized.toLowerCase();
|
|
1403
|
+
// // --------------------------------------------------------
|
|
1404
|
+
// // Return normalized value
|
|
1405
|
+
// // --------------------------------------------------------
|
|
1406
|
+
// return normalized;
|
|
1407
|
+
// }
|
|
1408
|
+
// // ----------------------------------------------------------
|
|
1409
|
+
// // STEP 2:
|
|
1410
|
+
// // Normalize component name
|
|
1411
|
+
// // ----------------------------------------------------------
|
|
1412
|
+
// const normalizedComponentName = useKebabCase
|
|
1413
|
+
// ? normalize(componentName)
|
|
1414
|
+
// : componentName;
|
|
1415
|
+
// // ----------------------------------------------------------
|
|
1416
|
+
// // STEP 3:
|
|
1417
|
+
// // Start class name with component scope
|
|
1418
|
+
// //
|
|
1419
|
+
// // Example:
|
|
1420
|
+
// //
|
|
1421
|
+
// // header
|
|
1422
|
+
// // ----------------------------------------------------------
|
|
1423
|
+
// let className = normalizedComponentName;
|
|
1424
|
+
// // ----------------------------------------------------------
|
|
1425
|
+
// // STEP 4:
|
|
1426
|
+
// // Add element segment
|
|
1427
|
+
// //
|
|
1428
|
+
// // Example:
|
|
1429
|
+
// //
|
|
1430
|
+
// // header__title
|
|
1431
|
+
// // ----------------------------------------------------------
|
|
1432
|
+
// if (elementName) {
|
|
1433
|
+
// const normalizedElementName = useKebabCase
|
|
1434
|
+
// ? normalize(elementName)
|
|
1435
|
+
// : elementName;
|
|
1436
|
+
// className += `__${normalizedElementName}`;
|
|
1437
|
+
// }
|
|
1438
|
+
// // ----------------------------------------------------------
|
|
1439
|
+
// // STEP 5:
|
|
1440
|
+
// // Add modifier segment
|
|
1441
|
+
// //
|
|
1442
|
+
// // Example:
|
|
1443
|
+
// //
|
|
1444
|
+
// // header__title--active
|
|
1445
|
+
// // ----------------------------------------------------------
|
|
1446
|
+
// if (modifier) {
|
|
1447
|
+
// const normalizedModifier = useKebabCase ? normalize(modifier) : modifier;
|
|
1448
|
+
// className += `--${normalizedModifier}`;
|
|
1449
|
+
// }
|
|
1450
|
+
// // ----------------------------------------------------------
|
|
1451
|
+
// // STEP 6:
|
|
1452
|
+
// // Return generated class name
|
|
1453
|
+
// // ----------------------------------------------------------
|
|
1454
|
+
// return { className };
|
|
1455
|
+
// }
|
|
1456
|
+
/**
|
|
1457
|
+
* Synchronizes JSX class usage
|
|
1458
|
+
* with component CSS styles.
|
|
1459
|
+
*
|
|
1460
|
+
* ------------------------------------------------------------
|
|
1461
|
+
* WHAT THIS FUNCTION DOES
|
|
1462
|
+
* ------------------------------------------------------------
|
|
1463
|
+
*
|
|
1464
|
+
* This function ensures that:
|
|
1465
|
+
*
|
|
1466
|
+
* - JSX classNames
|
|
1467
|
+
* - CSS selectors
|
|
1468
|
+
*
|
|
1469
|
+
* stay synchronized.
|
|
1470
|
+
*
|
|
1471
|
+
* It performs:
|
|
1472
|
+
*
|
|
1473
|
+
* 1. Detect class names used in JSX
|
|
1474
|
+
* 2. Detect class names defined in CSS
|
|
1475
|
+
* 3. Find missing CSS classes
|
|
1476
|
+
* 4. Auto-generate missing CSS blocks
|
|
1477
|
+
* 5. Find orphan CSS classes
|
|
1478
|
+
*
|
|
1479
|
+
* ------------------------------------------------------------
|
|
1480
|
+
* WHY THIS FUNCTION EXISTS
|
|
1481
|
+
* ------------------------------------------------------------
|
|
1482
|
+
*
|
|
1483
|
+
* AI-generated UI code can easily create:
|
|
1484
|
+
*
|
|
1485
|
+
* - missing styles
|
|
1486
|
+
* - orphan selectors
|
|
1487
|
+
* - inconsistent naming
|
|
1488
|
+
* - broken class references
|
|
1489
|
+
*
|
|
1490
|
+
* Example:
|
|
1491
|
+
*
|
|
1492
|
+
* JSX:
|
|
1493
|
+
*
|
|
1494
|
+
* <button className="header__button" />
|
|
1495
|
+
*
|
|
1496
|
+
* CSS:
|
|
1497
|
+
*
|
|
1498
|
+
* // missing .header__button
|
|
1499
|
+
*
|
|
1500
|
+
* This causes:
|
|
1501
|
+
*
|
|
1502
|
+
* - unstyled UI
|
|
1503
|
+
* - inconsistent rendering
|
|
1504
|
+
* - architecture drift
|
|
1505
|
+
*
|
|
1506
|
+
* This function prevents that.
|
|
1507
|
+
*
|
|
1508
|
+
* ------------------------------------------------------------
|
|
1509
|
+
* IMPORTANT DESIGN DECISION
|
|
1510
|
+
* ------------------------------------------------------------
|
|
1511
|
+
*
|
|
1512
|
+
* This function focuses ONLY on:
|
|
1513
|
+
*
|
|
1514
|
+
* synchronization.
|
|
1515
|
+
*
|
|
1516
|
+
* It does NOT:
|
|
1517
|
+
*
|
|
1518
|
+
* - generate advanced styles
|
|
1519
|
+
* - optimize css
|
|
1520
|
+
* - merge selectors
|
|
1521
|
+
* - infer design systems
|
|
1522
|
+
*
|
|
1523
|
+
* It only ensures:
|
|
1524
|
+
*
|
|
1525
|
+
* JSX ↔ CSS consistency.
|
|
1526
|
+
*
|
|
1527
|
+
* ------------------------------------------------------------
|
|
1528
|
+
* DEPENDENCIES
|
|
1529
|
+
* ------------------------------------------------------------
|
|
1530
|
+
*
|
|
1531
|
+
* This function assumes:
|
|
1532
|
+
*
|
|
1533
|
+
* - ensureStyleFile()
|
|
1534
|
+
* - insertStyles()
|
|
1535
|
+
* - updateStyles()
|
|
1536
|
+
* - removeStyles()
|
|
1537
|
+
* - renameCssClass()
|
|
1538
|
+
* - generateClassNames()
|
|
1539
|
+
*
|
|
1540
|
+
* already exist.
|
|
1541
|
+
*
|
|
1542
|
+
* ------------------------------------------------------------
|
|
1543
|
+
* WHERE THIS FUNCTION IS USEFUL
|
|
1544
|
+
* ------------------------------------------------------------
|
|
1545
|
+
*
|
|
1546
|
+
* Useful in commands like:
|
|
1547
|
+
*
|
|
1548
|
+
* - "create navbar"
|
|
1549
|
+
* - "generate styles"
|
|
1550
|
+
* - "extract component"
|
|
1551
|
+
* - "cleanup component"
|
|
1552
|
+
* - "normalize css"
|
|
1553
|
+
* - "sync styles"
|
|
1554
|
+
* - "fix missing styles"
|
|
1555
|
+
*
|
|
1556
|
+
* Usually executed:
|
|
1557
|
+
*
|
|
1558
|
+
* AFTER JSX mutations.
|
|
1559
|
+
*
|
|
1560
|
+
* ------------------------------------------------------------
|
|
1561
|
+
* IMPORTANT LIMITATION
|
|
1562
|
+
* ------------------------------------------------------------
|
|
1563
|
+
*
|
|
1564
|
+
* Current implementation supports:
|
|
1565
|
+
*
|
|
1566
|
+
* - className="..."
|
|
1567
|
+
* - class="..."
|
|
1568
|
+
*
|
|
1569
|
+
* It does NOT fully support:
|
|
1570
|
+
*
|
|
1571
|
+
* - clsx()
|
|
1572
|
+
* - classnames()
|
|
1573
|
+
* - template literals
|
|
1574
|
+
* - dynamic class generation
|
|
1575
|
+
*
|
|
1576
|
+
* Those require AST-level JSX analysis.
|
|
1577
|
+
*
|
|
1578
|
+
* ------------------------------------------------------------
|
|
1579
|
+
* PARAMS
|
|
1580
|
+
* ------------------------------------------------------------
|
|
1581
|
+
*
|
|
1582
|
+
* @param {Object} params
|
|
1583
|
+
*
|
|
1584
|
+
* @param {string} params.componentPath
|
|
1585
|
+
* JSX component file path.
|
|
1586
|
+
*
|
|
1587
|
+
* @param {string} params.cssPath
|
|
1588
|
+
* CSS file path.
|
|
1589
|
+
*
|
|
1590
|
+
* @param {boolean} [params.removeOrphanStyles=false]
|
|
1591
|
+
* Whether unused css selectors should be removed.
|
|
1592
|
+
*
|
|
1593
|
+
* ------------------------------------------------------------
|
|
1594
|
+
* RETURNS
|
|
1595
|
+
* ------------------------------------------------------------
|
|
1596
|
+
*
|
|
1597
|
+
* @returns {Object}
|
|
1598
|
+
*
|
|
1599
|
+
* {
|
|
1600
|
+
* missingClasses: string[], // when className is present in JSX but missing in CSS
|
|
1601
|
+
* orphanClasses: string[], // when class is defined in CSS but not used in JSX
|
|
1602
|
+
* insertedClasses: string[], // when this function auto-generates missing CSS blocks
|
|
1603
|
+
* removedClasses: string[] // when this function removes orphan CSS blocks (if enabled)
|
|
1604
|
+
* }
|
|
1605
|
+
*
|
|
1606
|
+
*/
|
|
1607
|
+
// function syncComponentStyles({
|
|
1608
|
+
// componentPath,
|
|
1609
|
+
// cssPath,
|
|
1610
|
+
// removeOrphanStyles = false,
|
|
1611
|
+
// }) {
|
|
1612
|
+
// // ----------------------------------------------------------
|
|
1613
|
+
// // STEP 1:
|
|
1614
|
+
// // Validate component file existence
|
|
1615
|
+
// // ----------------------------------------------------------
|
|
1616
|
+
// if (!fs.existsSync(componentPath)) {
|
|
1617
|
+
// throw new Error(`Component file does not exist: ${componentPath}`);
|
|
1618
|
+
// }
|
|
1619
|
+
// // ----------------------------------------------------------
|
|
1620
|
+
// // STEP 2:
|
|
1621
|
+
// // Validate css file existence
|
|
1622
|
+
// // ----------------------------------------------------------
|
|
1623
|
+
// if (!fs.existsSync(cssPath)) {
|
|
1624
|
+
// throw new Error(`CSS file does not exist: ${cssPath}`);
|
|
1625
|
+
// }
|
|
1626
|
+
// // ----------------------------------------------------------
|
|
1627
|
+
// // STEP 3:
|
|
1628
|
+
// // Read JSX/component source code
|
|
1629
|
+
// // ----------------------------------------------------------
|
|
1630
|
+
// const componentCode = fs.readFileSync(componentPath, "utf8");
|
|
1631
|
+
// // ----------------------------------------------------------
|
|
1632
|
+
// // STEP 4:
|
|
1633
|
+
// // Read CSS source code
|
|
1634
|
+
// // ----------------------------------------------------------
|
|
1635
|
+
// const cssCode = fs.readFileSync(cssPath, "utf8");
|
|
1636
|
+
// // ----------------------------------------------------------
|
|
1637
|
+
// // STEP 5:
|
|
1638
|
+
// // Extract class names from JSX
|
|
1639
|
+
// //
|
|
1640
|
+
// // Handles:
|
|
1641
|
+
// //
|
|
1642
|
+
// // className="..."
|
|
1643
|
+
// // class="..."
|
|
1644
|
+
// // ----------------------------------------------------------
|
|
1645
|
+
// const jsxClassRegex = /class(Name)?=["'`]([^"'`]+)["'`]/g;
|
|
1646
|
+
// // ----------------------------------------------------------
|
|
1647
|
+
// // Store unique JSX classes
|
|
1648
|
+
// // ----------------------------------------------------------
|
|
1649
|
+
// const jsxClasses = new Set();
|
|
1650
|
+
// // ----------------------------------------------------------
|
|
1651
|
+
// // Iterate JSX matches
|
|
1652
|
+
// // ----------------------------------------------------------
|
|
1653
|
+
// let jsxMatch;
|
|
1654
|
+
// while ((jsxMatch = jsxClassRegex.exec(componentCode)) !== null) {
|
|
1655
|
+
// // --------------------------------------------------------
|
|
1656
|
+
// // Extract full class string
|
|
1657
|
+
// //
|
|
1658
|
+
// // Example:
|
|
1659
|
+
// //
|
|
1660
|
+
// // "header button active"
|
|
1661
|
+
// // --------------------------------------------------------
|
|
1662
|
+
// const classString = jsxMatch[2];
|
|
1663
|
+
// // --------------------------------------------------------
|
|
1664
|
+
// // Split multiple classes
|
|
1665
|
+
// // --------------------------------------------------------
|
|
1666
|
+
// classString.split(/\s+/).forEach((className) => {
|
|
1667
|
+
// // ----------------------------------------------------
|
|
1668
|
+
// // Ignore empty values
|
|
1669
|
+
// // ----------------------------------------------------
|
|
1670
|
+
// if (!className.trim()) {
|
|
1671
|
+
// return;
|
|
1672
|
+
// }
|
|
1673
|
+
// jsxClasses.add(className.trim());
|
|
1674
|
+
// });
|
|
1675
|
+
// }
|
|
1676
|
+
// // ----------------------------------------------------------
|
|
1677
|
+
// // STEP 6:
|
|
1678
|
+
// // Extract css selectors
|
|
1679
|
+
// //
|
|
1680
|
+
// // Example:
|
|
1681
|
+
// //
|
|
1682
|
+
// // .header__button
|
|
1683
|
+
// // ----------------------------------------------------------
|
|
1684
|
+
// const cssSelectorRegex = /\.([a-zA-Z0-9_-]+)/g;
|
|
1685
|
+
// // ----------------------------------------------------------
|
|
1686
|
+
// // Store unique CSS classes
|
|
1687
|
+
// // ----------------------------------------------------------
|
|
1688
|
+
// const cssClasses = new Set();
|
|
1689
|
+
// let cssMatch;
|
|
1690
|
+
// while ((cssMatch = cssSelectorRegex.exec(cssCode)) !== null) {
|
|
1691
|
+
// cssClasses.add(cssMatch[1]);
|
|
1692
|
+
// }
|
|
1693
|
+
// // ----------------------------------------------------------
|
|
1694
|
+
// // STEP 7:
|
|
1695
|
+
// // Detect missing classes
|
|
1696
|
+
// //
|
|
1697
|
+
// // Present in JSX
|
|
1698
|
+
// // Missing in CSS
|
|
1699
|
+
// // ----------------------------------------------------------
|
|
1700
|
+
// const missingClasses = [];
|
|
1701
|
+
// jsxClasses.forEach((className) => {
|
|
1702
|
+
// if (!cssClasses.has(className)) {
|
|
1703
|
+
// missingClasses.push(className);
|
|
1704
|
+
// }
|
|
1705
|
+
// });
|
|
1706
|
+
// // ----------------------------------------------------------
|
|
1707
|
+
// // STEP 8:
|
|
1708
|
+
// // Detect orphan classes
|
|
1709
|
+
// //
|
|
1710
|
+
// // Present in CSS
|
|
1711
|
+
// // Missing in JSX
|
|
1712
|
+
// // ----------------------------------------------------------
|
|
1713
|
+
// const orphanClasses = [];
|
|
1714
|
+
// cssClasses.forEach((className) => {
|
|
1715
|
+
// if (!jsxClasses.has(className)) {
|
|
1716
|
+
// orphanClasses.push(className);
|
|
1717
|
+
// }
|
|
1718
|
+
// });
|
|
1719
|
+
// // ----------------------------------------------------------
|
|
1720
|
+
// // STEP 9:
|
|
1721
|
+
// // Insert missing css blocks
|
|
1722
|
+
// // ----------------------------------------------------------
|
|
1723
|
+
// const insertedClasses = [];
|
|
1724
|
+
// if (missingClasses.length > 0) {
|
|
1725
|
+
// // --------------------------------------------------------
|
|
1726
|
+
// // Generate css blocks
|
|
1727
|
+
// // --------------------------------------------------------
|
|
1728
|
+
// const generatedStyles = missingClasses
|
|
1729
|
+
// .map(
|
|
1730
|
+
// (className) => `
|
|
1731
|
+
// .${className} {
|
|
1732
|
+
// }
|
|
1733
|
+
// `
|
|
1734
|
+
// )
|
|
1735
|
+
// .join("\n");
|
|
1736
|
+
// // --------------------------------------------------------
|
|
1737
|
+
// // Append generated styles
|
|
1738
|
+
// // --------------------------------------------------------
|
|
1739
|
+
// fs.appendFileSync(cssPath, `\n\n${generatedStyles}`, "utf8");
|
|
1740
|
+
// insertedClasses.push(...missingClasses);
|
|
1741
|
+
// }
|
|
1742
|
+
// // ----------------------------------------------------------
|
|
1743
|
+
// // STEP 10:
|
|
1744
|
+
// // Remove orphan styles if enabled
|
|
1745
|
+
// // ----------------------------------------------------------
|
|
1746
|
+
// const removedClasses = [];
|
|
1747
|
+
// if (removeOrphanStyles && orphanClasses.length > 0) {
|
|
1748
|
+
// let updatedCssCode = cssCode;
|
|
1749
|
+
// orphanClasses.forEach((className) => {
|
|
1750
|
+
// // ----------------------------------------------------
|
|
1751
|
+
// // Match entire css block
|
|
1752
|
+
// // ----------------------------------------------------
|
|
1753
|
+
// const blockRegex = new RegExp(`\\.${className}\\s*\\{[\\s\\S]*?\\}`, "g");
|
|
1754
|
+
// // ----------------------------------------------------
|
|
1755
|
+
// // Remove css block
|
|
1756
|
+
// // ----------------------------------------------------
|
|
1757
|
+
// updatedCssCode = updatedCssCode.replace(blockRegex, "");
|
|
1758
|
+
// removedClasses.push(className);
|
|
1759
|
+
// });
|
|
1760
|
+
// // --------------------------------------------------------
|
|
1761
|
+
// // Cleanup excessive spacing
|
|
1762
|
+
// // --------------------------------------------------------
|
|
1763
|
+
// updatedCssCode = updatedCssCode.replace(/\n{3,}/g, "\n\n");
|
|
1764
|
+
// // --------------------------------------------------------
|
|
1765
|
+
// // Write cleaned css
|
|
1766
|
+
// // --------------------------------------------------------
|
|
1767
|
+
// fs.writeFileSync(cssPath, updatedCssCode.trim(), "utf8");
|
|
1768
|
+
// }
|
|
1769
|
+
// // ----------------------------------------------------------
|
|
1770
|
+
// // STEP 11:
|
|
1771
|
+
// // Return synchronization summary
|
|
1772
|
+
// // ----------------------------------------------------------
|
|
1773
|
+
// return {
|
|
1774
|
+
// missingClasses,
|
|
1775
|
+
// orphanClasses,
|
|
1776
|
+
// insertedClasses,
|
|
1777
|
+
// removedClasses,
|
|
1778
|
+
// };
|
|
1779
|
+
// }
|
|
1780
|
+
/**
|
|
1781
|
+
* Resolves CSS class naming conflicts
|
|
1782
|
+
* inside component CSS and JSX files.
|
|
1783
|
+
*
|
|
1784
|
+
* ------------------------------------------------------------
|
|
1785
|
+
* WHAT THIS FUNCTION DOES
|
|
1786
|
+
* ------------------------------------------------------------
|
|
1787
|
+
*
|
|
1788
|
+
* This function detects duplicate or conflicting
|
|
1789
|
+
* css class names and renames them safely.
|
|
1790
|
+
*
|
|
1791
|
+
* It synchronizes changes across:
|
|
1792
|
+
*
|
|
1793
|
+
* - CSS file
|
|
1794
|
+
* - JSX component file
|
|
1795
|
+
*
|
|
1796
|
+
* Example:
|
|
1797
|
+
*
|
|
1798
|
+
* BEFORE:
|
|
1799
|
+
*
|
|
1800
|
+
* Header.css
|
|
1801
|
+
*
|
|
1802
|
+
* .button {
|
|
1803
|
+
* color: red;
|
|
1804
|
+
* }
|
|
1805
|
+
*
|
|
1806
|
+
* Card.css
|
|
1807
|
+
*
|
|
1808
|
+
* .button {
|
|
1809
|
+
* color: blue;
|
|
1810
|
+
* }
|
|
1811
|
+
*
|
|
1812
|
+
* AFTER:
|
|
1813
|
+
*
|
|
1814
|
+
* Header.css
|
|
1815
|
+
*
|
|
1816
|
+
* .header__button {
|
|
1817
|
+
* color: red;
|
|
1818
|
+
* }
|
|
1819
|
+
*
|
|
1820
|
+
* JSX:
|
|
1821
|
+
*
|
|
1822
|
+
* className="header__button"
|
|
1823
|
+
*
|
|
1824
|
+
* ------------------------------------------------------------
|
|
1825
|
+
* WHY THIS FUNCTION EXISTS
|
|
1826
|
+
* ------------------------------------------------------------
|
|
1827
|
+
*
|
|
1828
|
+
* AI-generated code frequently creates:
|
|
1829
|
+
*
|
|
1830
|
+
* - generic class names
|
|
1831
|
+
* - duplicated selectors
|
|
1832
|
+
* - style leakage
|
|
1833
|
+
* - component collisions
|
|
1834
|
+
*
|
|
1835
|
+
* Example bad class names:
|
|
1836
|
+
*
|
|
1837
|
+
* .container
|
|
1838
|
+
* .wrapper
|
|
1839
|
+
* .button
|
|
1840
|
+
* .title
|
|
1841
|
+
*
|
|
1842
|
+
* At scale these become:
|
|
1843
|
+
*
|
|
1844
|
+
* architecture disasters.
|
|
1845
|
+
*
|
|
1846
|
+
* This function enforces:
|
|
1847
|
+
*
|
|
1848
|
+
* component-scoped CSS naming.
|
|
1849
|
+
*
|
|
1850
|
+
* ------------------------------------------------------------
|
|
1851
|
+
* IMPORTANT ARCHITECTURAL DECISION
|
|
1852
|
+
* ------------------------------------------------------------
|
|
1853
|
+
*
|
|
1854
|
+
* Looma should NEVER trust
|
|
1855
|
+
* raw AI-generated class names.
|
|
1856
|
+
*
|
|
1857
|
+
* Every generated class should become:
|
|
1858
|
+
*
|
|
1859
|
+
* deterministic
|
|
1860
|
+
* scoped
|
|
1861
|
+
* collision-safe
|
|
1862
|
+
*
|
|
1863
|
+
* Example:
|
|
1864
|
+
*
|
|
1865
|
+
* card__button
|
|
1866
|
+
* header__title
|
|
1867
|
+
* modal__footer
|
|
1868
|
+
*
|
|
1869
|
+
* ------------------------------------------------------------
|
|
1870
|
+
* DEPENDENCIES
|
|
1871
|
+
* ------------------------------------------------------------
|
|
1872
|
+
*
|
|
1873
|
+
* This function assumes:
|
|
1874
|
+
*
|
|
1875
|
+
* - parseCSS()
|
|
1876
|
+
* - findCssSelector()
|
|
1877
|
+
* - renameCssClass()
|
|
1878
|
+
* - generateClassNames()
|
|
1879
|
+
* - updateStyles()
|
|
1880
|
+
*
|
|
1881
|
+
* already exist.
|
|
1882
|
+
*
|
|
1883
|
+
* ------------------------------------------------------------
|
|
1884
|
+
* WHERE THIS FUNCTION IS USEFUL
|
|
1885
|
+
* ------------------------------------------------------------
|
|
1886
|
+
*
|
|
1887
|
+
* Useful in commands like:
|
|
1888
|
+
*
|
|
1889
|
+
* - "create component"
|
|
1890
|
+
* - "extract component"
|
|
1891
|
+
* - "cleanup css"
|
|
1892
|
+
* - "normalize styles"
|
|
1893
|
+
* - "fix css conflicts"
|
|
1894
|
+
* - "merge components"
|
|
1895
|
+
* - "generate UI"
|
|
1896
|
+
*
|
|
1897
|
+
* Usually executed AFTER:
|
|
1898
|
+
*
|
|
1899
|
+
* - component generation
|
|
1900
|
+
* - JSX insertion
|
|
1901
|
+
* - style generation
|
|
1902
|
+
*
|
|
1903
|
+
* ------------------------------------------------------------
|
|
1904
|
+
* IMPORTANT NOTE
|
|
1905
|
+
* ------------------------------------------------------------
|
|
1906
|
+
*
|
|
1907
|
+
* Current implementation supports:
|
|
1908
|
+
*
|
|
1909
|
+
* - static class names
|
|
1910
|
+
* - basic selectors
|
|
1911
|
+
*
|
|
1912
|
+
* It does NOT fully support:
|
|
1913
|
+
*
|
|
1914
|
+
* - clsx()
|
|
1915
|
+
* - CSS modules
|
|
1916
|
+
* - Tailwind
|
|
1917
|
+
* - styled-components
|
|
1918
|
+
* - dynamic class generation
|
|
1919
|
+
*
|
|
1920
|
+
* Those require AST-level JSX analysis.
|
|
1921
|
+
*
|
|
1922
|
+
* ------------------------------------------------------------
|
|
1923
|
+
* REQUIRED PACKAGE
|
|
1924
|
+
* ------------------------------------------------------------
|
|
1925
|
+
*
|
|
1926
|
+
* npm install postcss
|
|
1927
|
+
*
|
|
1928
|
+
* ------------------------------------------------------------
|
|
1929
|
+
* PARAMS
|
|
1930
|
+
* ------------------------------------------------------------
|
|
1931
|
+
*
|
|
1932
|
+
* @param {Object} params
|
|
1933
|
+
*
|
|
1934
|
+
* @param {string} params.componentName
|
|
1935
|
+
* Component name.
|
|
1936
|
+
*
|
|
1937
|
+
* Example:
|
|
1938
|
+
*
|
|
1939
|
+
* "Header"
|
|
1940
|
+
*
|
|
1941
|
+
* @param {string} params.cssPath
|
|
1942
|
+
* CSS file path.
|
|
1943
|
+
*
|
|
1944
|
+
* @param {string} params.componentPath
|
|
1945
|
+
* JSX component file path.
|
|
1946
|
+
*
|
|
1947
|
+
* @param {string[]} [params.genericClassNames]
|
|
1948
|
+
* Generic class names considered unsafe.
|
|
1949
|
+
*
|
|
1950
|
+
* Example:
|
|
1951
|
+
*
|
|
1952
|
+
* [
|
|
1953
|
+
* "button",
|
|
1954
|
+
* "container",
|
|
1955
|
+
* "wrapper"
|
|
1956
|
+
* ]
|
|
1957
|
+
*
|
|
1958
|
+
* ------------------------------------------------------------
|
|
1959
|
+
* RETURNS
|
|
1960
|
+
* ------------------------------------------------------------
|
|
1961
|
+
*
|
|
1962
|
+
* @returns {Object}
|
|
1963
|
+
*
|
|
1964
|
+
* {
|
|
1965
|
+
* renamedClasses: [
|
|
1966
|
+
* {
|
|
1967
|
+
* oldName,
|
|
1968
|
+
* newName
|
|
1969
|
+
* }
|
|
1970
|
+
* ]
|
|
1971
|
+
* }
|
|
1972
|
+
*
|
|
1973
|
+
*/
|
|
1974
|
+
// function resolveCssClassConflicts({
|
|
1975
|
+
// componentName,
|
|
1976
|
+
// cssPath,
|
|
1977
|
+
// componentPath,
|
|
1978
|
+
// genericClassNames = [
|
|
1979
|
+
// "container",
|
|
1980
|
+
// "wrapper",
|
|
1981
|
+
// "button",
|
|
1982
|
+
// "title",
|
|
1983
|
+
// "card",
|
|
1984
|
+
// "box",
|
|
1985
|
+
// "text",
|
|
1986
|
+
// "item",
|
|
1987
|
+
// "content",
|
|
1988
|
+
// ],
|
|
1989
|
+
// }) {
|
|
1990
|
+
// // ----------------------------------------------------------
|
|
1991
|
+
// // STEP 1:
|
|
1992
|
+
// // Validate CSS file existence
|
|
1993
|
+
// // ----------------------------------------------------------
|
|
1994
|
+
// if (!fs.existsSync(cssPath)) {
|
|
1995
|
+
// throw new Error(`CSS file does not exist: ${cssPath}`);
|
|
1996
|
+
// }
|
|
1997
|
+
// // ----------------------------------------------------------
|
|
1998
|
+
// // STEP 2:
|
|
1999
|
+
// // Validate component file existence
|
|
2000
|
+
// // ----------------------------------------------------------
|
|
2001
|
+
// if (!fs.existsSync(componentPath)) {
|
|
2002
|
+
// throw new Error(`Component file does not exist: ${componentPath}`);
|
|
2003
|
+
// }
|
|
2004
|
+
// // ----------------------------------------------------------
|
|
2005
|
+
// // STEP 3:
|
|
2006
|
+
// // Read CSS source code
|
|
2007
|
+
// // ----------------------------------------------------------
|
|
2008
|
+
// const cssCode = fs.readFileSync(cssPath, "utf8");
|
|
2009
|
+
// // ----------------------------------------------------------
|
|
2010
|
+
// // STEP 4:
|
|
2011
|
+
// // Parse CSS into AST
|
|
2012
|
+
// // ----------------------------------------------------------
|
|
2013
|
+
// const ast = postcss.parse(cssCode);
|
|
2014
|
+
// // ----------------------------------------------------------
|
|
2015
|
+
// // STEP 5:
|
|
2016
|
+
// // Store renamed classes
|
|
2017
|
+
// // ----------------------------------------------------------
|
|
2018
|
+
// const renamedClasses = [];
|
|
2019
|
+
// // ----------------------------------------------------------
|
|
2020
|
+
// // STEP 6:
|
|
2021
|
+
// // Track already processed classes
|
|
2022
|
+
// //
|
|
2023
|
+
// // Prevent duplicate renaming.
|
|
2024
|
+
// // ----------------------------------------------------------
|
|
2025
|
+
// const processedClasses = new Set();
|
|
2026
|
+
// // ----------------------------------------------------------
|
|
2027
|
+
// // STEP 7:
|
|
2028
|
+
// // Traverse all CSS rules
|
|
2029
|
+
// // ----------------------------------------------------------
|
|
2030
|
+
// ast.walkRules((rule) => {
|
|
2031
|
+
// // --------------------------------------------------------
|
|
2032
|
+
// // Extract selector
|
|
2033
|
+
// //
|
|
2034
|
+
// // Example:
|
|
2035
|
+
// //
|
|
2036
|
+
// // ".button"
|
|
2037
|
+
// // --------------------------------------------------------
|
|
2038
|
+
// const selector = rule.selector;
|
|
2039
|
+
// // --------------------------------------------------------
|
|
2040
|
+
// // Extract class name
|
|
2041
|
+
// //
|
|
2042
|
+
// // Example:
|
|
2043
|
+
// //
|
|
2044
|
+
// // button
|
|
2045
|
+
// // --------------------------------------------------------
|
|
2046
|
+
// const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/);
|
|
2047
|
+
// // --------------------------------------------------------
|
|
2048
|
+
// // Skip selectors without class
|
|
2049
|
+
// // --------------------------------------------------------
|
|
2050
|
+
// if (!classMatch) {
|
|
2051
|
+
// return;
|
|
2052
|
+
// }
|
|
2053
|
+
// // --------------------------------------------------------
|
|
2054
|
+
// // Extract class name
|
|
2055
|
+
// // --------------------------------------------------------
|
|
2056
|
+
// const oldClassName = classMatch[1];
|
|
2057
|
+
// // --------------------------------------------------------
|
|
2058
|
+
// // Skip already processed classes
|
|
2059
|
+
// // --------------------------------------------------------
|
|
2060
|
+
// if (processedClasses.has(oldClassName)) {
|
|
2061
|
+
// return;
|
|
2062
|
+
// }
|
|
2063
|
+
// // --------------------------------------------------------
|
|
2064
|
+
// // Mark class as processed
|
|
2065
|
+
// // --------------------------------------------------------
|
|
2066
|
+
// processedClasses.add(oldClassName);
|
|
2067
|
+
// // --------------------------------------------------------
|
|
2068
|
+
// // Check whether class is generic
|
|
2069
|
+
// // --------------------------------------------------------
|
|
2070
|
+
// const isGeneric = genericClassNames.includes(oldClassName);
|
|
2071
|
+
// // --------------------------------------------------------
|
|
2072
|
+
// // Skip safe classes
|
|
2073
|
+
// // --------------------------------------------------------
|
|
2074
|
+
// if (!isGeneric) {
|
|
2075
|
+
// return;
|
|
2076
|
+
// }
|
|
2077
|
+
// // --------------------------------------------------------
|
|
2078
|
+
// // Generate deterministic scoped class
|
|
2079
|
+
// //
|
|
2080
|
+
// // Example:
|
|
2081
|
+
// //
|
|
2082
|
+
// // button
|
|
2083
|
+
// // →
|
|
2084
|
+
// // header__button
|
|
2085
|
+
// // --------------------------------------------------------
|
|
2086
|
+
// const newClassName = `${componentName.toLowerCase()}__${oldClassName}`;
|
|
2087
|
+
// // --------------------------------------------------------
|
|
2088
|
+
// // Rename class in CSS
|
|
2089
|
+
// // --------------------------------------------------------
|
|
2090
|
+
// const updatedSelector = selector.replace(
|
|
2091
|
+
// `.${oldClassName}`,
|
|
2092
|
+
// `.${newClassName}`
|
|
2093
|
+
// );
|
|
2094
|
+
// rule.selector = updatedSelector;
|
|
2095
|
+
// // --------------------------------------------------------
|
|
2096
|
+
// // Rename class in JSX/component file
|
|
2097
|
+
// // --------------------------------------------------------
|
|
2098
|
+
// const componentCode = fs.readFileSync(componentPath, "utf8");
|
|
2099
|
+
// // --------------------------------------------------------
|
|
2100
|
+
// // Replace JSX class usage
|
|
2101
|
+
// // --------------------------------------------------------
|
|
2102
|
+
// const updatedComponentCode = componentCode.replace(
|
|
2103
|
+
// new RegExp(`\\b${oldClassName}\\b`, "g"),
|
|
2104
|
+
// newClassName
|
|
2105
|
+
// );
|
|
2106
|
+
// // --------------------------------------------------------
|
|
2107
|
+
// // Write updated JSX/component file
|
|
2108
|
+
// // --------------------------------------------------------
|
|
2109
|
+
// fs.writeFileSync(componentPath, updatedComponentCode, "utf8");
|
|
2110
|
+
// // --------------------------------------------------------
|
|
2111
|
+
// // Store rename result
|
|
2112
|
+
// // --------------------------------------------------------
|
|
2113
|
+
// renamedClasses.push({
|
|
2114
|
+
// oldName: oldClassName,
|
|
2115
|
+
// newName: newClassName,
|
|
2116
|
+
// });
|
|
2117
|
+
// });
|
|
2118
|
+
// // ----------------------------------------------------------
|
|
2119
|
+
// // STEP 8:
|
|
2120
|
+
// // Write updated CSS back to file
|
|
2121
|
+
// // ----------------------------------------------------------
|
|
2122
|
+
// fs.writeFileSync(cssPath, ast.toString(), "utf8");
|
|
2123
|
+
// // ----------------------------------------------------------
|
|
2124
|
+
// // STEP 9:
|
|
2125
|
+
// // Return rename summary
|
|
2126
|
+
// // ----------------------------------------------------------
|
|
2127
|
+
// return {
|
|
2128
|
+
// renamedClasses,
|
|
2129
|
+
// };
|
|
2130
|
+
// }
|
|
2131
|
+
/**
|
|
2132
|
+
* Resolves style dependencies used by a component.
|
|
2133
|
+
*
|
|
2134
|
+
* ------------------------------------------------------------
|
|
2135
|
+
* WHAT THIS FUNCTION DOES
|
|
2136
|
+
* ------------------------------------------------------------
|
|
2137
|
+
*
|
|
2138
|
+
* This function analyzes:
|
|
2139
|
+
*
|
|
2140
|
+
* - CSS imports
|
|
2141
|
+
* - font imports
|
|
2142
|
+
* - variable usage
|
|
2143
|
+
* - animation usage
|
|
2144
|
+
* - media query dependencies
|
|
2145
|
+
* - shared utility styles
|
|
2146
|
+
* - root CSS variables
|
|
2147
|
+
*
|
|
2148
|
+
* and ensures required style dependencies
|
|
2149
|
+
* are properly connected.
|
|
2150
|
+
*
|
|
2151
|
+
* Example:
|
|
2152
|
+
*
|
|
2153
|
+
* BEFORE:
|
|
2154
|
+
*
|
|
2155
|
+
* Button.css
|
|
2156
|
+
*
|
|
2157
|
+
* .button {
|
|
2158
|
+
* color: var(--primary-color);
|
|
2159
|
+
* }
|
|
2160
|
+
*
|
|
2161
|
+
* But:
|
|
2162
|
+
*
|
|
2163
|
+
* variables.css is NOT imported.
|
|
2164
|
+
*
|
|
2165
|
+
* AFTER:
|
|
2166
|
+
*
|
|
2167
|
+
* variables.css gets imported automatically.
|
|
2168
|
+
*
|
|
2169
|
+
* ------------------------------------------------------------
|
|
2170
|
+
* WHY THIS FUNCTION EXISTS
|
|
2171
|
+
* ------------------------------------------------------------
|
|
2172
|
+
*
|
|
2173
|
+
* AI-generated CSS often creates:
|
|
2174
|
+
*
|
|
2175
|
+
* - missing variables
|
|
2176
|
+
* - broken animations
|
|
2177
|
+
* - undefined fonts
|
|
2178
|
+
* - disconnected utility classes
|
|
2179
|
+
* - missing shared styles
|
|
2180
|
+
*
|
|
2181
|
+
* which causes:
|
|
2182
|
+
*
|
|
2183
|
+
* broken runtime UI.
|
|
2184
|
+
*
|
|
2185
|
+
* This function acts as:
|
|
2186
|
+
*
|
|
2187
|
+
* style dependency synchronization layer.
|
|
2188
|
+
*
|
|
2189
|
+
* ------------------------------------------------------------
|
|
2190
|
+
* IMPORTANT ARCHITECTURAL DECISION
|
|
2191
|
+
* ------------------------------------------------------------
|
|
2192
|
+
*
|
|
2193
|
+
* Looma should NEVER allow:
|
|
2194
|
+
*
|
|
2195
|
+
* orphaned style references.
|
|
2196
|
+
*
|
|
2197
|
+
* Every style dependency should be:
|
|
2198
|
+
*
|
|
2199
|
+
* - traceable
|
|
2200
|
+
* - deterministic
|
|
2201
|
+
* - auto-repairable
|
|
2202
|
+
*
|
|
2203
|
+
* because runtime editing requires:
|
|
2204
|
+
*
|
|
2205
|
+
* predictable styling architecture.
|
|
2206
|
+
*
|
|
2207
|
+
* ------------------------------------------------------------
|
|
2208
|
+
* IMPORTANT NOTE
|
|
2209
|
+
* ------------------------------------------------------------
|
|
2210
|
+
*
|
|
2211
|
+
* Current implementation supports:
|
|
2212
|
+
*
|
|
2213
|
+
* - CSS variable detection
|
|
2214
|
+
* - @import detection
|
|
2215
|
+
* - animation detection
|
|
2216
|
+
*
|
|
2217
|
+
* Production version should later support:
|
|
2218
|
+
*
|
|
2219
|
+
* - Tailwind dependency graphs
|
|
2220
|
+
* - CSS modules
|
|
2221
|
+
* - CSS-in-JS
|
|
2222
|
+
* - design token systems
|
|
2223
|
+
* - theme inheritance
|
|
2224
|
+
*
|
|
2225
|
+
* ------------------------------------------------------------
|
|
2226
|
+
* DEPENDENCIES
|
|
2227
|
+
* ------------------------------------------------------------
|
|
2228
|
+
*
|
|
2229
|
+
* This function complements:
|
|
2230
|
+
*
|
|
2231
|
+
* - parseCSS()
|
|
2232
|
+
* - findCssSelector()
|
|
2233
|
+
* - ensureStyleFile()
|
|
2234
|
+
* - insertStyles()
|
|
2235
|
+
* - updateStyles()
|
|
2236
|
+
* - removeStyles()
|
|
2237
|
+
* - renameCssClass()
|
|
2238
|
+
* - resolveCssClassConflicts()
|
|
2239
|
+
*
|
|
2240
|
+
* ------------------------------------------------------------
|
|
2241
|
+
* WHERE THIS FUNCTION IS USEFUL
|
|
2242
|
+
* ------------------------------------------------------------
|
|
2243
|
+
*
|
|
2244
|
+
* Useful in commands like:
|
|
2245
|
+
*
|
|
2246
|
+
* - "create component"
|
|
2247
|
+
* - "generate styles"
|
|
2248
|
+
* - "extract component"
|
|
2249
|
+
* - "move component"
|
|
2250
|
+
* - "repair styling"
|
|
2251
|
+
* - "fix css variables"
|
|
2252
|
+
* - "cleanup styles"
|
|
2253
|
+
*
|
|
2254
|
+
* Usually executed AFTER:
|
|
2255
|
+
*
|
|
2256
|
+
* - style generation
|
|
2257
|
+
* - css insertion
|
|
2258
|
+
* - component extraction
|
|
2259
|
+
*
|
|
2260
|
+
* ------------------------------------------------------------
|
|
2261
|
+
* REQUIRED PACKAGE
|
|
2262
|
+
* ------------------------------------------------------------
|
|
2263
|
+
*
|
|
2264
|
+
* npm install postcss
|
|
2265
|
+
*
|
|
2266
|
+
* ------------------------------------------------------------
|
|
2267
|
+
* PARAMS
|
|
2268
|
+
* ------------------------------------------------------------
|
|
2269
|
+
*
|
|
2270
|
+
* @param {Object} params
|
|
2271
|
+
*
|
|
2272
|
+
* @param {string} params.cssPath
|
|
2273
|
+
* Component css file path.
|
|
2274
|
+
*
|
|
2275
|
+
* Example:
|
|
2276
|
+
*
|
|
2277
|
+
* "./src/components/Button/Button.css"
|
|
2278
|
+
*
|
|
2279
|
+
* @param {string[]} [params.availableDependencyFiles]
|
|
2280
|
+
* List of available shared style files.
|
|
2281
|
+
*
|
|
2282
|
+
* Example:
|
|
2283
|
+
*
|
|
2284
|
+
* [
|
|
2285
|
+
* "./src/styles/variables.css",
|
|
2286
|
+
* "./src/styles/animations.css"
|
|
2287
|
+
* ]
|
|
2288
|
+
*
|
|
2289
|
+
* @param {boolean} [params.autoImport=true]
|
|
2290
|
+
* Whether missing dependencies should
|
|
2291
|
+
* be imported automatically.
|
|
2292
|
+
*
|
|
2293
|
+
* ------------------------------------------------------------
|
|
2294
|
+
* RETURNS
|
|
2295
|
+
* ------------------------------------------------------------
|
|
2296
|
+
*
|
|
2297
|
+
* @returns {Object}
|
|
2298
|
+
*
|
|
2299
|
+
* {
|
|
2300
|
+
* resolvedDependencies: string[],
|
|
2301
|
+
* missingDependencies: string[],
|
|
2302
|
+
* insertedImports: string[]
|
|
2303
|
+
* }
|
|
2304
|
+
*
|
|
2305
|
+
*/
|
|
2306
|
+
function resolveStyleDependencies({ cssPath, availableDependencyFiles = [], autoImport = true, }) {
|
|
2307
|
+
// ----------------------------------------------------------
|
|
2308
|
+
// STEP 1:
|
|
2309
|
+
// Resolve absolute css path
|
|
2310
|
+
// ----------------------------------------------------------
|
|
2311
|
+
const absoluteCssPath = path.resolve(cssPath);
|
|
2312
|
+
// ----------------------------------------------------------
|
|
2313
|
+
// STEP 2:
|
|
2314
|
+
// Validate css file existence
|
|
2315
|
+
// ----------------------------------------------------------
|
|
2316
|
+
if (!fs.existsSync(absoluteCssPath)) {
|
|
2317
|
+
throw new LoomaError(ERROR_CODES.FILE_NOT_FOUND, `CSS file does not exist: ${absoluteCssPath}`);
|
|
2318
|
+
}
|
|
2319
|
+
// ----------------------------------------------------------
|
|
2320
|
+
// STEP 3:
|
|
2321
|
+
// Read css source code
|
|
2322
|
+
// ----------------------------------------------------------
|
|
2323
|
+
let cssCode = fs.readFileSync(absoluteCssPath, "utf8");
|
|
2324
|
+
// ----------------------------------------------------------
|
|
2325
|
+
// STEP 4:
|
|
2326
|
+
// Parse CSS into AST
|
|
2327
|
+
// ----------------------------------------------------------
|
|
2328
|
+
const ast = postcss.parse(cssCode);
|
|
2329
|
+
// ----------------------------------------------------------
|
|
2330
|
+
// STEP 5:
|
|
2331
|
+
// Track dependencies
|
|
2332
|
+
// ----------------------------------------------------------
|
|
2333
|
+
const resolvedDependencies = [];
|
|
2334
|
+
const missingDependencies = [];
|
|
2335
|
+
const insertedImports = [];
|
|
2336
|
+
// ----------------------------------------------------------
|
|
2337
|
+
// STEP 6:
|
|
2338
|
+
// Detect CSS variable usage
|
|
2339
|
+
//
|
|
2340
|
+
// Example:
|
|
2341
|
+
//
|
|
2342
|
+
// var(--primary-color)
|
|
2343
|
+
// ----------------------------------------------------------
|
|
2344
|
+
const variableMatches = cssCode.match(/var\(--([a-zA-Z0-9-_]+)\)/g) || [];
|
|
2345
|
+
// ----------------------------------------------------------
|
|
2346
|
+
// STEP 7:
|
|
2347
|
+
// If variables are used,
|
|
2348
|
+
// ensure variables.css exists
|
|
2349
|
+
// ----------------------------------------------------------
|
|
2350
|
+
if (variableMatches.length > 0) {
|
|
2351
|
+
// --------------------------------------------------------
|
|
2352
|
+
// Try finding variables.css
|
|
2353
|
+
// --------------------------------------------------------
|
|
2354
|
+
const variablesFile = availableDependencyFiles.find((file) => {
|
|
2355
|
+
return file.includes("variables.css");
|
|
2356
|
+
});
|
|
2357
|
+
// --------------------------------------------------------
|
|
2358
|
+
// If found, resolve dependency
|
|
2359
|
+
// --------------------------------------------------------
|
|
2360
|
+
if (variablesFile) {
|
|
2361
|
+
resolvedDependencies.push(variablesFile);
|
|
2362
|
+
// ------------------------------------------------------
|
|
2363
|
+
// Auto import variables.css
|
|
2364
|
+
// ------------------------------------------------------
|
|
2365
|
+
if (autoImport && !cssCode.includes(variablesFile)) {
|
|
2366
|
+
// ----------------------------------------------------
|
|
2367
|
+
// Build relative import path
|
|
2368
|
+
// ----------------------------------------------------
|
|
2369
|
+
const relativeImportPath = path.relative(path.dirname(absoluteCssPath), variablesFile);
|
|
2370
|
+
// ----------------------------------------------------
|
|
2371
|
+
// Normalize slashes
|
|
2372
|
+
// ----------------------------------------------------
|
|
2373
|
+
const normalizedImportPath = relativeImportPath.replace(/\\/g, "/");
|
|
2374
|
+
// ----------------------------------------------------
|
|
2375
|
+
// Build import statement
|
|
2376
|
+
// ----------------------------------------------------
|
|
2377
|
+
const importStatement = `@import "${normalizedImportPath}";\n`;
|
|
2378
|
+
// ----------------------------------------------------
|
|
2379
|
+
// Insert import at top
|
|
2380
|
+
// ----------------------------------------------------
|
|
2381
|
+
cssCode = importStatement + cssCode;
|
|
2382
|
+
insertedImports.push(normalizedImportPath);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
// --------------------------------------------------------
|
|
2386
|
+
// Track missing dependency
|
|
2387
|
+
// --------------------------------------------------------
|
|
2388
|
+
else {
|
|
2389
|
+
missingDependencies.push("variables.css");
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
// ----------------------------------------------------------
|
|
2393
|
+
// STEP 8:
|
|
2394
|
+
// Detect animation usage
|
|
2395
|
+
//
|
|
2396
|
+
// Example:
|
|
2397
|
+
//
|
|
2398
|
+
// animation: fadeIn
|
|
2399
|
+
// ----------------------------------------------------------
|
|
2400
|
+
const animationMatches = cssCode.match(/animation\s*:\s*([a-zA-Z0-9_-]+)/g) || [];
|
|
2401
|
+
// ----------------------------------------------------------
|
|
2402
|
+
// STEP 9:
|
|
2403
|
+
// Resolve animations.css dependency
|
|
2404
|
+
// ----------------------------------------------------------
|
|
2405
|
+
if (animationMatches.length > 0) {
|
|
2406
|
+
// --------------------------------------------------------
|
|
2407
|
+
// Find animations.css
|
|
2408
|
+
// --------------------------------------------------------
|
|
2409
|
+
const animationsFile = availableDependencyFiles.find((file) => {
|
|
2410
|
+
return file.includes("animations.css");
|
|
2411
|
+
});
|
|
2412
|
+
// --------------------------------------------------------
|
|
2413
|
+
// If found, resolve dependency
|
|
2414
|
+
// --------------------------------------------------------
|
|
2415
|
+
if (animationsFile) {
|
|
2416
|
+
resolvedDependencies.push(animationsFile);
|
|
2417
|
+
// ------------------------------------------------------
|
|
2418
|
+
// Auto import if enabled
|
|
2419
|
+
// ------------------------------------------------------
|
|
2420
|
+
if (autoImport && !cssCode.includes(animationsFile)) {
|
|
2421
|
+
// ----------------------------------------------------
|
|
2422
|
+
// Build relative path
|
|
2423
|
+
// ----------------------------------------------------
|
|
2424
|
+
const relativeImportPath = path.relative(path.dirname(absoluteCssPath), animationsFile);
|
|
2425
|
+
// ----------------------------------------------------
|
|
2426
|
+
// Normalize path separators
|
|
2427
|
+
// ----------------------------------------------------
|
|
2428
|
+
const normalizedImportPath = relativeImportPath.replace(/\\/g, "/");
|
|
2429
|
+
// ----------------------------------------------------
|
|
2430
|
+
// Build import statement
|
|
2431
|
+
// ----------------------------------------------------
|
|
2432
|
+
const importStatement = `@import "${normalizedImportPath}";\n`;
|
|
2433
|
+
// ----------------------------------------------------
|
|
2434
|
+
// Insert import
|
|
2435
|
+
// ----------------------------------------------------
|
|
2436
|
+
cssCode = importStatement + cssCode;
|
|
2437
|
+
insertedImports.push(normalizedImportPath);
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
// --------------------------------------------------------
|
|
2441
|
+
// Track missing dependency
|
|
2442
|
+
// --------------------------------------------------------
|
|
2443
|
+
else {
|
|
2444
|
+
missingDependencies.push("animations.css");
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
// ----------------------------------------------------------
|
|
2448
|
+
// STEP 10:
|
|
2449
|
+
// Detect existing @imports
|
|
2450
|
+
// ----------------------------------------------------------
|
|
2451
|
+
ast.walkAtRules("import", (rule) => {
|
|
2452
|
+
// ------------------------------------------------------
|
|
2453
|
+
// Extract imported file
|
|
2454
|
+
// ------------------------------------------------------
|
|
2455
|
+
const importMatch = rule.params.match(/["'](.+?)["']/);
|
|
2456
|
+
// ------------------------------------------------------
|
|
2457
|
+
// Skip invalid imports
|
|
2458
|
+
// ------------------------------------------------------
|
|
2459
|
+
if (!importMatch) {
|
|
2460
|
+
return;
|
|
2461
|
+
}
|
|
2462
|
+
// ------------------------------------------------------
|
|
2463
|
+
// Extract import path
|
|
2464
|
+
// ------------------------------------------------------
|
|
2465
|
+
const importPath = importMatch[1];
|
|
2466
|
+
// ------------------------------------------------------
|
|
2467
|
+
// Register dependency
|
|
2468
|
+
// ------------------------------------------------------
|
|
2469
|
+
resolvedDependencies.push(importPath);
|
|
2470
|
+
});
|
|
2471
|
+
// ----------------------------------------------------------
|
|
2472
|
+
// STEP 11:
|
|
2473
|
+
// Remove duplicate dependencies
|
|
2474
|
+
// ----------------------------------------------------------
|
|
2475
|
+
const uniqueResolvedDependencies = [...new Set(resolvedDependencies)];
|
|
2476
|
+
const uniqueInsertedImports = [...new Set(insertedImports)];
|
|
2477
|
+
const uniqueMissingDependencies = [...new Set(missingDependencies)];
|
|
2478
|
+
// ----------------------------------------------------------
|
|
2479
|
+
// STEP 12:
|
|
2480
|
+
// Write updated css back to file
|
|
2481
|
+
// ----------------------------------------------------------
|
|
2482
|
+
fs.writeFileSync(absoluteCssPath, cssCode, "utf8");
|
|
2483
|
+
// ----------------------------------------------------------
|
|
2484
|
+
// STEP 13:
|
|
2485
|
+
// Return dependency resolution summary
|
|
2486
|
+
// ----------------------------------------------------------
|
|
2487
|
+
return {
|
|
2488
|
+
success: true,
|
|
2489
|
+
resolvedDependencies: uniqueResolvedDependencies,
|
|
2490
|
+
missingDependencies: uniqueMissingDependencies,
|
|
2491
|
+
insertedImports: uniqueInsertedImports,
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* Inserts generic code into a file.
|
|
2496
|
+
*
|
|
2497
|
+
* Useful for:
|
|
2498
|
+
* - inserting hooks
|
|
2499
|
+
* - inserting utilities
|
|
2500
|
+
* - inserting exports
|
|
2501
|
+
* - inserting config
|
|
2502
|
+
* - inserting arbitrary JS/TS code
|
|
2503
|
+
*/
|
|
2504
|
+
// function insertCode({ filePath, codeToInsert, insertAt = "end" }) {
|
|
2505
|
+
// // ----------------------------------------------------------
|
|
2506
|
+
// // STEP 1:
|
|
2507
|
+
// // Ensure file exists
|
|
2508
|
+
// // ----------------------------------------------------------
|
|
2509
|
+
// if (!fs.existsSync(filePath)) {
|
|
2510
|
+
// throw new Error(`File does not exist: ${filePath}`);
|
|
2511
|
+
// }
|
|
2512
|
+
// // ----------------------------------------------------------
|
|
2513
|
+
// // STEP 2:
|
|
2514
|
+
// // Read existing file content
|
|
2515
|
+
// // ----------------------------------------------------------
|
|
2516
|
+
// const existingCode = fs.readFileSync(filePath, "utf8");
|
|
2517
|
+
// // ----------------------------------------------------------
|
|
2518
|
+
// // STEP 3:
|
|
2519
|
+
// // Decide insertion strategy
|
|
2520
|
+
// // ----------------------------------------------------------
|
|
2521
|
+
// let updatedCode = existingCode;
|
|
2522
|
+
// // ----------------------------------------------------------
|
|
2523
|
+
// // Insert at beginning
|
|
2524
|
+
// // ----------------------------------------------------------
|
|
2525
|
+
// if (insertAt === "start") {
|
|
2526
|
+
// updatedCode = `${codeToInsert}\n\n${existingCode}`;
|
|
2527
|
+
// }
|
|
2528
|
+
// // ----------------------------------------------------------
|
|
2529
|
+
// // Insert at end
|
|
2530
|
+
// // ----------------------------------------------------------
|
|
2531
|
+
// else if (insertAt === "end") {
|
|
2532
|
+
// updatedCode = `${existingCode}\n\n${codeToInsert}`;
|
|
2533
|
+
// }
|
|
2534
|
+
// // ----------------------------------------------------------
|
|
2535
|
+
// // Unsupported insertion strategy
|
|
2536
|
+
// // ----------------------------------------------------------
|
|
2537
|
+
// else {
|
|
2538
|
+
// throw new Error(`Unsupported insertAt value: ${insertAt}`);
|
|
2539
|
+
// }
|
|
2540
|
+
// // ----------------------------------------------------------
|
|
2541
|
+
// // STEP 4:
|
|
2542
|
+
// // Write updated code back to file
|
|
2543
|
+
// // ----------------------------------------------------------
|
|
2544
|
+
// fs.writeFileSync(filePath, updatedCode, "utf8");
|
|
2545
|
+
// // ----------------------------------------------------------
|
|
2546
|
+
// // STEP 5:
|
|
2547
|
+
// // Return result
|
|
2548
|
+
// // ----------------------------------------------------------
|
|
2549
|
+
// return {
|
|
2550
|
+
// inserted: true,
|
|
2551
|
+
// filePath,
|
|
2552
|
+
// insertAt,
|
|
2553
|
+
// };
|
|
2554
|
+
// }
|
|
2555
|
+
/**
|
|
2556
|
+
* Generates source code from Babel AST.
|
|
2557
|
+
*/
|
|
2558
|
+
function generateCodeFromAST({ ast, options = {} }) {
|
|
2559
|
+
return generate(ast, {
|
|
2560
|
+
retainLines: false,
|
|
2561
|
+
compact: false,
|
|
2562
|
+
concise: false,
|
|
2563
|
+
comments: true,
|
|
2564
|
+
jsescOption: {
|
|
2565
|
+
minimal: true,
|
|
2566
|
+
},
|
|
2567
|
+
...options,
|
|
2568
|
+
}).code;
|
|
2569
|
+
}
|
|
2570
|
+
function matchesSelector({ openingElement, selector }) {
|
|
2571
|
+
// ----------------------------------------------------------
|
|
2572
|
+
// Ignore unsupported JSX names
|
|
2573
|
+
// ----------------------------------------------------------
|
|
2574
|
+
if (!t.isJSXIdentifier(openingElement.name)) {
|
|
2575
|
+
return false;
|
|
2576
|
+
}
|
|
2577
|
+
const { tagName, classes, attributes } = parsers.parseSelector(selector);
|
|
2578
|
+
// ----------------------------------------------------------
|
|
2579
|
+
// Match tag
|
|
2580
|
+
// ----------------------------------------------------------
|
|
2581
|
+
if (openingElement.name.name !== tagName) {
|
|
2582
|
+
return false;
|
|
2583
|
+
}
|
|
2584
|
+
// ----------------------------------------------------------
|
|
2585
|
+
// Build attribute map
|
|
2586
|
+
// ----------------------------------------------------------
|
|
2587
|
+
const attributeMap = {};
|
|
2588
|
+
openingElement.attributes.forEach((attr) => {
|
|
2589
|
+
if (!t.isJSXAttribute(attr) || !t.isJSXIdentifier(attr.name)) {
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
// ------------------------------------------------------
|
|
2593
|
+
// String literal:
|
|
2594
|
+
//
|
|
2595
|
+
// href="#"
|
|
2596
|
+
// ------------------------------------------------------
|
|
2597
|
+
if (t.isStringLiteral(attr.value)) {
|
|
2598
|
+
attributeMap[attr.name.name] = attr.value.value;
|
|
2599
|
+
}
|
|
2600
|
+
// ------------------------------------------------------
|
|
2601
|
+
// JSX expression with string literal:
|
|
2602
|
+
//
|
|
2603
|
+
// src={"/logo.png"}
|
|
2604
|
+
// ------------------------------------------------------
|
|
2605
|
+
else if (t.isJSXExpressionContainer(attr.value) &&
|
|
2606
|
+
t.isStringLiteral(attr.value.expression)) {
|
|
2607
|
+
attributeMap[attr.name.name] = attr.value.expression.value;
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
// ----------------------------------------------------------
|
|
2611
|
+
// Match class names
|
|
2612
|
+
// ----------------------------------------------------------
|
|
2613
|
+
if (classes.length > 0) {
|
|
2614
|
+
const classNames = (attributeMap.className || "")
|
|
2615
|
+
.split(/\s+/)
|
|
2616
|
+
.filter(Boolean);
|
|
2617
|
+
const hasAllClasses = classes.every((className) => classNames.includes(className));
|
|
2618
|
+
if (!hasAllClasses) {
|
|
2619
|
+
return false;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
// ----------------------------------------------------------
|
|
2623
|
+
// Match attributes
|
|
2624
|
+
// ----------------------------------------------------------
|
|
2625
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
2626
|
+
if (attributeMap[key] !== value) {
|
|
2627
|
+
return false;
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
return true;
|
|
2631
|
+
}
|
|
2632
|
+
function analyzeComponent() { }
|
|
2633
|
+
// Architecture I Would Prefer
|
|
2634
|
+
// Parsers
|
|
2635
|
+
// parseAST
|
|
2636
|
+
// parseCSS
|
|
2637
|
+
// parseJSCode
|
|
2638
|
+
// generateCodeFromAST
|
|
2639
|
+
// Finders
|
|
2640
|
+
// findNodeByLine
|
|
2641
|
+
// findComponentByName
|
|
2642
|
+
// findJSXElement
|
|
2643
|
+
// findImportDeclaration
|
|
2644
|
+
// findCssSelector
|
|
2645
|
+
// findComponentDirectory
|
|
2646
|
+
// Ensurers
|
|
2647
|
+
// ensureLibrary
|
|
2648
|
+
// ensureStyleFile
|
|
2649
|
+
// ensureComponentStructure
|
|
2650
|
+
// normalizeComponent
|
|
2651
|
+
// Import Helpers
|
|
2652
|
+
// buildImportDeclaration
|
|
2653
|
+
// getImportSpecifiers
|
|
2654
|
+
// removeImportSpecifier
|
|
2655
|
+
// mergeImportDeclarations
|
|
2656
|
+
// isImportUsed
|
|
2657
|
+
// Analyzers (instead of many parseX functions)
|
|
2658
|
+
// analyzeComponent
|
|
2659
|
+
// analyzeDependencies
|
|
2660
|
+
// analyzeTypes
|
|
2661
|
+
// analyzeHooks
|
|
2662
|
+
// analyzeExports
|
|
2663
|
+
// const mutationTasks = getExportedFunctionNames("./lib/tasks/mutations.ts");
|
|
2664
|
+
// const astTasks = getExportedFunctionNames("./lib/tasks/ast.ts");
|
|
2665
|
+
/**
|
|
2666
|
+
* Parses a file and returns an array of exported function names.
|
|
2667
|
+
* @param {string} filePath - Path to the index.ts file.
|
|
2668
|
+
* @returns {string[]} Array of function name strings.
|
|
2669
|
+
*/
|
|
2670
|
+
async function getExportedFunctionNames(type) {
|
|
2671
|
+
let taskModule;
|
|
2672
|
+
if (type === "ast") {
|
|
2673
|
+
taskModule = await import("../tasks/ast.js");
|
|
2674
|
+
}
|
|
2675
|
+
if (type === "mutation") {
|
|
2676
|
+
taskModule = await import("../tasks/mutations.js");
|
|
2677
|
+
}
|
|
2678
|
+
if (type === "generators") {
|
|
2679
|
+
taskModule = await import("../tasks/generators.js");
|
|
2680
|
+
}
|
|
2681
|
+
if (type === "query") {
|
|
2682
|
+
taskModule = await import("../tasks/query.js");
|
|
2683
|
+
}
|
|
2684
|
+
let tasksList = Object.keys(taskModule.default);
|
|
2685
|
+
return tasksList;
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
*
|
|
2689
|
+
* @returns
|
|
2690
|
+
*/
|
|
2691
|
+
async function generateTasksDocs() {
|
|
2692
|
+
// let astTasksList = await getExportedFunctionNames("ast");
|
|
2693
|
+
// // const astModule = await import("../lib/tasks/ast.ts");
|
|
2694
|
+
// // let astTasksList = Object.keys(astModule.default);
|
|
2695
|
+
// let astTasks = [];
|
|
2696
|
+
let mutationTasksList = await getExportedFunctionNames("mutation");
|
|
2697
|
+
// const mutationsModule = await import("../lib/tasks/mutations.ts");
|
|
2698
|
+
// let mutationTasksList = Object.keys(mutationsModule.default);
|
|
2699
|
+
let mutationTasks = [];
|
|
2700
|
+
let generatorTasksList = await getExportedFunctionNames("generators");
|
|
2701
|
+
// const generatorModule = await import("../lib/tasks/generators.ts");
|
|
2702
|
+
// let generatorTasksList = Object.keys(generatorModule.default);
|
|
2703
|
+
let generatorTasks = [];
|
|
2704
|
+
let queryTasksList = await getExportedFunctionNames("query");
|
|
2705
|
+
// const queryModule = await import("../lib/tasks/query.ts");
|
|
2706
|
+
// let queryTasksList = Object.keys(queryModule.default);
|
|
2707
|
+
let queryTasks = [];
|
|
2708
|
+
for (let [taskName, task] of Object.entries(TaskRegistry)) {
|
|
2709
|
+
const payloadFields = "shape" in task.payload ? Object.keys(task.payload.shape) : [];
|
|
2710
|
+
const returnFields = "shape" in task.return ? Object.keys(task.return.shape) : [];
|
|
2711
|
+
const payloadString = payloadFields.length > 0 ? payloadFields.join(",\n ") : "";
|
|
2712
|
+
const returnString = returnFields.length > 0 ? returnFields.join(",\n ") : "";
|
|
2713
|
+
const taskDesc = `${taskName}({
|
|
2714
|
+
${payloadString}
|
|
2715
|
+
}) returns {
|
|
2716
|
+
${returnString}
|
|
2717
|
+
} - ${"description" in task ? task.description : "No description available"}`;
|
|
2718
|
+
// if (astTasksList.includes(taskName)) astTasks.push(taskDesc);
|
|
2719
|
+
if (mutationTasksList.includes(taskName))
|
|
2720
|
+
mutationTasks.push(taskDesc);
|
|
2721
|
+
if (generatorTasksList.includes(taskName))
|
|
2722
|
+
generatorTasks.push(taskDesc);
|
|
2723
|
+
if (queryTasksList.includes(taskName))
|
|
2724
|
+
queryTasks.push(taskDesc);
|
|
2725
|
+
}
|
|
2726
|
+
return {
|
|
2727
|
+
// astTasks: astTasks.join("\n\n"),
|
|
2728
|
+
mutationTasks: mutationTasks.join("\n\n"),
|
|
2729
|
+
generatorTasks: generatorTasks.join("\n\n"),
|
|
2730
|
+
queryTasks: queryTasks.join("\n\n"),
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
// Point synckit to your worker file
|
|
2734
|
+
// const formatSync = createSyncFn(
|
|
2735
|
+
// path.join(getInitializationContext().workersDir, "prettier.js"),
|
|
2736
|
+
// );
|
|
2737
|
+
// const formatCodeSync = createSyncFn(paths.worker("prettier"));
|
|
2738
|
+
function formatCode(code) {
|
|
2739
|
+
try {
|
|
2740
|
+
return createSyncFn(path.join(getInitializationContext().workersDir, "prettier.js"))(code);
|
|
2741
|
+
}
|
|
2742
|
+
catch (error) {
|
|
2743
|
+
throw new LoomaError(ERROR_CODES.VALIDATION_FAILED, `Syntax validation failed:\n${error.message}`);
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
function formatObjectCode(obj) {
|
|
2747
|
+
const result = { ...obj };
|
|
2748
|
+
const keysToFormat = ["code", "css", "component"];
|
|
2749
|
+
for (const key of keysToFormat) {
|
|
2750
|
+
if (typeof result[key] === "string") {
|
|
2751
|
+
result[key] = formatCode(result[key]);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
return result;
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Resolves all task references inside a payload.
|
|
2758
|
+
*
|
|
2759
|
+
* Example:
|
|
2760
|
+
*
|
|
2761
|
+
* payload:
|
|
2762
|
+
* {
|
|
2763
|
+
* code: {
|
|
2764
|
+
* $ref: {
|
|
2765
|
+
* source: "task_1",
|
|
2766
|
+
* path: "generatedCode"
|
|
2767
|
+
* }
|
|
2768
|
+
* }
|
|
2769
|
+
* }
|
|
2770
|
+
*
|
|
2771
|
+
* taskOutputs:
|
|
2772
|
+
* {
|
|
2773
|
+
* task_1: {
|
|
2774
|
+
* generatedCode: "<div>Hello</div>"
|
|
2775
|
+
* }
|
|
2776
|
+
* }
|
|
2777
|
+
*
|
|
2778
|
+
* result:
|
|
2779
|
+
* {
|
|
2780
|
+
* code: "<div>Hello</div>"
|
|
2781
|
+
* }
|
|
2782
|
+
*/
|
|
2783
|
+
function resolveTaskReferences({ value, taskOutputs, }) {
|
|
2784
|
+
// ----------------------------------------------------------
|
|
2785
|
+
// STEP 1:
|
|
2786
|
+
// Handle arrays.
|
|
2787
|
+
//
|
|
2788
|
+
// If the current value is an array,
|
|
2789
|
+
// recursively resolve every item.
|
|
2790
|
+
// ----------------------------------------------------------
|
|
2791
|
+
if (Array.isArray(value)) {
|
|
2792
|
+
return value.map((item) => resolveTaskReferences({ value: item, taskOutputs }));
|
|
2793
|
+
}
|
|
2794
|
+
// ----------------------------------------------------------
|
|
2795
|
+
// STEP 2:
|
|
2796
|
+
// Handle objects.
|
|
2797
|
+
//
|
|
2798
|
+
// Objects can either be:
|
|
2799
|
+
//
|
|
2800
|
+
// 1. A reference object
|
|
2801
|
+
// 2. A normal object containing nested references
|
|
2802
|
+
// ----------------------------------------------------------
|
|
2803
|
+
if (value && typeof value === "object") {
|
|
2804
|
+
// --------------------------------------------------------
|
|
2805
|
+
// STEP 3:
|
|
2806
|
+
// Check if this object is a reference.
|
|
2807
|
+
//
|
|
2808
|
+
// Example:
|
|
2809
|
+
//
|
|
2810
|
+
// {
|
|
2811
|
+
// $ref: {
|
|
2812
|
+
// source: "task_1",
|
|
2813
|
+
// path: "generatedCode"
|
|
2814
|
+
// }
|
|
2815
|
+
// }
|
|
2816
|
+
// --------------------------------------------------------
|
|
2817
|
+
if ("$ref" in value && value.$ref?.source) {
|
|
2818
|
+
const { source, path } = value.$ref;
|
|
2819
|
+
// ------------------------------------------------------
|
|
2820
|
+
// STEP 4:
|
|
2821
|
+
// Find task output.
|
|
2822
|
+
// ------------------------------------------------------
|
|
2823
|
+
const taskOutput = taskOutputs[source];
|
|
2824
|
+
if (!taskOutput) {
|
|
2825
|
+
throw new LoomaError(ERROR_CODES.TASK_ERROR, `Task output not found: ${source}`, {
|
|
2826
|
+
taskOutputs,
|
|
2827
|
+
source,
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
// ------------------------------------------------------
|
|
2831
|
+
// STEP 5:
|
|
2832
|
+
// Resolve nested path.
|
|
2833
|
+
//
|
|
2834
|
+
// Example:
|
|
2835
|
+
//
|
|
2836
|
+
// path:
|
|
2837
|
+
// "generatedCode"
|
|
2838
|
+
//
|
|
2839
|
+
// path:
|
|
2840
|
+
// "component.jsx"
|
|
2841
|
+
// ------------------------------------------------------
|
|
2842
|
+
const pathParts = path.split(".");
|
|
2843
|
+
let currentValue = taskOutput;
|
|
2844
|
+
for (const part of pathParts) {
|
|
2845
|
+
if (currentValue == null || !(part in currentValue)) {
|
|
2846
|
+
throw new LoomaError(ERROR_CODES.TASK_ERROR, `Unable to resolve path "${path}" from task "${source}"`, {
|
|
2847
|
+
task: resolveTaskReferences.name,
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
currentValue = currentValue[part];
|
|
2851
|
+
}
|
|
2852
|
+
// ------------------------------------------------------
|
|
2853
|
+
// STEP 6:
|
|
2854
|
+
// Return resolved value.
|
|
2855
|
+
// ------------------------------------------------------
|
|
2856
|
+
return currentValue;
|
|
2857
|
+
}
|
|
2858
|
+
// --------------------------------------------------------
|
|
2859
|
+
// STEP 7:
|
|
2860
|
+
// Resolve every property of a normal object.
|
|
2861
|
+
// --------------------------------------------------------
|
|
2862
|
+
return Object.fromEntries(Object.entries(value).map(([key, val]) => [
|
|
2863
|
+
key,
|
|
2864
|
+
resolveTaskReferences({ value: val, taskOutputs }),
|
|
2865
|
+
]));
|
|
2866
|
+
}
|
|
2867
|
+
// ----------------------------------------------------------
|
|
2868
|
+
// STEP 8:
|
|
2869
|
+
// Primitive values need no processing.
|
|
2870
|
+
//
|
|
2871
|
+
// Examples:
|
|
2872
|
+
// string
|
|
2873
|
+
// number
|
|
2874
|
+
// boolean
|
|
2875
|
+
// null
|
|
2876
|
+
// undefined
|
|
2877
|
+
// ----------------------------------------------------------
|
|
2878
|
+
return value;
|
|
2879
|
+
}
|
|
2880
|
+
// function detectFileType(filename) {
|
|
2881
|
+
// const ext = path.extname(filename);
|
|
2882
|
+
// if (ext === ".jsx" || ext === ".tsx") return "jsx";
|
|
2883
|
+
// if (ext === ".js" || ext === ".ts") return "js";
|
|
2884
|
+
// if (ext === ".css") return "css";
|
|
2885
|
+
// if (ext === ".svg") return "svg";
|
|
2886
|
+
// if (filename === "package.json") return "json";
|
|
2887
|
+
// return "file";
|
|
2888
|
+
// }
|
|
2889
|
+
// const projectDir = path.resolve("../../");
|
|
2890
|
+
// const map = generateMap(projectDir);
|
|
2891
|
+
// fs.writeFileSync("../file-map.json", JSON.stringify(map, null, 2));
|
|
2892
|
+
// console.log("Generated file-map.json");
|
|
2893
|
+
// const SRC_DIR = path.join(__dirname, 'looma-test', 'src'); // where your frontend code lives
|
|
2894
|
+
// console.log(__dirname)
|
|
2895
|
+
// console.log(SRC_DIR)
|
|
2896
|
+
// console.log(SRC_DIR)
|
|
2897
|
+
// function readDirectoryStructure(dir, prefix = "") {
|
|
2898
|
+
// let structure = "";
|
|
2899
|
+
// const items = fs.readdirSync(dir);
|
|
2900
|
+
// for (const item of items) {
|
|
2901
|
+
// const fullPath = path.join(dir, item);
|
|
2902
|
+
// const relativePath = path.join(prefix, item);
|
|
2903
|
+
// const stat = fs.statSync(fullPath);
|
|
2904
|
+
// if (stat.isDirectory()) {
|
|
2905
|
+
// structure += `Directory: ${relativePath}\n`;
|
|
2906
|
+
// structure += readDirectoryStructure(fullPath, relativePath);
|
|
2907
|
+
// } else {
|
|
2908
|
+
// structure += `File: ${relativePath}\n`;
|
|
2909
|
+
// }
|
|
2910
|
+
// }
|
|
2911
|
+
// return structure;
|
|
2912
|
+
// }
|
|
2913
|
+
// function getFileTree(dir, depth = 0) {
|
|
2914
|
+
// let result = "";
|
|
2915
|
+
// const items = fs.readdirSync(dir);
|
|
2916
|
+
// for (const item of items) {
|
|
2917
|
+
// const path = `${dir}/${item}`;
|
|
2918
|
+
// const stats = fs.statSync(path);
|
|
2919
|
+
// result += " ".repeat(depth) + item + "\n";
|
|
2920
|
+
// if (stats.isDirectory()) result += getFileTree(path, depth + 1);
|
|
2921
|
+
// }
|
|
2922
|
+
// return result;
|
|
2923
|
+
// }
|
|
2924
|
+
// console.log(readDirectoryStructure(PROJECT_SRC))
|
|
2925
|
+
//save it to a local text file
|
|
2926
|
+
// function generatePrompt({ command, component }) {
|
|
2927
|
+
// const projectDetails = "It is a react project";
|
|
2928
|
+
// const llmRole = "You are a front end developer";
|
|
2929
|
+
// const componentText = "Consider the following code";
|
|
2930
|
+
// const outputText =
|
|
2931
|
+
// "Your response should be just the code, nothing else, no explaination";
|
|
2932
|
+
// const designPreferences = "";
|
|
2933
|
+
// const text = `Write a very simple React functional component named "Header" using javascript. Do not include explanations or markdown. Do not include import react or export statment. Output only the component code. `;
|
|
2934
|
+
// }
|
|
2935
|
+
// function backupFile(filePath) {
|
|
2936
|
+
// const content = fs.readFileSync(filePath, "utf8");
|
|
2937
|
+
// const timestamp = new Date().toISOString().replace(/[:.]/g, "_");
|
|
2938
|
+
// const fileName = path.basename(filePath);
|
|
2939
|
+
// const backupPath = path.join("llm_backups", `${fileName}_${timestamp}.bak`);
|
|
2940
|
+
// fs.writeFileSync(backupPath, content);
|
|
2941
|
+
// }
|
|
2942
|
+
// function restoreLastBackup(filePath) {
|
|
2943
|
+
// const fileName = path.basename(filePath);
|
|
2944
|
+
// const backups = fs
|
|
2945
|
+
// .readdirSync("llm_backups")
|
|
2946
|
+
// .filter((f) => f.startsWith(fileName))
|
|
2947
|
+
// .sort()
|
|
2948
|
+
// .reverse();
|
|
2949
|
+
// if (backups.length === 0) return;
|
|
2950
|
+
// const lastBackup = path.join("llm_backups", backups[0]);
|
|
2951
|
+
// const backupContent = fs.readFileSync(lastBackup, "utf8");
|
|
2952
|
+
// fs.writeFileSync(filePath, backupContent);
|
|
2953
|
+
// }
|
|
2954
|
+
// // file: scanStructure.js
|
|
2955
|
+
// // const fs from 'fs');
|
|
2956
|
+
// // const path from 'path');
|
|
2957
|
+
// import fs from "fs";
|
|
2958
|
+
// import path from "path";
|
|
2959
|
+
// function walk(dir, fileList = []) {
|
|
2960
|
+
// const files = fs.readdirSync(dir);
|
|
2961
|
+
// for (const file of files) {
|
|
2962
|
+
// const filepath = path.join(dir, file);
|
|
2963
|
+
// const stat = fs.statSync(filepath);
|
|
2964
|
+
// if (stat.isDirectory()) {
|
|
2965
|
+
// walk(filepath, fileList);
|
|
2966
|
+
// } else {
|
|
2967
|
+
// const relativePath = path.relative(process.cwd(), filepath);
|
|
2968
|
+
// const content = fs.readFileSync(filepath, 'utf-8');
|
|
2969
|
+
// fileList.push({ file: relativePath, content });
|
|
2970
|
+
// }
|
|
2971
|
+
// }
|
|
2972
|
+
// return fileList;
|
|
2973
|
+
// }
|
|
2974
|
+
// const files = walk('./src');
|
|
2975
|
+
// const summary = files.map(f => `--- ${f.file} ---\n${f.content}`).join('\n\n');
|
|
2976
|
+
// fs.writeFileSync('structure.txt', summary);
|
|
2977
|
+
/**
|
|
2978
|
+
* Recursively crawls component directories
|
|
2979
|
+
* and builds Looma component registry.
|
|
2980
|
+
*
|
|
2981
|
+
* ------------------------------------------------------------
|
|
2982
|
+
* WHY RECURSIVE CRAWLING IS IMPORTANT
|
|
2983
|
+
* ------------------------------------------------------------
|
|
2984
|
+
*
|
|
2985
|
+
* Modern React applications almost always
|
|
2986
|
+
* organize components in nested structures.
|
|
2987
|
+
*
|
|
2988
|
+
* Example:
|
|
2989
|
+
*
|
|
2990
|
+
* src/components/
|
|
2991
|
+
* Dashboard/
|
|
2992
|
+
* Header/
|
|
2993
|
+
* Sidebar/
|
|
2994
|
+
*
|
|
2995
|
+
* Flat-only crawling becomes insufficient
|
|
2996
|
+
* for scalable applications.
|
|
2997
|
+
*
|
|
2998
|
+
* This function recursively traverses
|
|
2999
|
+
* all nested component directories.
|
|
3000
|
+
*
|
|
3001
|
+
* ------------------------------------------------------------
|
|
3002
|
+
* WHAT THIS FUNCTION DOES
|
|
3003
|
+
* ------------------------------------------------------------
|
|
3004
|
+
*
|
|
3005
|
+
* - crawls nested component directories
|
|
3006
|
+
* - finds JSX/TSX component files
|
|
3007
|
+
* - detects CSS files
|
|
3008
|
+
* - generates component registry entries
|
|
3009
|
+
* - supports scalable project structures
|
|
3010
|
+
*
|
|
3011
|
+
* ------------------------------------------------------------
|
|
3012
|
+
* PARAMS
|
|
3013
|
+
* ------------------------------------------------------------
|
|
3014
|
+
*
|
|
3015
|
+
* @param {Object} params
|
|
3016
|
+
*
|
|
3017
|
+
* @param {string} params.componentsPath
|
|
3018
|
+
* Root components directory.
|
|
3019
|
+
*
|
|
3020
|
+
* Example:
|
|
3021
|
+
*
|
|
3022
|
+
* src/components
|
|
3023
|
+
*
|
|
3024
|
+
* @param {Object} params.registry
|
|
3025
|
+
* Existing registry object.
|
|
3026
|
+
*
|
|
3027
|
+
* ------------------------------------------------------------
|
|
3028
|
+
* RETURNS
|
|
3029
|
+
* ------------------------------------------------------------
|
|
3030
|
+
*
|
|
3031
|
+
* @returns {Object}
|
|
3032
|
+
*
|
|
3033
|
+
* Component registry object.
|
|
3034
|
+
*
|
|
3035
|
+
*/
|
|
3036
|
+
function createComponentRegistry({ componentsPath, projectRoot, registry = {}, }) {
|
|
3037
|
+
/**
|
|
3038
|
+
* ----------------------------------------------------------
|
|
3039
|
+
* INTERNAL RECURSIVE DIRECTORY WALKER
|
|
3040
|
+
* ----------------------------------------------------------
|
|
3041
|
+
*/
|
|
3042
|
+
function walkDirectory(currentDirectory) {
|
|
3043
|
+
// --------------------------------------------------------
|
|
3044
|
+
// Read all files/folders inside current directory
|
|
3045
|
+
// --------------------------------------------------------
|
|
3046
|
+
const entries = fs.readdirSync(currentDirectory, {
|
|
3047
|
+
withFileTypes: true,
|
|
3048
|
+
});
|
|
3049
|
+
// --------------------------------------------------------
|
|
3050
|
+
// Process every directory entry
|
|
3051
|
+
// --------------------------------------------------------
|
|
3052
|
+
for (const entry of entries) {
|
|
3053
|
+
// ------------------------------------------------------
|
|
3054
|
+
// Build absolute path
|
|
3055
|
+
// ------------------------------------------------------
|
|
3056
|
+
const absoluteEntryPath = path.join(currentDirectory, entry.name);
|
|
3057
|
+
// ------------------------------------------------------
|
|
3058
|
+
// Build project-relative path
|
|
3059
|
+
// ------------------------------------------------------
|
|
3060
|
+
const relativeEntryPath = path.relative(projectRoot, absoluteEntryPath);
|
|
3061
|
+
// ------------------------------------------------------
|
|
3062
|
+
// RECURSIVE DIRECTORY HANDLING
|
|
3063
|
+
// ------------------------------------------------------
|
|
3064
|
+
if (entry.isDirectory()) {
|
|
3065
|
+
// ----------------------------------------------------
|
|
3066
|
+
// Continue recursive crawl
|
|
3067
|
+
// ----------------------------------------------------
|
|
3068
|
+
walkDirectory(absoluteEntryPath);
|
|
3069
|
+
// ----------------------------------------------------
|
|
3070
|
+
// Move to next entry
|
|
3071
|
+
// ----------------------------------------------------
|
|
3072
|
+
continue;
|
|
3073
|
+
}
|
|
3074
|
+
// ------------------------------------------------------
|
|
3075
|
+
// Skip non JSX/TSX files
|
|
3076
|
+
// ------------------------------------------------------
|
|
3077
|
+
const isComponentFile = entry.name.endsWith(".jsx") || entry.name.endsWith(".tsx");
|
|
3078
|
+
if (!isComponentFile) {
|
|
3079
|
+
continue;
|
|
3080
|
+
}
|
|
3081
|
+
// ------------------------------------------------------
|
|
3082
|
+
// Extract component name
|
|
3083
|
+
// ------------------------------------------------------
|
|
3084
|
+
const componentName = path.basename(entry.name, path.extname(entry.name));
|
|
3085
|
+
// ------------------------------------------------------
|
|
3086
|
+
// Build component id
|
|
3087
|
+
// ------------------------------------------------------
|
|
3088
|
+
const componentId = `cmp_${componentName.toLowerCase()}`;
|
|
3089
|
+
// --------------------------------------------------------
|
|
3090
|
+
// Read component source code
|
|
3091
|
+
// --------------------------------------------------------
|
|
3092
|
+
const code = fs.readFileSync(path.join(currentDirectory, `${componentName}.jsx`), "utf8");
|
|
3093
|
+
// --------------------------------------------------------
|
|
3094
|
+
// Parse AST
|
|
3095
|
+
// --------------------------------------------------------
|
|
3096
|
+
const ast = parse(code, {
|
|
3097
|
+
sourceType: "module",
|
|
3098
|
+
plugins: ["jsx"],
|
|
3099
|
+
});
|
|
3100
|
+
// --------------------------------------------------------
|
|
3101
|
+
// Default metadata values
|
|
3102
|
+
// --------------------------------------------------------
|
|
3103
|
+
let exported = false;
|
|
3104
|
+
let props = [];
|
|
3105
|
+
let rootElement = null;
|
|
3106
|
+
const childComponents = [];
|
|
3107
|
+
// --------------------------------------------------------
|
|
3108
|
+
// Traverse AST
|
|
3109
|
+
// --------------------------------------------------------
|
|
3110
|
+
traverse.default(ast, {
|
|
3111
|
+
// ------------------------------------------------------
|
|
3112
|
+
// Detect exported component
|
|
3113
|
+
// ------------------------------------------------------
|
|
3114
|
+
ExportDefaultDeclaration(path) {
|
|
3115
|
+
exported = true;
|
|
3116
|
+
},
|
|
3117
|
+
// ------------------------------------------------------
|
|
3118
|
+
// Detect component declaration
|
|
3119
|
+
// ------------------------------------------------------
|
|
3120
|
+
FunctionDeclaration(path) {
|
|
3121
|
+
// ----------------------------------------------------
|
|
3122
|
+
// Ignore unrelated functions
|
|
3123
|
+
// ----------------------------------------------------
|
|
3124
|
+
if (path.node.id.name !== componentName) {
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
// ----------------------------------------------------
|
|
3128
|
+
// Extract props
|
|
3129
|
+
// ----------------------------------------------------
|
|
3130
|
+
const firstParam = path.node.params[0];
|
|
3131
|
+
// ----------------------------------------------------
|
|
3132
|
+
// props object destructuring
|
|
3133
|
+
//
|
|
3134
|
+
// function Header({
|
|
3135
|
+
// title,
|
|
3136
|
+
// logo
|
|
3137
|
+
// })
|
|
3138
|
+
// ----------------------------------------------------
|
|
3139
|
+
if (t.isObjectPattern(firstParam)) {
|
|
3140
|
+
firstParam.properties.forEach((property) => {
|
|
3141
|
+
if (t.isObjectProperty(property)) {
|
|
3142
|
+
if (t.isIdentifier(property.key)) {
|
|
3143
|
+
props.push(property.key.name);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
// ----------------------------------------------------
|
|
3149
|
+
// plain props object
|
|
3150
|
+
//
|
|
3151
|
+
// function Header(props)
|
|
3152
|
+
// ----------------------------------------------------
|
|
3153
|
+
if (t.isIdentifier(firstParam)) {
|
|
3154
|
+
props.push(firstParam.name);
|
|
3155
|
+
}
|
|
3156
|
+
},
|
|
3157
|
+
// ------------------------------------------------------
|
|
3158
|
+
// Detect JSX root element
|
|
3159
|
+
// ------------------------------------------------------
|
|
3160
|
+
ReturnStatement(path) {
|
|
3161
|
+
// ----------------------------------------------------
|
|
3162
|
+
// Only inspect JSX returns
|
|
3163
|
+
// ----------------------------------------------------
|
|
3164
|
+
if (!t.isJSXElement(path.node.argument)) {
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
const openingElement = path.node.argument.openingElement;
|
|
3168
|
+
// ----------------------------------------------------
|
|
3169
|
+
// Extract root element name
|
|
3170
|
+
//
|
|
3171
|
+
// Example:
|
|
3172
|
+
//
|
|
3173
|
+
// <header>
|
|
3174
|
+
// ----------------------------------------------------
|
|
3175
|
+
if (t.isJSXIdentifier(openingElement.name)) {
|
|
3176
|
+
rootElement = openingElement.name.name;
|
|
3177
|
+
}
|
|
3178
|
+
},
|
|
3179
|
+
// ------------------------------------------------------
|
|
3180
|
+
// Detect child component usage
|
|
3181
|
+
// ------------------------------------------------------
|
|
3182
|
+
JSXOpeningElement(path) {
|
|
3183
|
+
// ----------------------------------------------------
|
|
3184
|
+
// Ignore html tags
|
|
3185
|
+
// ----------------------------------------------------
|
|
3186
|
+
if (!t.isJSXIdentifier(path.node.name)) {
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
const tagName = path.node.name.name;
|
|
3190
|
+
// ----------------------------------------------------
|
|
3191
|
+
// React component names start uppercase
|
|
3192
|
+
// ----------------------------------------------------
|
|
3193
|
+
const isReactComponent = /^[A-Z]/.test(tagName);
|
|
3194
|
+
if (!isReactComponent) {
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
// ----------------------------------------------------
|
|
3198
|
+
// Ignore self-reference
|
|
3199
|
+
// ----------------------------------------------------
|
|
3200
|
+
if (tagName === componentName) {
|
|
3201
|
+
return;
|
|
3202
|
+
}
|
|
3203
|
+
// ----------------------------------------------------
|
|
3204
|
+
// Prevent duplicates
|
|
3205
|
+
// ----------------------------------------------------
|
|
3206
|
+
if (!childComponents.includes(tagName)) {
|
|
3207
|
+
childComponents.push(tagName);
|
|
3208
|
+
}
|
|
3209
|
+
},
|
|
3210
|
+
});
|
|
3211
|
+
// ------------------------------------------------------
|
|
3212
|
+
// Detect CSS file
|
|
3213
|
+
// ------------------------------------------------------
|
|
3214
|
+
const cssCandidates = [
|
|
3215
|
+
`${componentName}.css`,
|
|
3216
|
+
`${componentName}.module.css`,
|
|
3217
|
+
];
|
|
3218
|
+
// ------------------------------------------------------
|
|
3219
|
+
// Try finding matching CSS file
|
|
3220
|
+
// ------------------------------------------------------
|
|
3221
|
+
let cssPath = null;
|
|
3222
|
+
for (const cssFileName of cssCandidates) {
|
|
3223
|
+
// ----------------------------------------------------
|
|
3224
|
+
// Build candidate CSS path
|
|
3225
|
+
// ----------------------------------------------------
|
|
3226
|
+
const absoluteCssPath = path.join(currentDirectory, cssFileName);
|
|
3227
|
+
// ----------------------------------------------------
|
|
3228
|
+
// Check if CSS file exists
|
|
3229
|
+
// ----------------------------------------------------
|
|
3230
|
+
if (fs.existsSync(absoluteCssPath)) {
|
|
3231
|
+
// --------------------------------------------------
|
|
3232
|
+
// Store project-relative css path
|
|
3233
|
+
// --------------------------------------------------
|
|
3234
|
+
cssPath = path.relative(projectRoot, absoluteCssPath);
|
|
3235
|
+
break;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
// ------------------------------------------------------
|
|
3239
|
+
// Create registry entry
|
|
3240
|
+
// ------------------------------------------------------
|
|
3241
|
+
registry[componentId] = {
|
|
3242
|
+
componentId,
|
|
3243
|
+
componentName,
|
|
3244
|
+
filePath: relativeEntryPath,
|
|
3245
|
+
cssPath: fs.existsSync(cssPath) ? cssPath : null,
|
|
3246
|
+
importPath: relativeEntryPath,
|
|
3247
|
+
parentComponent: null,
|
|
3248
|
+
childComponents,
|
|
3249
|
+
exported,
|
|
3250
|
+
props,
|
|
3251
|
+
rootElement,
|
|
3252
|
+
lastUpdated: Date.now(),
|
|
3253
|
+
};
|
|
3254
|
+
// ----------------------------------------------------------
|
|
3255
|
+
// STEP 5:
|
|
3256
|
+
// Resolve parent-child relationships
|
|
3257
|
+
// ----------------------------------------------------------
|
|
3258
|
+
Object.values(registry).forEach((component) => {
|
|
3259
|
+
component.childComponents.forEach((childName) => {
|
|
3260
|
+
// --------------------------------------------------
|
|
3261
|
+
// Find matching child component
|
|
3262
|
+
// --------------------------------------------------
|
|
3263
|
+
const childComponent = Object.values(registry).find((entry) => entry.componentName === childName);
|
|
3264
|
+
if (!childComponent) {
|
|
3265
|
+
return;
|
|
3266
|
+
}
|
|
3267
|
+
// --------------------------------------------------
|
|
3268
|
+
// Assign parent relationship
|
|
3269
|
+
// --------------------------------------------------
|
|
3270
|
+
childComponent.parentComponent = component.componentName;
|
|
3271
|
+
});
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
// ----------------------------------------------------------
|
|
3276
|
+
// Start recursive crawling
|
|
3277
|
+
// ----------------------------------------------------------
|
|
3278
|
+
walkDirectory(componentsPath);
|
|
3279
|
+
// ----------------------------------------------------------
|
|
3280
|
+
// Return final registry
|
|
3281
|
+
// ----------------------------------------------------------
|
|
3282
|
+
return { ...registry };
|
|
3283
|
+
}
|
|
3284
|
+
/**
|
|
3285
|
+
* Reads package.json and returns:
|
|
3286
|
+
*
|
|
3287
|
+
* - dependencies
|
|
3288
|
+
* - devDependencies
|
|
3289
|
+
*
|
|
3290
|
+
* ------------------------------------------------------------
|
|
3291
|
+
* WHY THIS FUNCTION EXISTS
|
|
3292
|
+
* ------------------------------------------------------------
|
|
3293
|
+
*
|
|
3294
|
+
* Looma's planner must understand:
|
|
3295
|
+
*
|
|
3296
|
+
* - available UI libraries
|
|
3297
|
+
* - routing libraries
|
|
3298
|
+
* - css systems
|
|
3299
|
+
* - animation libraries
|
|
3300
|
+
* - state management tools
|
|
3301
|
+
*
|
|
3302
|
+
* before generating code.
|
|
3303
|
+
*
|
|
3304
|
+
* Example:
|
|
3305
|
+
*
|
|
3306
|
+
* If project already has:
|
|
3307
|
+
*
|
|
3308
|
+
* - tailwindcss
|
|
3309
|
+
* - framer-motion
|
|
3310
|
+
* - react-router-dom
|
|
3311
|
+
*
|
|
3312
|
+
* then Looma should REUSE them
|
|
3313
|
+
* instead of generating custom solutions.
|
|
3314
|
+
*
|
|
3315
|
+
* ------------------------------------------------------------
|
|
3316
|
+
* WHERE THIS FUNCTION IS USEFUL
|
|
3317
|
+
* ------------------------------------------------------------
|
|
3318
|
+
*
|
|
3319
|
+
* Useful before:
|
|
3320
|
+
*
|
|
3321
|
+
* - planning mutations
|
|
3322
|
+
* - generating components
|
|
3323
|
+
* - generating styles
|
|
3324
|
+
* - deciding architecture
|
|
3325
|
+
* - choosing UI patterns
|
|
3326
|
+
*
|
|
3327
|
+
* ------------------------------------------------------------
|
|
3328
|
+
* PARAMS
|
|
3329
|
+
* ------------------------------------------------------------
|
|
3330
|
+
*
|
|
3331
|
+
* @param {Object} params
|
|
3332
|
+
*
|
|
3333
|
+
* @param {string} params.projectRoot
|
|
3334
|
+
* Root directory of project.
|
|
3335
|
+
*
|
|
3336
|
+
* Example:
|
|
3337
|
+
*
|
|
3338
|
+
* "/Users/sarv/project"
|
|
3339
|
+
*
|
|
3340
|
+
* ------------------------------------------------------------
|
|
3341
|
+
* RETURNS
|
|
3342
|
+
* ------------------------------------------------------------
|
|
3343
|
+
*
|
|
3344
|
+
* @returns {Object}
|
|
3345
|
+
*
|
|
3346
|
+
* {
|
|
3347
|
+
* dependencies,
|
|
3348
|
+
* devDependencies,
|
|
3349
|
+
* allPackages
|
|
3350
|
+
* }
|
|
3351
|
+
*
|
|
3352
|
+
*/
|
|
3353
|
+
function getProjectDependencies({ projectRoot, }) {
|
|
3354
|
+
// ----------------------------------------------------------
|
|
3355
|
+
// STEP 1:
|
|
3356
|
+
// Build absolute package.json path
|
|
3357
|
+
// ----------------------------------------------------------
|
|
3358
|
+
const packageJsonPath = path.join(path.resolve(projectRoot), "package.json");
|
|
3359
|
+
// ----------------------------------------------------------
|
|
3360
|
+
// STEP 2:
|
|
3361
|
+
// Validate package.json existence
|
|
3362
|
+
// ----------------------------------------------------------
|
|
3363
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
3364
|
+
throw new LoomaError(ERROR_CODES.FILE_NOT_FOUND, `package.json not found at: ${packageJsonPath}`);
|
|
3365
|
+
}
|
|
3366
|
+
// ----------------------------------------------------------
|
|
3367
|
+
// STEP 3:
|
|
3368
|
+
// Read package.json file
|
|
3369
|
+
// ----------------------------------------------------------
|
|
3370
|
+
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
|
|
3371
|
+
// ----------------------------------------------------------
|
|
3372
|
+
// STEP 4:
|
|
3373
|
+
// Parse JSON safely
|
|
3374
|
+
// ----------------------------------------------------------
|
|
3375
|
+
let packageJson;
|
|
3376
|
+
try {
|
|
3377
|
+
packageJson = JSON.parse(packageJsonContent);
|
|
3378
|
+
}
|
|
3379
|
+
catch (error) {
|
|
3380
|
+
throw new LoomaError(ERROR_CODES.INTERNAL_ERROR, `Invalid package.json format`);
|
|
3381
|
+
}
|
|
3382
|
+
// ----------------------------------------------------------
|
|
3383
|
+
// STEP 5:
|
|
3384
|
+
// Extract dependencies
|
|
3385
|
+
// ----------------------------------------------------------
|
|
3386
|
+
const dependencies = packageJson.dependencies || {};
|
|
3387
|
+
// ----------------------------------------------------------
|
|
3388
|
+
// STEP 6:
|
|
3389
|
+
// Extract devDependencies
|
|
3390
|
+
// ----------------------------------------------------------
|
|
3391
|
+
const devDependencies = packageJson.devDependencies || {};
|
|
3392
|
+
// ----------------------------------------------------------
|
|
3393
|
+
// STEP 7:
|
|
3394
|
+
// Merge all packages
|
|
3395
|
+
// ----------------------------------------------------------
|
|
3396
|
+
const allPackages = {
|
|
3397
|
+
...dependencies,
|
|
3398
|
+
...devDependencies,
|
|
3399
|
+
};
|
|
3400
|
+
// ----------------------------------------------------------
|
|
3401
|
+
// STEP 8:
|
|
3402
|
+
// Return package information
|
|
3403
|
+
// ----------------------------------------------------------
|
|
3404
|
+
return {
|
|
3405
|
+
dependencies,
|
|
3406
|
+
devDependencies,
|
|
3407
|
+
allPackages,
|
|
3408
|
+
};
|
|
3409
|
+
}
|
|
3410
|
+
function runtimeUiSnapshot() {
|
|
3411
|
+
return {
|
|
3412
|
+
currentRoute: "/dashboard",
|
|
3413
|
+
visibleComponents: [
|
|
3414
|
+
{
|
|
3415
|
+
id: "cmp_42",
|
|
3416
|
+
name: "Sidebar",
|
|
3417
|
+
bounds: {},
|
|
3418
|
+
visible: true,
|
|
3419
|
+
},
|
|
3420
|
+
],
|
|
3421
|
+
selectedComponent: {
|
|
3422
|
+
id: "cmp_42",
|
|
3423
|
+
},
|
|
3424
|
+
appState: {
|
|
3425
|
+
theme: "dark",
|
|
3426
|
+
auth: true,
|
|
3427
|
+
},
|
|
3428
|
+
viewport: {
|
|
3429
|
+
width: 1440,
|
|
3430
|
+
height: 900,
|
|
3431
|
+
},
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3434
|
+
function getOperationById() { }
|
|
3435
|
+
// function generateTasksDocs(filePath) {
|
|
3436
|
+
// const code = fs.readFileSync(path.resolve(filePath), "utf8");
|
|
3437
|
+
// // console.log(code);
|
|
3438
|
+
// const ast = parse(code, {
|
|
3439
|
+
// sourceType: "module",
|
|
3440
|
+
// plugins: ["typescript", "jsx"],
|
|
3441
|
+
// });
|
|
3442
|
+
// const exportedNames = new Set();
|
|
3443
|
+
// const functionDetails = {};
|
|
3444
|
+
// // Helper to extract the "Description: ..." text from a node's leading comments
|
|
3445
|
+
// function extractDescription(node) {
|
|
3446
|
+
// if (!node.leadingComments) return "";
|
|
3447
|
+
// for (const comment of node.leadingComments) {
|
|
3448
|
+
// // Look for "Description:" followed by any text inside the block comment
|
|
3449
|
+
// const match = comment.value.match(/DescriptionForPrompt:\s*(.*)/i);
|
|
3450
|
+
// if (match && match[1]) {
|
|
3451
|
+
// return match[1].trim(); // Returns just the description string
|
|
3452
|
+
// }
|
|
3453
|
+
// }
|
|
3454
|
+
// return "";
|
|
3455
|
+
// }
|
|
3456
|
+
// function extractJSDocReturns(node) {
|
|
3457
|
+
// if (!node.leadingComments) return "";
|
|
3458
|
+
// for (const comment of node.leadingComments) {
|
|
3459
|
+
// // This regex looks for @returns followed by anything wrapped in double curly braces {{ ... }}
|
|
3460
|
+
// // The 's' flag at the end allows the regex to match across multiple lines
|
|
3461
|
+
// const match = comment.value.match(/@returns\s*\{\{(.*?)\}\}/s);
|
|
3462
|
+
// if (match && match[1]) {
|
|
3463
|
+
// // 1. Split the captured block into individual lines
|
|
3464
|
+
// // 2. Remove leading spaces and JSDoc asterisks (*) from each line
|
|
3465
|
+
// // 3. Filter out empty lines and join them back together
|
|
3466
|
+
// const cleanedLines = match[1]
|
|
3467
|
+
// .split("\n")
|
|
3468
|
+
// .map((line) => line.replace(/^\s*\*\s*/, "").trim())
|
|
3469
|
+
// .filter((line) => line.length > 0);
|
|
3470
|
+
// // Reconstruct the object string format
|
|
3471
|
+
// return `{\n ${cleanedLines.join("\n ")}\n}`;
|
|
3472
|
+
// }
|
|
3473
|
+
// }
|
|
3474
|
+
// return "";
|
|
3475
|
+
// }
|
|
3476
|
+
// // Helper to convert Babel parameter nodes back into a string
|
|
3477
|
+
// function getParamsString(paramsNodes) {
|
|
3478
|
+
// return paramsNodes.map((param) => generate.default(param).code).join(", ");
|
|
3479
|
+
// }
|
|
3480
|
+
// // 1. First Pass: Find what is exported via module.exports
|
|
3481
|
+
// traverse.default(ast, {
|
|
3482
|
+
// AssignmentExpression(path) {
|
|
3483
|
+
// const { left, right } = path.node;
|
|
3484
|
+
// const isModuleExports =
|
|
3485
|
+
// left.type === "MemberExpression" &&
|
|
3486
|
+
// t.isIdentifier(left.object) &&
|
|
3487
|
+
// left.object.name === "module" &&
|
|
3488
|
+
// t.isIdentifier(left.property) &&
|
|
3489
|
+
// left.property.name === "exports";
|
|
3490
|
+
// if (isModuleExports && right.type === "ObjectExpression") {
|
|
3491
|
+
// for (const prop of right.properties) {
|
|
3492
|
+
// if (
|
|
3493
|
+
// prop.type === "ObjectProperty" &&
|
|
3494
|
+
// t.isIdentifier(prop.key) &&
|
|
3495
|
+
// prop.key.name
|
|
3496
|
+
// ) {
|
|
3497
|
+
// exportedNames.add(prop.key.name);
|
|
3498
|
+
// }
|
|
3499
|
+
// }
|
|
3500
|
+
// }
|
|
3501
|
+
// },
|
|
3502
|
+
// });
|
|
3503
|
+
// // 2. Second Pass: Extract parameters AND descriptions
|
|
3504
|
+
// traverse.default(ast, {
|
|
3505
|
+
// // Matches: function fun1() {}
|
|
3506
|
+
// FunctionDeclaration(path) {
|
|
3507
|
+
// const name = path.node.id?.name;
|
|
3508
|
+
// if (exportedNames.has(name)) {
|
|
3509
|
+
// let actualReturn = "void";
|
|
3510
|
+
// // Safe inner traversal using the current path context
|
|
3511
|
+
// path.traverse({
|
|
3512
|
+
// ReturnStatement(innerPath) {
|
|
3513
|
+
// if (innerPath.getFunctionParent().node !== path.node) return; // Skip sub-functions
|
|
3514
|
+
// actualReturn = innerPath.node.argument
|
|
3515
|
+
// ? generate.default(innerPath.node.argument).code
|
|
3516
|
+
// : "undefined";
|
|
3517
|
+
// },
|
|
3518
|
+
// });
|
|
3519
|
+
// functionDetails[name] = {
|
|
3520
|
+
// params: getParamsString(path.node.params),
|
|
3521
|
+
// returnsDoc: extractJSDocReturns(path.node) || "void",
|
|
3522
|
+
// actualReturn: actualReturn.replace(/\s+/g, " "),
|
|
3523
|
+
// description: extractDescription(path.node),
|
|
3524
|
+
// };
|
|
3525
|
+
// }
|
|
3526
|
+
// },
|
|
3527
|
+
// // Matches: const fun1 = () => {}
|
|
3528
|
+
// // Note: Comments are attached to the VariableDeclaration statement container
|
|
3529
|
+
// VariableDeclarator(path) {
|
|
3530
|
+
// const name = t.isIdentifier(path.node.id) ? path.node.id.name : null;
|
|
3531
|
+
// if (exportedNames.has(name)) {
|
|
3532
|
+
// const init = path.node.init;
|
|
3533
|
+
// if (
|
|
3534
|
+
// init &&
|
|
3535
|
+
// ["ArrowFunctionExpression", "FunctionExpression"].includes(init.type)
|
|
3536
|
+
// ) {
|
|
3537
|
+
// // For variables, leadingComments live on the parent VariableDeclaration node
|
|
3538
|
+
// const parentNode = path.parentPath.node;
|
|
3539
|
+
// let actualReturn = "void";
|
|
3540
|
+
// // Arrow functions can have an implicit return expression instead of a block statement body
|
|
3541
|
+
// if (
|
|
3542
|
+
// (t.isFunctionExpression(init) ||
|
|
3543
|
+
// t.isArrowFunctionExpression(init)) &&
|
|
3544
|
+
// init.body.type !== "BlockStatement"
|
|
3545
|
+
// ) {
|
|
3546
|
+
// // Implicit arrow return: () => ({ status: 'ok' })
|
|
3547
|
+
// actualReturn = generate.default(init.body).code;
|
|
3548
|
+
// } else {
|
|
3549
|
+
// // Block statement arrow return: () => { return { status: 'ok' } }
|
|
3550
|
+
// path.traverse({
|
|
3551
|
+
// ReturnStatement(innerPath) {
|
|
3552
|
+
// if (innerPath.getFunctionParent().node !== init) return; // Skip sub-functions
|
|
3553
|
+
// actualReturn = innerPath.node.argument
|
|
3554
|
+
// ? generate.default(innerPath.node.argument).code
|
|
3555
|
+
// : "undefined";
|
|
3556
|
+
// },
|
|
3557
|
+
// });
|
|
3558
|
+
// }
|
|
3559
|
+
// const complexReturnDoc = extractJSDocReturns(path.parentPath.node);
|
|
3560
|
+
// functionDetails[name] = {
|
|
3561
|
+
// params:
|
|
3562
|
+
// t.isFunctionExpression(init) || t.isArrowFunctionExpression(init)
|
|
3563
|
+
// ? getParamsString(init.params)
|
|
3564
|
+
// : "",
|
|
3565
|
+
// returnsDoc: complexReturnDoc || "void",
|
|
3566
|
+
// actualReturn: actualReturn.replace(/\s+/g, " "),
|
|
3567
|
+
// description: extractDescription(parentNode),
|
|
3568
|
+
// };
|
|
3569
|
+
// }
|
|
3570
|
+
// }
|
|
3571
|
+
// },
|
|
3572
|
+
// });
|
|
3573
|
+
// // 3. Format the documentation string
|
|
3574
|
+
// const formattedLines = Array.from(exportedNames).map((name: any) => {
|
|
3575
|
+
// const details = functionDetails[name];
|
|
3576
|
+
// if (!details) return `${name}()`;
|
|
3577
|
+
// const paramStr = details.params;
|
|
3578
|
+
// const descStr = details.description ? ` - ${details.description}` : "";
|
|
3579
|
+
// return `${name}(${paramStr}) returns ${details.returnsDoc}${descStr}`;
|
|
3580
|
+
// // return `
|
|
3581
|
+
// // ${name}: {
|
|
3582
|
+
// // payload: ${paramStr};
|
|
3583
|
+
// // result: ${details.actualReturn};
|
|
3584
|
+
// // };
|
|
3585
|
+
// // `;
|
|
3586
|
+
// });
|
|
3587
|
+
// const functionsListString = formattedLines.join("\n");
|
|
3588
|
+
// return functionsListString;
|
|
3589
|
+
// }
|
|
3590
|
+
function appendAction({ command, operations, undoPath }) {
|
|
3591
|
+
// Generate a unique action id.
|
|
3592
|
+
const actionId = `ac_${Date.now()}_${crypto.randomBytes(4).toString("hex")}`;
|
|
3593
|
+
// Construct the action object.
|
|
3594
|
+
const action = {
|
|
3595
|
+
id: actionId,
|
|
3596
|
+
command,
|
|
3597
|
+
operations,
|
|
3598
|
+
timestamp: new Date().toISOString(),
|
|
3599
|
+
};
|
|
3600
|
+
// Read the current contents of undo.json.
|
|
3601
|
+
const actions = readActionStack(undoPath);
|
|
3602
|
+
// Append the new action to the stack.
|
|
3603
|
+
actions.push(action);
|
|
3604
|
+
// Persist the updated stack.
|
|
3605
|
+
writeActionStack(undoPath, actions);
|
|
3606
|
+
console.log("Logged action:", actionId);
|
|
3607
|
+
}
|
|
3608
|
+
/**
|
|
3609
|
+
* Reads an action stack (undo.json or redo.json).
|
|
3610
|
+
*
|
|
3611
|
+
* - Creates the file if it does not exist.
|
|
3612
|
+
* - Initializes it with an empty array if needed.
|
|
3613
|
+
* - Returns the parsed array of actions.
|
|
3614
|
+
*
|
|
3615
|
+
* @param {string} filePath
|
|
3616
|
+
* @returns {Array}
|
|
3617
|
+
*/
|
|
3618
|
+
function readActionStack(filePath) {
|
|
3619
|
+
// Create the file if it doesn't exist.
|
|
3620
|
+
if (!fs.existsSync(filePath)) {
|
|
3621
|
+
fs.writeFileSync(filePath, "[]", "utf8");
|
|
3622
|
+
}
|
|
3623
|
+
// Read the contents of the file.
|
|
3624
|
+
const fileContents = fs.readFileSync(filePath, "utf8").trim();
|
|
3625
|
+
// Treat an empty file as an empty stack.
|
|
3626
|
+
if (fileContents.length === 0) {
|
|
3627
|
+
return [];
|
|
3628
|
+
}
|
|
3629
|
+
// Parse and return the action stack.
|
|
3630
|
+
return JSON.parse(fileContents);
|
|
3631
|
+
}
|
|
3632
|
+
/**
|
|
3633
|
+
* Writes an action stack (undo.json or redo.json).
|
|
3634
|
+
*
|
|
3635
|
+
* @param {string} filePath
|
|
3636
|
+
* @param {Array} actions
|
|
3637
|
+
*/
|
|
3638
|
+
function writeActionStack(filePath, actions) {
|
|
3639
|
+
// Persist the updated stack in a readable format.
|
|
3640
|
+
fs.writeFileSync(filePath, JSON.stringify(actions, null, 2), "utf8");
|
|
3641
|
+
}
|
|
3642
|
+
// function appendLLMLog(log) {
|
|
3643
|
+
// console.log(path.resolve("./looma-logs/llm-logs.jsonl"));
|
|
3644
|
+
// fs.appendFileSync(
|
|
3645
|
+
// "./looma-logs/llm-logs.jsonl",
|
|
3646
|
+
// JSON.stringify(log) + "\n",
|
|
3647
|
+
// "utf8",
|
|
3648
|
+
// );
|
|
3649
|
+
// }
|
|
3650
|
+
async function listFiles({ directory, recursive, }) {
|
|
3651
|
+
const files = [];
|
|
3652
|
+
async function walk(dir) {
|
|
3653
|
+
const entries = await fs.promises.readdir(dir, {
|
|
3654
|
+
withFileTypes: true,
|
|
3655
|
+
});
|
|
3656
|
+
for (const entry of entries) {
|
|
3657
|
+
const fullPath = path.join(dir, entry.name);
|
|
3658
|
+
if (entry.isDirectory()) {
|
|
3659
|
+
if (recursive) {
|
|
3660
|
+
await walk(fullPath);
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
else {
|
|
3664
|
+
files.push(fullPath);
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
await walk(directory);
|
|
3669
|
+
return { files };
|
|
3670
|
+
}
|
|
3671
|
+
function readFile(filePath) {
|
|
3672
|
+
return fs.readFileSync(filePath, "utf8");
|
|
3673
|
+
}
|
|
3674
|
+
function writeFile({ filePath, content, }) {
|
|
3675
|
+
const formatted = formatCode(content);
|
|
3676
|
+
fs.writeFileSync(filePath, formatted, "utf8");
|
|
3677
|
+
}
|
|
3678
|
+
export default {
|
|
3679
|
+
// insertCode,
|
|
3680
|
+
// expose only when planner becomes mature enough to use it directly
|
|
3681
|
+
// ensureImport,
|
|
3682
|
+
// removeImport,
|
|
3683
|
+
// enrichImport,
|
|
3684
|
+
// optimizeImports,
|
|
3685
|
+
// updateComponentImports,
|
|
3686
|
+
// findNodeByLine,
|
|
3687
|
+
// findComponentByName,
|
|
3688
|
+
// findJSXElement,
|
|
3689
|
+
// resolveImportConflicts,
|
|
3690
|
+
// findImportDeclaration,
|
|
3691
|
+
// buildImportDeclaration,
|
|
3692
|
+
// getImportSpecifiers,
|
|
3693
|
+
// removeImportSpecifier,
|
|
3694
|
+
// mergeImportDeclarations,
|
|
3695
|
+
// isImportUsed,
|
|
3696
|
+
// do not expose
|
|
3697
|
+
// ensureLibrary,
|
|
3698
|
+
// ensureComponentStructure,
|
|
3699
|
+
// normalizeComponent,
|
|
3700
|
+
// findComponentDirectory,
|
|
3701
|
+
// inferComponentName,
|
|
3702
|
+
matchesSelector,
|
|
3703
|
+
// ensureStyleFile,
|
|
3704
|
+
// findCssSelector,
|
|
3705
|
+
// resolveCssClassConflicts,
|
|
3706
|
+
resolveStyleDependencies,
|
|
3707
|
+
listFiles,
|
|
3708
|
+
// parseAST,
|
|
3709
|
+
// parseCSS,
|
|
3710
|
+
// parseRoutes,
|
|
3711
|
+
// parseComponentDependencies,
|
|
3712
|
+
// parseProps,
|
|
3713
|
+
// // parseTypescriptTypes,
|
|
3714
|
+
// parseExports,
|
|
3715
|
+
// parseJSCode,
|
|
3716
|
+
// parseHooksUsage,
|
|
3717
|
+
// // parseStateUsage,
|
|
3718
|
+
// // parseEventHandlers,
|
|
3719
|
+
// // parseAPICalls,
|
|
3720
|
+
// // parseDOMHierarchy,
|
|
3721
|
+
generateCodeFromAST,
|
|
3722
|
+
createComponentRegistry,
|
|
3723
|
+
getProjectDependencies,
|
|
3724
|
+
runtimeUiSnapshot,
|
|
3725
|
+
getExportedFunctionNames,
|
|
3726
|
+
generateTasksDocs,
|
|
3727
|
+
resolveTaskReferences,
|
|
3728
|
+
appendAction,
|
|
3729
|
+
// mutationTasks,
|
|
3730
|
+
// astTasks,
|
|
3731
|
+
formatCode,
|
|
3732
|
+
formatObjectCode,
|
|
3733
|
+
readFile,
|
|
3734
|
+
writeFile,
|
|
3735
|
+
};
|
|
3736
|
+
//# sourceMappingURL=general.js.map
|