@wtdlee/repomap 0.3.1 → 0.3.2
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/dist/analyzers/index.js +1 -1
- package/dist/{chunk-M6YNU536.js → chunk-4K4MGTPV.js} +27 -714
- package/dist/chunk-6F4PWJZI.js +0 -1
- package/dist/chunk-J2CM7T7U.js +1 -0
- package/dist/{chunk-3YFXZAP7.js → chunk-MOEA75XK.js} +170 -359
- package/dist/{chunk-E4WRODSI.js → chunk-SL2GMDBN.js} +35 -108
- package/dist/chunk-UJT5KTVK.js +36 -0
- package/dist/chunk-VV3A3UE3.js +1 -0
- package/dist/chunk-XWZH2RDG.js +19 -0
- package/dist/cli.js +29 -395
- package/dist/env-detector-BIWJ7OYF.js +1 -0
- package/dist/generators/assets/common.css +564 -23
- package/dist/generators/index.js +1 -2
- package/dist/index.js +1 -8
- package/dist/page-map-generator-XNZ4TDJT.js +1 -0
- package/dist/rails-TJCDGBBF.js +1 -0
- package/dist/rails-map-generator-JL5PKHYP.js +1 -0
- package/dist/server/index.js +1 -7
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-3PWXDB7B.js +0 -153
- package/dist/chunk-GNBMJMET.js +0 -2519
- package/dist/chunk-OWM6WNLE.js +0 -2610
- package/dist/chunk-SSU6QFTX.js +0 -1058
- package/dist/env-detector-EEMVUEIA.js +0 -1
- package/dist/page-map-generator-6MJGPBVA.js +0 -1
- package/dist/rails-UWSDRS33.js +0 -1
- package/dist/rails-map-generator-D2URLMVJ.js +0 -2
package/dist/chunk-OWM6WNLE.js
DELETED
|
@@ -1,2610 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path7 from 'path';
|
|
3
|
-
import { Parser, Language } from 'web-tree-sitter';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { glob } from 'glob';
|
|
6
|
-
import * as fs7 from 'fs/promises';
|
|
7
|
-
|
|
8
|
-
// src/analyzers/rails/rails-routes-analyzer.ts
|
|
9
|
-
var __filename$1 = fileURLToPath(import.meta.url);
|
|
10
|
-
var __dirname$1 = path7.dirname(__filename$1);
|
|
11
|
-
var parserInitialized = false;
|
|
12
|
-
var rubyLanguage = null;
|
|
13
|
-
var parser = null;
|
|
14
|
-
async function initRubyParser() {
|
|
15
|
-
if (parser && rubyLanguage) {
|
|
16
|
-
return parser;
|
|
17
|
-
}
|
|
18
|
-
if (!parserInitialized) {
|
|
19
|
-
await Parser.init();
|
|
20
|
-
parserInitialized = true;
|
|
21
|
-
}
|
|
22
|
-
parser = new Parser();
|
|
23
|
-
const possiblePaths = [
|
|
24
|
-
// When running from source
|
|
25
|
-
path7.join(process.cwd(), "node_modules/tree-sitter-wasms/out/tree-sitter-ruby.wasm"),
|
|
26
|
-
// When installed as dependency
|
|
27
|
-
path7.join(__dirname$1, "../../../node_modules/tree-sitter-wasms/out/tree-sitter-ruby.wasm"),
|
|
28
|
-
path7.join(__dirname$1, "../../../../tree-sitter-wasms/out/tree-sitter-ruby.wasm")
|
|
29
|
-
];
|
|
30
|
-
let wasmPath = null;
|
|
31
|
-
for (const p of possiblePaths) {
|
|
32
|
-
if (fs.existsSync(p)) {
|
|
33
|
-
wasmPath = p;
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (!wasmPath) {
|
|
38
|
-
throw new Error("tree-sitter-ruby.wasm not found. Please install tree-sitter-wasms package.");
|
|
39
|
-
}
|
|
40
|
-
rubyLanguage = await Language.load(wasmPath);
|
|
41
|
-
parser.setLanguage(rubyLanguage);
|
|
42
|
-
return parser;
|
|
43
|
-
}
|
|
44
|
-
async function parseRuby(code) {
|
|
45
|
-
const p = await initRubyParser();
|
|
46
|
-
const tree = p.parse(code);
|
|
47
|
-
if (!tree) {
|
|
48
|
-
throw new Error("Failed to parse Ruby code");
|
|
49
|
-
}
|
|
50
|
-
return tree;
|
|
51
|
-
}
|
|
52
|
-
async function parseRubyFile(filePath) {
|
|
53
|
-
const code = fs.readFileSync(filePath, "utf-8");
|
|
54
|
-
return parseRuby(code);
|
|
55
|
-
}
|
|
56
|
-
function findNodes(node, type) {
|
|
57
|
-
const results = [];
|
|
58
|
-
if (node.type === type) {
|
|
59
|
-
results.push(node);
|
|
60
|
-
}
|
|
61
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
62
|
-
const child = node.child(i);
|
|
63
|
-
if (child) {
|
|
64
|
-
results.push(...findNodes(child, type));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return results;
|
|
68
|
-
}
|
|
69
|
-
function getChildByType(node, type) {
|
|
70
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
71
|
-
const child = node.child(i);
|
|
72
|
-
if (child && child.type === type) {
|
|
73
|
-
return child;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
function getChildrenByType(node, type) {
|
|
79
|
-
const results = [];
|
|
80
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
81
|
-
const child = node.child(i);
|
|
82
|
-
if (child && child.type === type) {
|
|
83
|
-
results.push(child);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return results;
|
|
87
|
-
}
|
|
88
|
-
function getCallArguments(callNode) {
|
|
89
|
-
const args = callNode.childForFieldName("arguments");
|
|
90
|
-
if (!args) return [];
|
|
91
|
-
const results = [];
|
|
92
|
-
for (let i = 0; i < args.childCount; i++) {
|
|
93
|
-
const child = args.child(i);
|
|
94
|
-
if (child && child.type !== "(" && child.type !== ")" && child.type !== ",") {
|
|
95
|
-
results.push(child);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return results;
|
|
99
|
-
}
|
|
100
|
-
function getClassName(classNode) {
|
|
101
|
-
const nameNode = classNode.childForFieldName("name");
|
|
102
|
-
return nameNode ? nameNode.text : null;
|
|
103
|
-
}
|
|
104
|
-
function getSuperclass(classNode) {
|
|
105
|
-
const superclassNode = classNode.childForFieldName("superclass");
|
|
106
|
-
if (!superclassNode) return null;
|
|
107
|
-
const constantNode = getChildByType(superclassNode, "constant") || getChildByType(superclassNode, "scope_resolution");
|
|
108
|
-
return constantNode ? constantNode.text : null;
|
|
109
|
-
}
|
|
110
|
-
function getMethodName(methodNode) {
|
|
111
|
-
const nameNode = methodNode.childForFieldName("name");
|
|
112
|
-
return nameNode ? nameNode.text : null;
|
|
113
|
-
}
|
|
114
|
-
function getMethodParameters(methodNode) {
|
|
115
|
-
const paramsNode = methodNode.childForFieldName("parameters");
|
|
116
|
-
if (!paramsNode) return [];
|
|
117
|
-
const params = [];
|
|
118
|
-
for (let i = 0; i < paramsNode.childCount; i++) {
|
|
119
|
-
const child = paramsNode.child(i);
|
|
120
|
-
if (child && (child.type === "identifier" || child.type === "keyword_parameter" || child.type === "optional_parameter" || child.type === "splat_parameter")) {
|
|
121
|
-
const nameNode = child.childForFieldName("name") || child;
|
|
122
|
-
if (nameNode.type === "identifier") {
|
|
123
|
-
params.push(nameNode.text);
|
|
124
|
-
} else {
|
|
125
|
-
params.push(child.text);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return params;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// src/analyzers/rails/rails-routes-analyzer.ts
|
|
133
|
-
var RailsRoutesAnalyzer = class {
|
|
134
|
-
constructor(rootPath) {
|
|
135
|
-
this.rootPath = rootPath;
|
|
136
|
-
this.routesDir = path7.join(rootPath, "config", "routes");
|
|
137
|
-
}
|
|
138
|
-
routesDir;
|
|
139
|
-
routes = [];
|
|
140
|
-
namespaces = [];
|
|
141
|
-
resources = [];
|
|
142
|
-
mountedEngines = [];
|
|
143
|
-
drawnFiles = [];
|
|
144
|
-
errors = [];
|
|
145
|
-
async analyze() {
|
|
146
|
-
const mainRoutesFile = path7.join(this.rootPath, "config", "routes.rb");
|
|
147
|
-
if (!fs.existsSync(mainRoutesFile)) {
|
|
148
|
-
return {
|
|
149
|
-
routes: [],
|
|
150
|
-
namespaces: [],
|
|
151
|
-
resources: [],
|
|
152
|
-
mountedEngines: [],
|
|
153
|
-
drawnFiles: [],
|
|
154
|
-
errors: [`routes.rb not found at ${mainRoutesFile}`]
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
try {
|
|
158
|
-
await this.parseRoutesFile(mainRoutesFile, []);
|
|
159
|
-
} catch (error) {
|
|
160
|
-
this.errors.push(`Error parsing ${mainRoutesFile}: ${error}`);
|
|
161
|
-
}
|
|
162
|
-
return {
|
|
163
|
-
routes: this.routes,
|
|
164
|
-
namespaces: [...new Set(this.namespaces)],
|
|
165
|
-
resources: this.resources,
|
|
166
|
-
mountedEngines: this.mountedEngines,
|
|
167
|
-
drawnFiles: this.drawnFiles,
|
|
168
|
-
errors: this.errors
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
async parseRoutesFile(filePath, currentNamespaces) {
|
|
172
|
-
const tree = await parseRubyFile(filePath);
|
|
173
|
-
const rootNode = tree.rootNode;
|
|
174
|
-
const calls = findNodes(rootNode, "call");
|
|
175
|
-
for (const call of calls) {
|
|
176
|
-
const methodNode = call.childForFieldName("method");
|
|
177
|
-
if (!methodNode) continue;
|
|
178
|
-
const methodName = methodNode.text;
|
|
179
|
-
const line = call.startPosition.row + 1;
|
|
180
|
-
switch (methodName) {
|
|
181
|
-
case "get":
|
|
182
|
-
case "post":
|
|
183
|
-
case "put":
|
|
184
|
-
case "patch":
|
|
185
|
-
case "delete":
|
|
186
|
-
case "match":
|
|
187
|
-
this.parseHttpRoute(call, methodName, currentNamespaces, line);
|
|
188
|
-
break;
|
|
189
|
-
case "resources":
|
|
190
|
-
case "resource":
|
|
191
|
-
await this.parseResources(call, currentNamespaces, line, methodName === "resource");
|
|
192
|
-
break;
|
|
193
|
-
case "namespace":
|
|
194
|
-
await this.parseNamespace(call, currentNamespaces, filePath);
|
|
195
|
-
break;
|
|
196
|
-
case "mount":
|
|
197
|
-
this.parseMount(call, line);
|
|
198
|
-
break;
|
|
199
|
-
case "draw":
|
|
200
|
-
await this.parseDraw(call, currentNamespaces);
|
|
201
|
-
break;
|
|
202
|
-
case "devise_for":
|
|
203
|
-
this.parseDeviseFor(call, currentNamespaces, line);
|
|
204
|
-
break;
|
|
205
|
-
case "root":
|
|
206
|
-
this.parseRoot(call, currentNamespaces, line);
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
parseHttpRoute(call, method, namespaces, line) {
|
|
212
|
-
const args = getCallArguments(call);
|
|
213
|
-
if (args.length === 0) return;
|
|
214
|
-
const pathArg = args[0];
|
|
215
|
-
const routePath = this.extractStringValue(pathArg);
|
|
216
|
-
if (!routePath) return;
|
|
217
|
-
let controller = "";
|
|
218
|
-
let action = "";
|
|
219
|
-
for (const arg of args) {
|
|
220
|
-
if (arg.type === "hash" || arg.type === "pair") {
|
|
221
|
-
const pairs = arg.type === "hash" ? getChildrenByType(arg, "pair") : [arg];
|
|
222
|
-
for (const pair of pairs) {
|
|
223
|
-
const key = pair.child(0)?.text?.replace(/^:/, "");
|
|
224
|
-
const value = pair.child(2);
|
|
225
|
-
if (key === "to" && value) {
|
|
226
|
-
const toValue = this.extractStringValue(value);
|
|
227
|
-
if (toValue && toValue.includes("#")) {
|
|
228
|
-
[controller, action] = toValue.split("#");
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
} else if (arg.type === "string" || arg.type === "string_content") {
|
|
233
|
-
const strValue = this.extractStringValue(arg);
|
|
234
|
-
if (strValue && strValue.includes("#") && !controller) {
|
|
235
|
-
[controller, action] = strValue.split("#");
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
if (!controller && !action) {
|
|
240
|
-
const pathParts = routePath.replace(/^\//, "").split("/");
|
|
241
|
-
controller = pathParts[0] || "";
|
|
242
|
-
action = pathParts[1] || "index";
|
|
243
|
-
}
|
|
244
|
-
if (namespaces.length > 0 && controller && !controller.includes("/")) {
|
|
245
|
-
controller = `${namespaces.join("/")}/${controller}`;
|
|
246
|
-
}
|
|
247
|
-
const fullPath = this.buildPath(namespaces, routePath);
|
|
248
|
-
this.routes.push({
|
|
249
|
-
method: method === "match" ? "ALL" : method.toUpperCase(),
|
|
250
|
-
path: fullPath,
|
|
251
|
-
controller,
|
|
252
|
-
action,
|
|
253
|
-
namespace: namespaces.join("/") || void 0,
|
|
254
|
-
line
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
async parseResources(call, namespaces, line, singular) {
|
|
258
|
-
const args = getCallArguments(call);
|
|
259
|
-
if (args.length === 0) return;
|
|
260
|
-
const nameArg = args[0];
|
|
261
|
-
const resourceName = nameArg.text.replace(/^:/, "");
|
|
262
|
-
const resource = {
|
|
263
|
-
name: resourceName,
|
|
264
|
-
controller: namespaces.length > 0 ? `${namespaces.join("/")}/${resourceName}` : resourceName,
|
|
265
|
-
nested: [],
|
|
266
|
-
memberRoutes: [],
|
|
267
|
-
collectionRoutes: [],
|
|
268
|
-
line
|
|
269
|
-
};
|
|
270
|
-
for (const arg of args) {
|
|
271
|
-
if (arg.type === "hash") {
|
|
272
|
-
const pairs = getChildrenByType(arg, "pair");
|
|
273
|
-
for (const pair of pairs) {
|
|
274
|
-
const key = pair.child(0)?.text?.replace(/^:/, "");
|
|
275
|
-
const value = pair.child(2);
|
|
276
|
-
if (key === "only" && value) {
|
|
277
|
-
resource.only = this.extractArrayValues(value);
|
|
278
|
-
} else if (key === "except" && value) {
|
|
279
|
-
resource.except = this.extractArrayValues(value);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
this.generateResourceRoutes(resource, namespaces, singular);
|
|
285
|
-
this.resources.push(resource);
|
|
286
|
-
const block = call.childForFieldName("block");
|
|
287
|
-
if (block) {
|
|
288
|
-
const nestedCalls = findNodes(block, "call");
|
|
289
|
-
for (const nestedCall of nestedCalls) {
|
|
290
|
-
const nestedMethod = nestedCall.childForFieldName("method")?.text;
|
|
291
|
-
if (nestedMethod === "member") {
|
|
292
|
-
const memberBlock = nestedCall.childForFieldName("block");
|
|
293
|
-
if (memberBlock) {
|
|
294
|
-
this.parseMemberCollectionRoutes(memberBlock, resource, namespaces, "member");
|
|
295
|
-
}
|
|
296
|
-
} else if (nestedMethod === "collection") {
|
|
297
|
-
const collectionBlock = nestedCall.childForFieldName("block");
|
|
298
|
-
if (collectionBlock) {
|
|
299
|
-
this.parseMemberCollectionRoutes(collectionBlock, resource, namespaces, "collection");
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
parseMemberCollectionRoutes(block, resource, namespaces, type) {
|
|
306
|
-
const calls = findNodes(block, "call");
|
|
307
|
-
for (const call of calls) {
|
|
308
|
-
const methodNode = call.childForFieldName("method");
|
|
309
|
-
if (!methodNode) continue;
|
|
310
|
-
const method = methodNode.text;
|
|
311
|
-
if (!["get", "post", "put", "patch", "delete"].includes(method)) continue;
|
|
312
|
-
const args = getCallArguments(call);
|
|
313
|
-
if (args.length === 0) continue;
|
|
314
|
-
const actionName = args[0].text.replace(/^:/, "");
|
|
315
|
-
const basePath = type === "member" ? `/${resource.name}/:id/${actionName}` : `/${resource.name}/${actionName}`;
|
|
316
|
-
const route = {
|
|
317
|
-
method: method.toUpperCase(),
|
|
318
|
-
path: this.buildPath(namespaces, basePath),
|
|
319
|
-
controller: resource.controller,
|
|
320
|
-
action: actionName,
|
|
321
|
-
namespace: namespaces.join("/") || void 0,
|
|
322
|
-
line: call.startPosition.row + 1
|
|
323
|
-
};
|
|
324
|
-
if (type === "member") {
|
|
325
|
-
resource.memberRoutes.push(route);
|
|
326
|
-
} else {
|
|
327
|
-
resource.collectionRoutes.push(route);
|
|
328
|
-
}
|
|
329
|
-
this.routes.push(route);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
async parseNamespace(call, currentNamespaces, _currentFile) {
|
|
333
|
-
const args = getCallArguments(call);
|
|
334
|
-
if (args.length === 0) return;
|
|
335
|
-
const nsName = args[0].text.replace(/^:/, "");
|
|
336
|
-
this.namespaces.push(nsName);
|
|
337
|
-
const newNamespaces = [...currentNamespaces, nsName];
|
|
338
|
-
const block = call.childForFieldName("block");
|
|
339
|
-
if (block) {
|
|
340
|
-
const nestedCalls = findNodes(block, "call");
|
|
341
|
-
for (const nestedCall of nestedCalls) {
|
|
342
|
-
const methodNode = nestedCall.childForFieldName("method");
|
|
343
|
-
if (!methodNode) continue;
|
|
344
|
-
const methodName = methodNode.text;
|
|
345
|
-
const line = nestedCall.startPosition.row + 1;
|
|
346
|
-
switch (methodName) {
|
|
347
|
-
case "get":
|
|
348
|
-
case "post":
|
|
349
|
-
case "put":
|
|
350
|
-
case "patch":
|
|
351
|
-
case "delete":
|
|
352
|
-
case "match":
|
|
353
|
-
this.parseHttpRoute(nestedCall, methodName, newNamespaces, line);
|
|
354
|
-
break;
|
|
355
|
-
case "resources":
|
|
356
|
-
case "resource":
|
|
357
|
-
await this.parseResources(nestedCall, newNamespaces, line, methodName === "resource");
|
|
358
|
-
break;
|
|
359
|
-
case "draw":
|
|
360
|
-
await this.parseDraw(nestedCall, newNamespaces);
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
parseMount(call, line) {
|
|
367
|
-
const args = getCallArguments(call);
|
|
368
|
-
if (args.length === 0) return;
|
|
369
|
-
const engineArg = args[0];
|
|
370
|
-
const engine = engineArg.text;
|
|
371
|
-
let mountPath = "/";
|
|
372
|
-
for (const arg of args) {
|
|
373
|
-
if (arg.type === "hash" || arg.type === "pair") {
|
|
374
|
-
const pairs = arg.type === "hash" ? getChildrenByType(arg, "pair") : [arg];
|
|
375
|
-
for (const pair of pairs) {
|
|
376
|
-
const key = pair.child(0)?.text?.replace(/^:/, "");
|
|
377
|
-
const value = pair.child(2);
|
|
378
|
-
if (key === "at" && value) {
|
|
379
|
-
mountPath = this.extractStringValue(value) || mountPath;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
} else if (arg.type === "string") {
|
|
383
|
-
const strValue = this.extractStringValue(arg);
|
|
384
|
-
if (strValue && strValue.startsWith("/")) {
|
|
385
|
-
mountPath = strValue;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
this.mountedEngines.push({
|
|
390
|
-
engine,
|
|
391
|
-
mountPath,
|
|
392
|
-
line
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
async parseDraw(call, namespaces) {
|
|
396
|
-
const args = getCallArguments(call);
|
|
397
|
-
if (args.length === 0) return;
|
|
398
|
-
const drawName = args[0].text.replace(/^:/, "");
|
|
399
|
-
const drawFile = path7.join(this.routesDir, `${drawName}.rb`);
|
|
400
|
-
if (fs.existsSync(drawFile)) {
|
|
401
|
-
this.drawnFiles.push(drawFile);
|
|
402
|
-
try {
|
|
403
|
-
await this.parseRoutesFile(drawFile, namespaces);
|
|
404
|
-
} catch (error) {
|
|
405
|
-
this.errors.push(`Error parsing drawn file ${drawFile}: ${error}`);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
parseDeviseFor(call, namespaces, line) {
|
|
410
|
-
const args = getCallArguments(call);
|
|
411
|
-
if (args.length === 0) return;
|
|
412
|
-
const resource = args[0].text.replace(/^:/, "");
|
|
413
|
-
const deviseRoutes = [
|
|
414
|
-
{ method: "GET", path: `/${resource}/sign_in`, action: "new", controller: "devise/sessions" },
|
|
415
|
-
{
|
|
416
|
-
method: "POST",
|
|
417
|
-
path: `/${resource}/sign_in`,
|
|
418
|
-
action: "create",
|
|
419
|
-
controller: "devise/sessions"
|
|
420
|
-
},
|
|
421
|
-
{
|
|
422
|
-
method: "DELETE",
|
|
423
|
-
path: `/${resource}/sign_out`,
|
|
424
|
-
action: "destroy",
|
|
425
|
-
controller: "devise/sessions"
|
|
426
|
-
},
|
|
427
|
-
{
|
|
428
|
-
method: "GET",
|
|
429
|
-
path: `/${resource}/password/new`,
|
|
430
|
-
action: "new",
|
|
431
|
-
controller: "devise/passwords"
|
|
432
|
-
},
|
|
433
|
-
{
|
|
434
|
-
method: "POST",
|
|
435
|
-
path: `/${resource}/password`,
|
|
436
|
-
action: "create",
|
|
437
|
-
controller: "devise/passwords"
|
|
438
|
-
},
|
|
439
|
-
{
|
|
440
|
-
method: "GET",
|
|
441
|
-
path: `/${resource}/sign_up`,
|
|
442
|
-
action: "new",
|
|
443
|
-
controller: "devise/registrations"
|
|
444
|
-
},
|
|
445
|
-
{
|
|
446
|
-
method: "POST",
|
|
447
|
-
path: `/${resource}`,
|
|
448
|
-
action: "create",
|
|
449
|
-
controller: "devise/registrations"
|
|
450
|
-
}
|
|
451
|
-
];
|
|
452
|
-
for (const dr of deviseRoutes) {
|
|
453
|
-
this.routes.push({
|
|
454
|
-
method: dr.method,
|
|
455
|
-
path: this.buildPath(namespaces, dr.path),
|
|
456
|
-
controller: dr.controller,
|
|
457
|
-
action: dr.action,
|
|
458
|
-
namespace: namespaces.join("/") || void 0,
|
|
459
|
-
line,
|
|
460
|
-
authenticated: false
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
parseRoot(call, namespaces, line) {
|
|
465
|
-
const args = getCallArguments(call);
|
|
466
|
-
let controller = "";
|
|
467
|
-
let action = "index";
|
|
468
|
-
for (const arg of args) {
|
|
469
|
-
if (arg.type === "string") {
|
|
470
|
-
const value = this.extractStringValue(arg);
|
|
471
|
-
if (value && value.includes("#")) {
|
|
472
|
-
[controller, action] = value.split("#");
|
|
473
|
-
}
|
|
474
|
-
} else if (arg.type === "hash" || arg.type === "pair") {
|
|
475
|
-
const pairs = arg.type === "hash" ? getChildrenByType(arg, "pair") : [arg];
|
|
476
|
-
for (const pair of pairs) {
|
|
477
|
-
const key = pair.child(0)?.text?.replace(/^:/, "");
|
|
478
|
-
const value = pair.child(2);
|
|
479
|
-
if (key === "to" && value) {
|
|
480
|
-
const toValue = this.extractStringValue(value);
|
|
481
|
-
if (toValue && toValue.includes("#")) {
|
|
482
|
-
[controller, action] = toValue.split("#");
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
if (controller) {
|
|
489
|
-
this.routes.push({
|
|
490
|
-
method: "GET",
|
|
491
|
-
path: this.buildPath(namespaces, "/"),
|
|
492
|
-
controller,
|
|
493
|
-
action,
|
|
494
|
-
namespace: namespaces.join("/") || void 0,
|
|
495
|
-
line
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
generateResourceRoutes(resource, namespaces, singular) {
|
|
500
|
-
const basePath = this.buildPath(namespaces, `/${resource.name}`);
|
|
501
|
-
const allActions = singular ? ["show", "new", "create", "edit", "update", "destroy"] : ["index", "show", "new", "create", "edit", "update", "destroy"];
|
|
502
|
-
const actions = resource.only || (resource.except ? allActions.filter((a) => !resource.except.includes(a)) : allActions);
|
|
503
|
-
const restfulRoutes = [];
|
|
504
|
-
if (!singular) {
|
|
505
|
-
if (actions.includes("index")) {
|
|
506
|
-
restfulRoutes.push({ method: "GET", path: basePath, action: "index" });
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
if (actions.includes("new")) {
|
|
510
|
-
restfulRoutes.push({ method: "GET", path: `${basePath}/new`, action: "new" });
|
|
511
|
-
}
|
|
512
|
-
if (actions.includes("create")) {
|
|
513
|
-
restfulRoutes.push({ method: "POST", path: basePath, action: "create" });
|
|
514
|
-
}
|
|
515
|
-
const showPath = singular ? basePath : `${basePath}/:id`;
|
|
516
|
-
if (actions.includes("show")) {
|
|
517
|
-
restfulRoutes.push({ method: "GET", path: showPath, action: "show" });
|
|
518
|
-
}
|
|
519
|
-
if (actions.includes("edit")) {
|
|
520
|
-
restfulRoutes.push({ method: "GET", path: `${showPath}/edit`, action: "edit" });
|
|
521
|
-
}
|
|
522
|
-
if (actions.includes("update")) {
|
|
523
|
-
restfulRoutes.push({ method: "PUT", path: showPath, action: "update" });
|
|
524
|
-
restfulRoutes.push({ method: "PATCH", path: showPath, action: "update" });
|
|
525
|
-
}
|
|
526
|
-
if (actions.includes("destroy")) {
|
|
527
|
-
restfulRoutes.push({ method: "DELETE", path: showPath, action: "destroy" });
|
|
528
|
-
}
|
|
529
|
-
for (const route of restfulRoutes) {
|
|
530
|
-
this.routes.push({
|
|
531
|
-
method: route.method,
|
|
532
|
-
path: route.path,
|
|
533
|
-
controller: resource.controller,
|
|
534
|
-
action: route.action,
|
|
535
|
-
namespace: namespaces.join("/") || void 0,
|
|
536
|
-
line: resource.line
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
buildPath(namespaces, routePath) {
|
|
541
|
-
if (routePath.startsWith("/")) {
|
|
542
|
-
return routePath;
|
|
543
|
-
}
|
|
544
|
-
const nsPath = namespaces.length > 0 ? `/${namespaces.join("/")}` : "";
|
|
545
|
-
return `${nsPath}/${routePath}`;
|
|
546
|
-
}
|
|
547
|
-
extractStringValue(node) {
|
|
548
|
-
if (node.type === "string") {
|
|
549
|
-
const content = getChildByType(node, "string_content");
|
|
550
|
-
return content ? content.text : node.text.replace(/^["']|["']$/g, "");
|
|
551
|
-
}
|
|
552
|
-
if (node.type === "string_content") {
|
|
553
|
-
return node.text;
|
|
554
|
-
}
|
|
555
|
-
if (node.type === "simple_symbol" || node.type === "symbol") {
|
|
556
|
-
return node.text.replace(/^:/, "");
|
|
557
|
-
}
|
|
558
|
-
return node.text.replace(/^["']|["']$/g, "");
|
|
559
|
-
}
|
|
560
|
-
extractArrayValues(node) {
|
|
561
|
-
const values = [];
|
|
562
|
-
if (node.type === "array") {
|
|
563
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
564
|
-
const child = node.child(i);
|
|
565
|
-
if (child && child.type !== "[" && child.type !== "]" && child.type !== ",") {
|
|
566
|
-
const value = this.extractStringValue(child);
|
|
567
|
-
if (value) values.push(value);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
} else {
|
|
571
|
-
const value = this.extractStringValue(node);
|
|
572
|
-
if (value) values.push(value);
|
|
573
|
-
}
|
|
574
|
-
return values;
|
|
575
|
-
}
|
|
576
|
-
};
|
|
577
|
-
async function main() {
|
|
578
|
-
const targetPath = process.argv[2] || process.cwd();
|
|
579
|
-
console.log(`Analyzing routes in: ${targetPath}`);
|
|
580
|
-
const analyzer = new RailsRoutesAnalyzer(targetPath);
|
|
581
|
-
const result = await analyzer.analyze();
|
|
582
|
-
console.log("\n=== Rails Routes Analysis ===\n");
|
|
583
|
-
console.log(`Total routes: ${result.routes.length}`);
|
|
584
|
-
console.log(`Namespaces: ${result.namespaces.join(", ") || "(none)"}`);
|
|
585
|
-
console.log(`Resources: ${result.resources.length}`);
|
|
586
|
-
console.log(`Mounted engines: ${result.mountedEngines.length}`);
|
|
587
|
-
console.log(`External route files: ${result.drawnFiles.length}`);
|
|
588
|
-
if (result.errors.length > 0) {
|
|
589
|
-
console.log(`
|
|
590
|
-
--- Errors ---`);
|
|
591
|
-
for (const error of result.errors) {
|
|
592
|
-
console.log(` \u274C ${error}`);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
console.log("\n--- Sample Routes (first 30) ---");
|
|
596
|
-
for (const route of result.routes.slice(0, 30)) {
|
|
597
|
-
console.log(
|
|
598
|
-
` ${route.method.padEnd(7)} ${route.path.padEnd(50)} => ${route.controller}#${route.action}`
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
console.log("\n--- Mounted Engines ---");
|
|
602
|
-
for (const engine of result.mountedEngines) {
|
|
603
|
-
console.log(` ${engine.engine} => ${engine.mountPath}`);
|
|
604
|
-
}
|
|
605
|
-
console.log("\n--- External Route Files ---");
|
|
606
|
-
for (const file of result.drawnFiles) {
|
|
607
|
-
console.log(` ${path7.basename(file)}`);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
var isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
|
611
|
-
if (isMainModule) {
|
|
612
|
-
main().catch(console.error);
|
|
613
|
-
}
|
|
614
|
-
var RailsControllerAnalyzer = class {
|
|
615
|
-
constructor(rootPath) {
|
|
616
|
-
this.rootPath = rootPath;
|
|
617
|
-
this.controllersDir = path7.join(rootPath, "app", "controllers");
|
|
618
|
-
}
|
|
619
|
-
controllersDir;
|
|
620
|
-
controllers = [];
|
|
621
|
-
errors = [];
|
|
622
|
-
async analyze() {
|
|
623
|
-
if (!fs.existsSync(this.controllersDir)) {
|
|
624
|
-
return {
|
|
625
|
-
controllers: [],
|
|
626
|
-
totalActions: 0,
|
|
627
|
-
namespaces: [],
|
|
628
|
-
concerns: [],
|
|
629
|
-
errors: [`Controllers directory not found at ${this.controllersDir}`]
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
const controllerFiles = await glob("**/*_controller.rb", {
|
|
633
|
-
cwd: this.controllersDir,
|
|
634
|
-
ignore: ["concerns/**"]
|
|
635
|
-
});
|
|
636
|
-
for (const file of controllerFiles) {
|
|
637
|
-
const fullPath = path7.join(this.controllersDir, file);
|
|
638
|
-
try {
|
|
639
|
-
const controller = await this.parseControllerFile(fullPath, file);
|
|
640
|
-
if (controller) {
|
|
641
|
-
this.controllers.push(controller);
|
|
642
|
-
}
|
|
643
|
-
} catch (error) {
|
|
644
|
-
this.errors.push(`Error parsing ${file}: ${error}`);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
const namespaces = [
|
|
648
|
-
...new Set(this.controllers.filter((c) => c.namespace).map((c) => c.namespace))
|
|
649
|
-
];
|
|
650
|
-
const concerns = [...new Set(this.controllers.flatMap((c) => c.concerns))];
|
|
651
|
-
const totalActions = this.controllers.reduce((sum, c) => sum + c.actions.length, 0);
|
|
652
|
-
return {
|
|
653
|
-
controllers: this.controllers,
|
|
654
|
-
totalActions,
|
|
655
|
-
namespaces,
|
|
656
|
-
concerns,
|
|
657
|
-
errors: this.errors
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
async parseControllerFile(filePath, relativePath) {
|
|
661
|
-
const tree = await parseRubyFile(filePath);
|
|
662
|
-
const rootNode = tree.rootNode;
|
|
663
|
-
const pathParts = relativePath.replace(/_controller\.rb$/, "").split("/");
|
|
664
|
-
const namespace = pathParts.length > 1 ? pathParts.slice(0, -1).join("/") : void 0;
|
|
665
|
-
const controllerName = pathParts[pathParts.length - 1];
|
|
666
|
-
const classNodes = findNodes(rootNode, "class");
|
|
667
|
-
if (classNodes.length === 0) return null;
|
|
668
|
-
const classNode = classNodes[0];
|
|
669
|
-
const className = getClassName(classNode);
|
|
670
|
-
const parentClass = getSuperclass(classNode);
|
|
671
|
-
if (!className) return null;
|
|
672
|
-
const controller = {
|
|
673
|
-
name: controllerName,
|
|
674
|
-
filePath: relativePath,
|
|
675
|
-
className,
|
|
676
|
-
parentClass: parentClass || "ApplicationController",
|
|
677
|
-
namespace,
|
|
678
|
-
actions: [],
|
|
679
|
-
beforeActions: [],
|
|
680
|
-
afterActions: [],
|
|
681
|
-
aroundActions: [],
|
|
682
|
-
skipBeforeActions: [],
|
|
683
|
-
concerns: [],
|
|
684
|
-
helpers: [],
|
|
685
|
-
rescueFrom: [],
|
|
686
|
-
line: classNode.startPosition.row + 1
|
|
687
|
-
};
|
|
688
|
-
const calls = findNodes(classNode, "call");
|
|
689
|
-
for (const call of calls) {
|
|
690
|
-
const methodNode = call.childForFieldName("method");
|
|
691
|
-
if (!methodNode) continue;
|
|
692
|
-
const methodName = methodNode.text;
|
|
693
|
-
const line = call.startPosition.row + 1;
|
|
694
|
-
switch (methodName) {
|
|
695
|
-
case "before_action":
|
|
696
|
-
case "before_filter":
|
|
697
|
-
this.parseFilter(call, controller.beforeActions, line);
|
|
698
|
-
break;
|
|
699
|
-
case "after_action":
|
|
700
|
-
case "after_filter":
|
|
701
|
-
this.parseFilter(call, controller.afterActions, line);
|
|
702
|
-
break;
|
|
703
|
-
case "around_action":
|
|
704
|
-
case "around_filter":
|
|
705
|
-
this.parseFilter(call, controller.aroundActions, line);
|
|
706
|
-
break;
|
|
707
|
-
case "skip_before_action":
|
|
708
|
-
case "skip_before_filter":
|
|
709
|
-
this.parseFilter(call, controller.skipBeforeActions, line);
|
|
710
|
-
break;
|
|
711
|
-
case "include":
|
|
712
|
-
this.parseInclude(call, controller.concerns);
|
|
713
|
-
break;
|
|
714
|
-
case "helper":
|
|
715
|
-
this.parseHelper(call, controller.helpers);
|
|
716
|
-
break;
|
|
717
|
-
case "layout":
|
|
718
|
-
controller.layoutInfo = this.parseLayout(call);
|
|
719
|
-
break;
|
|
720
|
-
case "rescue_from":
|
|
721
|
-
this.parseRescueFrom(call, controller.rescueFrom, line);
|
|
722
|
-
break;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
findNodes(classNode, "method");
|
|
726
|
-
let currentVisibility = "public";
|
|
727
|
-
const bodyStatement = classNode.childForFieldName("body");
|
|
728
|
-
if (bodyStatement) {
|
|
729
|
-
for (let i = 0; i < bodyStatement.childCount; i++) {
|
|
730
|
-
const child = bodyStatement.child(i);
|
|
731
|
-
if (!child) continue;
|
|
732
|
-
if (child.type === "identifier") {
|
|
733
|
-
const text = child.text;
|
|
734
|
-
if (text === "private") currentVisibility = "private";
|
|
735
|
-
else if (text === "protected") currentVisibility = "protected";
|
|
736
|
-
else if (text === "public") currentVisibility = "public";
|
|
737
|
-
} else if (child.type === "method") {
|
|
738
|
-
const action = this.parseMethod(child, currentVisibility);
|
|
739
|
-
if (action) {
|
|
740
|
-
controller.actions.push(action);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
return controller;
|
|
746
|
-
}
|
|
747
|
-
parseFilter(call, filters, line) {
|
|
748
|
-
const args = this.getCallArguments(call);
|
|
749
|
-
if (args.length === 0) return;
|
|
750
|
-
const nameArg = args[0];
|
|
751
|
-
const filterName = nameArg.text.replace(/^:/, "");
|
|
752
|
-
const filter = {
|
|
753
|
-
name: filterName,
|
|
754
|
-
line
|
|
755
|
-
};
|
|
756
|
-
for (const arg of args.slice(1)) {
|
|
757
|
-
if (arg.type === "hash") {
|
|
758
|
-
const pairs = getChildrenByType(arg, "pair");
|
|
759
|
-
for (const pair of pairs) {
|
|
760
|
-
const key = pair.child(0)?.text?.replace(/^:/, "");
|
|
761
|
-
const value = pair.child(2);
|
|
762
|
-
if (!key || !value) continue;
|
|
763
|
-
switch (key) {
|
|
764
|
-
case "only":
|
|
765
|
-
filter.only = this.extractArrayValues(value);
|
|
766
|
-
break;
|
|
767
|
-
case "except":
|
|
768
|
-
filter.except = this.extractArrayValues(value);
|
|
769
|
-
break;
|
|
770
|
-
case "if":
|
|
771
|
-
filter.if = value.text;
|
|
772
|
-
break;
|
|
773
|
-
case "unless":
|
|
774
|
-
filter.unless = value.text;
|
|
775
|
-
break;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
filters.push(filter);
|
|
781
|
-
}
|
|
782
|
-
parseInclude(call, concerns) {
|
|
783
|
-
const args = this.getCallArguments(call);
|
|
784
|
-
for (const arg of args) {
|
|
785
|
-
if (arg.type === "constant" || arg.type === "scope_resolution") {
|
|
786
|
-
concerns.push(arg.text);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
parseHelper(call, helpers) {
|
|
791
|
-
const args = this.getCallArguments(call);
|
|
792
|
-
for (const arg of args) {
|
|
793
|
-
const value = arg.text.replace(/^:/, "");
|
|
794
|
-
helpers.push(value);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
parseLayout(call) {
|
|
798
|
-
const args = this.getCallArguments(call);
|
|
799
|
-
if (args.length === 0) return void 0;
|
|
800
|
-
const nameArg = args[0];
|
|
801
|
-
let layoutName = nameArg.text.replace(/^["']|["']$/g, "");
|
|
802
|
-
if (layoutName.startsWith(":")) {
|
|
803
|
-
layoutName = layoutName.substring(1);
|
|
804
|
-
}
|
|
805
|
-
if (nameArg.type === "lambda" || nameArg.type === "proc") {
|
|
806
|
-
layoutName = "(dynamic)";
|
|
807
|
-
}
|
|
808
|
-
const layout = { name: layoutName };
|
|
809
|
-
for (const arg of args.slice(1)) {
|
|
810
|
-
if (arg.type === "hash") {
|
|
811
|
-
layout.conditions = arg.text;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
return layout;
|
|
815
|
-
}
|
|
816
|
-
parseRescueFrom(call, rescues, line) {
|
|
817
|
-
const args = this.getCallArguments(call);
|
|
818
|
-
if (args.length === 0) return;
|
|
819
|
-
const exception = args[0].text;
|
|
820
|
-
let handler = "unknown";
|
|
821
|
-
for (const arg of args.slice(1)) {
|
|
822
|
-
if (arg.type === "hash" || arg.type === "pair") {
|
|
823
|
-
const pairs = arg.type === "hash" ? getChildrenByType(arg, "pair") : [arg];
|
|
824
|
-
for (const pair of pairs) {
|
|
825
|
-
const key = pair.child(0)?.text?.replace(/^:/, "");
|
|
826
|
-
const value = pair.child(2);
|
|
827
|
-
if (key === "with" && value) {
|
|
828
|
-
handler = value.text.replace(/^:/, "");
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
rescues.push({ exception, handler, line });
|
|
834
|
-
}
|
|
835
|
-
parseMethod(methodNode, visibility) {
|
|
836
|
-
const name = getMethodName(methodNode);
|
|
837
|
-
if (!name) return null;
|
|
838
|
-
if (methodNode.text.includes("def self.")) return null;
|
|
839
|
-
const action = {
|
|
840
|
-
name,
|
|
841
|
-
line: methodNode.startPosition.row + 1,
|
|
842
|
-
visibility,
|
|
843
|
-
parameters: getMethodParameters(methodNode),
|
|
844
|
-
servicesCalled: [],
|
|
845
|
-
modelsCalled: [],
|
|
846
|
-
methodCalls: [],
|
|
847
|
-
instanceVarAssignments: []
|
|
848
|
-
};
|
|
849
|
-
const bodyContent = methodNode.text;
|
|
850
|
-
const ivarRegex = /@([a-z_][a-z0-9_]*)\s*=\s*([^\n]+)/gi;
|
|
851
|
-
let ivarMatch;
|
|
852
|
-
while ((ivarMatch = ivarRegex.exec(bodyContent)) !== null) {
|
|
853
|
-
const varName = ivarMatch[1];
|
|
854
|
-
const assignedValue = ivarMatch[2].trim().slice(0, 100);
|
|
855
|
-
let assignedType;
|
|
856
|
-
const modelMatch = assignedValue.match(
|
|
857
|
-
/^([A-Z][a-zA-Z0-9]+)\.(find|find_by|find_by!|where|all|first|last|new|create|create!|build)/
|
|
858
|
-
);
|
|
859
|
-
if (modelMatch) {
|
|
860
|
-
assignedType = modelMatch[1];
|
|
861
|
-
}
|
|
862
|
-
const assocMatch = assignedValue.match(/^@([a-z_]+)\.([a-z_]+)/);
|
|
863
|
-
if (assocMatch && !assignedType) {
|
|
864
|
-
assignedType = `${assocMatch[1]}.${assocMatch[2]}`;
|
|
865
|
-
}
|
|
866
|
-
const currentMatch = assignedValue.match(/^current_([a-z_]+)/);
|
|
867
|
-
if (currentMatch && !assignedType) {
|
|
868
|
-
assignedType = currentMatch[1].charAt(0).toUpperCase() + currentMatch[1].slice(1);
|
|
869
|
-
}
|
|
870
|
-
const serviceMatch = assignedValue.match(/^([A-Z][a-zA-Z0-9]+Service)\.(call|new|perform)/);
|
|
871
|
-
if (serviceMatch && !assignedType) {
|
|
872
|
-
assignedType = `Service:${serviceMatch[1]}`;
|
|
873
|
-
}
|
|
874
|
-
if (action.instanceVarAssignments) {
|
|
875
|
-
action.instanceVarAssignments.push({
|
|
876
|
-
name: varName,
|
|
877
|
-
assignedType,
|
|
878
|
-
assignedValue: assignedValue.length > 60 ? assignedValue.slice(0, 57) + "..." : assignedValue
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
if (bodyContent.includes("render json:") || bodyContent.includes("render :json")) {
|
|
883
|
-
action.rendersJson = true;
|
|
884
|
-
}
|
|
885
|
-
if (bodyContent.includes("render") && !action.rendersJson) {
|
|
886
|
-
action.rendersHtml = true;
|
|
887
|
-
}
|
|
888
|
-
const redirectMatch = bodyContent.match(/redirect_to\s+([^,\n]+)/);
|
|
889
|
-
if (redirectMatch) {
|
|
890
|
-
action.redirectsTo = redirectMatch[1].trim();
|
|
891
|
-
}
|
|
892
|
-
if (bodyContent.includes("respond_to")) {
|
|
893
|
-
const formats = [];
|
|
894
|
-
if (bodyContent.includes("format.html")) formats.push("html");
|
|
895
|
-
if (bodyContent.includes("format.json")) formats.push("json");
|
|
896
|
-
if (bodyContent.includes("format.xml")) formats.push("xml");
|
|
897
|
-
if (bodyContent.includes("format.js")) formats.push("js");
|
|
898
|
-
if (bodyContent.includes("format.csv")) formats.push("csv");
|
|
899
|
-
if (bodyContent.includes("format.pdf")) formats.push("pdf");
|
|
900
|
-
if (formats.length > 0) {
|
|
901
|
-
action.respondsTo = formats;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
const serviceCalls = findNodes(methodNode, "call");
|
|
905
|
-
for (const call of serviceCalls) {
|
|
906
|
-
const receiver = call.childForFieldName("receiver");
|
|
907
|
-
const method = call.childForFieldName("method");
|
|
908
|
-
if (receiver && method) {
|
|
909
|
-
const receiverText = receiver.text;
|
|
910
|
-
const methodText = method.text;
|
|
911
|
-
if (receiverText.endsWith("Service") && ["call", "new", "perform", "execute"].includes(methodText)) {
|
|
912
|
-
if (!action.servicesCalled.includes(receiverText)) {
|
|
913
|
-
action.servicesCalled.push(receiverText);
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
const arMethods = [
|
|
917
|
-
"find",
|
|
918
|
-
"find_by",
|
|
919
|
-
"find_by!",
|
|
920
|
-
"where",
|
|
921
|
-
"all",
|
|
922
|
-
"first",
|
|
923
|
-
"last",
|
|
924
|
-
"create",
|
|
925
|
-
"create!",
|
|
926
|
-
"new",
|
|
927
|
-
"update",
|
|
928
|
-
"update!",
|
|
929
|
-
"destroy",
|
|
930
|
-
"delete"
|
|
931
|
-
];
|
|
932
|
-
if (/^[A-Z][a-zA-Z]+$/.test(receiverText) && arMethods.includes(methodText)) {
|
|
933
|
-
if (!["Rails", "ActiveRecord", "ActionController", "ApplicationRecord"].includes(
|
|
934
|
-
receiverText
|
|
935
|
-
)) {
|
|
936
|
-
if (!action.modelsCalled.includes(receiverText)) {
|
|
937
|
-
action.modelsCalled.push(receiverText);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
action.methodCalls.push(`${receiverText}.${methodText}`);
|
|
942
|
-
} else if (method && !receiver) {
|
|
943
|
-
action.methodCalls.push(method.text);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
return action;
|
|
947
|
-
}
|
|
948
|
-
getCallArguments(call) {
|
|
949
|
-
const args = call.childForFieldName("arguments");
|
|
950
|
-
if (!args) {
|
|
951
|
-
const results2 = [];
|
|
952
|
-
for (let i = 0; i < call.childCount; i++) {
|
|
953
|
-
const child = call.child(i);
|
|
954
|
-
if (child && !["identifier", "(", ")", ",", "call"].includes(child.type)) {
|
|
955
|
-
if (child !== call.childForFieldName("method") && child !== call.childForFieldName("receiver")) {
|
|
956
|
-
results2.push(child);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
return results2;
|
|
961
|
-
}
|
|
962
|
-
const results = [];
|
|
963
|
-
for (let i = 0; i < args.childCount; i++) {
|
|
964
|
-
const child = args.child(i);
|
|
965
|
-
if (child && child.type !== "(" && child.type !== ")" && child.type !== ",") {
|
|
966
|
-
results.push(child);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
return results;
|
|
970
|
-
}
|
|
971
|
-
extractArrayValues(node) {
|
|
972
|
-
const values = [];
|
|
973
|
-
if (node.type === "array") {
|
|
974
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
975
|
-
const child = node.child(i);
|
|
976
|
-
if (child && child.type !== "[" && child.type !== "]" && child.type !== ",") {
|
|
977
|
-
values.push(child.text.replace(/^:/, ""));
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
} else {
|
|
981
|
-
values.push(node.text.replace(/^:/, ""));
|
|
982
|
-
}
|
|
983
|
-
return values;
|
|
984
|
-
}
|
|
985
|
-
};
|
|
986
|
-
async function main2() {
|
|
987
|
-
const targetPath = process.argv[2] || process.cwd();
|
|
988
|
-
console.log(`Analyzing controllers in: ${targetPath}`);
|
|
989
|
-
const analyzer = new RailsControllerAnalyzer(targetPath);
|
|
990
|
-
const result = await analyzer.analyze();
|
|
991
|
-
console.log("\n=== Rails Controllers Analysis ===\n");
|
|
992
|
-
console.log(`Total controllers: ${result.controllers.length}`);
|
|
993
|
-
console.log(`Total actions: ${result.totalActions}`);
|
|
994
|
-
console.log(`Namespaces: ${result.namespaces.join(", ") || "(none)"}`);
|
|
995
|
-
console.log(`Shared concerns: ${result.concerns.length}`);
|
|
996
|
-
if (result.errors.length > 0) {
|
|
997
|
-
console.log(`
|
|
998
|
-
--- Errors (${result.errors.length}) ---`);
|
|
999
|
-
for (const error of result.errors.slice(0, 5)) {
|
|
1000
|
-
console.log(` \u274C ${error}`);
|
|
1001
|
-
}
|
|
1002
|
-
if (result.errors.length > 5) {
|
|
1003
|
-
console.log(` ... and ${result.errors.length - 5} more`);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
console.log("\n--- Sample Controllers (first 10) ---");
|
|
1007
|
-
for (const controller of result.controllers.slice(0, 10)) {
|
|
1008
|
-
console.log(`
|
|
1009
|
-
\u{1F4C1} ${controller.className} (${controller.filePath})`);
|
|
1010
|
-
console.log(` Parent: ${controller.parentClass}`);
|
|
1011
|
-
console.log(
|
|
1012
|
-
` Actions (${controller.actions.length}): ${controller.actions.map((a) => a.name).slice(0, 5).join(", ")}${controller.actions.length > 5 ? "..." : ""}`
|
|
1013
|
-
);
|
|
1014
|
-
if (controller.beforeActions.length > 0) {
|
|
1015
|
-
console.log(` Before: ${controller.beforeActions.map((f) => f.name).join(", ")}`);
|
|
1016
|
-
}
|
|
1017
|
-
if (controller.concerns.length > 0) {
|
|
1018
|
-
console.log(` Concerns: ${controller.concerns.join(", ")}`);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
const publicActions = result.controllers.flatMap(
|
|
1022
|
-
(c) => c.actions.filter((a) => a.visibility === "public")
|
|
1023
|
-
);
|
|
1024
|
-
const privateActions = result.controllers.flatMap(
|
|
1025
|
-
(c) => c.actions.filter((a) => a.visibility === "private")
|
|
1026
|
-
);
|
|
1027
|
-
console.log("\n--- Action Visibility Summary ---");
|
|
1028
|
-
console.log(` Public: ${publicActions.length}`);
|
|
1029
|
-
console.log(` Private: ${privateActions.length}`);
|
|
1030
|
-
}
|
|
1031
|
-
var isMainModule2 = import.meta.url === `file://${process.argv[1]}`;
|
|
1032
|
-
if (isMainModule2) {
|
|
1033
|
-
main2().catch(console.error);
|
|
1034
|
-
}
|
|
1035
|
-
var RailsModelAnalyzer = class {
|
|
1036
|
-
constructor(rootPath) {
|
|
1037
|
-
this.rootPath = rootPath;
|
|
1038
|
-
this.modelsDir = path7.join(rootPath, "app", "models");
|
|
1039
|
-
}
|
|
1040
|
-
modelsDir;
|
|
1041
|
-
models = [];
|
|
1042
|
-
errors = [];
|
|
1043
|
-
async analyze() {
|
|
1044
|
-
if (!fs.existsSync(this.modelsDir)) {
|
|
1045
|
-
return {
|
|
1046
|
-
models: [],
|
|
1047
|
-
totalAssociations: 0,
|
|
1048
|
-
totalValidations: 0,
|
|
1049
|
-
concerns: [],
|
|
1050
|
-
namespaces: [],
|
|
1051
|
-
errors: [`Models directory not found at ${this.modelsDir}`]
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
|
-
const modelFiles = await glob("**/*.rb", {
|
|
1055
|
-
cwd: this.modelsDir,
|
|
1056
|
-
ignore: ["concerns/**", "application_record.rb"]
|
|
1057
|
-
});
|
|
1058
|
-
for (const file of modelFiles) {
|
|
1059
|
-
const fullPath = path7.join(this.modelsDir, file);
|
|
1060
|
-
try {
|
|
1061
|
-
const model = await this.parseModelFile(fullPath, file);
|
|
1062
|
-
if (model) {
|
|
1063
|
-
this.models.push(model);
|
|
1064
|
-
}
|
|
1065
|
-
} catch (error) {
|
|
1066
|
-
this.errors.push(`Error parsing ${file}: ${error}`);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
const concerns = [...new Set(this.models.flatMap((m) => m.concerns))];
|
|
1070
|
-
const namespaces = [
|
|
1071
|
-
...new Set(
|
|
1072
|
-
this.models.map((m) => {
|
|
1073
|
-
const parts = m.filePath.split("/");
|
|
1074
|
-
return parts.length > 1 ? parts.slice(0, -1).join("/") : null;
|
|
1075
|
-
}).filter((n) => n !== null)
|
|
1076
|
-
)
|
|
1077
|
-
];
|
|
1078
|
-
const totalAssociations = this.models.reduce((sum, m) => sum + m.associations.length, 0);
|
|
1079
|
-
const totalValidations = this.models.reduce((sum, m) => sum + m.validations.length, 0);
|
|
1080
|
-
return {
|
|
1081
|
-
models: this.models,
|
|
1082
|
-
totalAssociations,
|
|
1083
|
-
totalValidations,
|
|
1084
|
-
concerns,
|
|
1085
|
-
namespaces,
|
|
1086
|
-
errors: this.errors
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
async parseModelFile(filePath, relativePath) {
|
|
1090
|
-
const tree = await parseRubyFile(filePath);
|
|
1091
|
-
const rootNode = tree.rootNode;
|
|
1092
|
-
const classNodes = findNodes(rootNode, "class");
|
|
1093
|
-
if (classNodes.length === 0) return null;
|
|
1094
|
-
const classNode = classNodes[0];
|
|
1095
|
-
const className = getClassName(classNode);
|
|
1096
|
-
const parentClass = getSuperclass(classNode);
|
|
1097
|
-
if (!className) return null;
|
|
1098
|
-
if (parentClass && !this.isActiveRecordModel(parentClass)) ;
|
|
1099
|
-
const model = {
|
|
1100
|
-
name: className.replace(/.*::/, ""),
|
|
1101
|
-
// Remove namespace prefix
|
|
1102
|
-
filePath: relativePath,
|
|
1103
|
-
className,
|
|
1104
|
-
parentClass: parentClass || "ApplicationRecord",
|
|
1105
|
-
associations: [],
|
|
1106
|
-
validations: [],
|
|
1107
|
-
callbacks: [],
|
|
1108
|
-
scopes: [],
|
|
1109
|
-
concerns: [],
|
|
1110
|
-
enums: [],
|
|
1111
|
-
attributes: [],
|
|
1112
|
-
classMethodsCount: 0,
|
|
1113
|
-
instanceMethodsCount: 0,
|
|
1114
|
-
line: classNode.startPosition.row + 1
|
|
1115
|
-
};
|
|
1116
|
-
model.tableName = this.parseTableName(classNode);
|
|
1117
|
-
const calls = findNodes(classNode, "call");
|
|
1118
|
-
for (const call of calls) {
|
|
1119
|
-
const methodNode = call.childForFieldName("method");
|
|
1120
|
-
if (!methodNode) continue;
|
|
1121
|
-
const methodName = methodNode.text;
|
|
1122
|
-
const line = call.startPosition.row + 1;
|
|
1123
|
-
if (["belongs_to", "has_one", "has_many", "has_and_belongs_to_many"].includes(methodName)) {
|
|
1124
|
-
this.parseAssociation(
|
|
1125
|
-
call,
|
|
1126
|
-
methodName,
|
|
1127
|
-
model.associations,
|
|
1128
|
-
line
|
|
1129
|
-
);
|
|
1130
|
-
} else if (methodName.startsWith("validates") || methodName === "validate") {
|
|
1131
|
-
this.parseValidation(call, methodName, model.validations, line);
|
|
1132
|
-
} else if (this.isCallback(methodName)) {
|
|
1133
|
-
this.parseCallback(call, methodName, model.callbacks, line);
|
|
1134
|
-
} else if (methodName === "scope") {
|
|
1135
|
-
this.parseScope(call, model.scopes, line);
|
|
1136
|
-
} else if (methodName === "include") {
|
|
1137
|
-
this.parseInclude(call, model.concerns);
|
|
1138
|
-
} else if (methodName === "enum") {
|
|
1139
|
-
this.parseEnum(call, model.enums, line);
|
|
1140
|
-
} else if (methodName === "attribute") {
|
|
1141
|
-
this.parseAttribute(call, model.attributes, line);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
const methods = findNodes(classNode, "method");
|
|
1145
|
-
const singletonMethods = findNodes(classNode, "singleton_method");
|
|
1146
|
-
model.instanceMethodsCount = methods.length;
|
|
1147
|
-
model.classMethodsCount = singletonMethods.length;
|
|
1148
|
-
return model;
|
|
1149
|
-
}
|
|
1150
|
-
isActiveRecordModel(parentClass) {
|
|
1151
|
-
const arBases = ["ApplicationRecord", "ActiveRecord::Base", "ActiveRecord"];
|
|
1152
|
-
return arBases.some((base) => parentClass.includes(base));
|
|
1153
|
-
}
|
|
1154
|
-
parseTableName(classNode) {
|
|
1155
|
-
const calls = findNodes(classNode, "call");
|
|
1156
|
-
for (const call of calls) {
|
|
1157
|
-
const methodNode = call.childForFieldName("method");
|
|
1158
|
-
if (methodNode?.text === "table_name=") {
|
|
1159
|
-
const args = this.getCallArguments(call);
|
|
1160
|
-
if (args.length > 0) {
|
|
1161
|
-
return args[0].text.replace(/^["']|["']$/g, "");
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
const assignments = findNodes(classNode, "assignment");
|
|
1166
|
-
for (const assignment of assignments) {
|
|
1167
|
-
const left = assignment.child(0);
|
|
1168
|
-
if (left?.text?.includes("table_name")) {
|
|
1169
|
-
const right = assignment.child(2);
|
|
1170
|
-
if (right) {
|
|
1171
|
-
return right.text.replace(/^["']|["']$/g, "");
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
return void 0;
|
|
1176
|
-
}
|
|
1177
|
-
parseAssociation(call, type, associations, line) {
|
|
1178
|
-
const args = this.getCallArguments(call);
|
|
1179
|
-
if (args.length === 0) return;
|
|
1180
|
-
const nameArg = args[0];
|
|
1181
|
-
const assocName = nameArg.text.replace(/^:/, "");
|
|
1182
|
-
const association = {
|
|
1183
|
-
type,
|
|
1184
|
-
name: assocName,
|
|
1185
|
-
line
|
|
1186
|
-
};
|
|
1187
|
-
for (const arg of args.slice(1)) {
|
|
1188
|
-
if (arg.type === "hash") {
|
|
1189
|
-
const pairs = getChildrenByType(arg, "pair");
|
|
1190
|
-
for (const pair of pairs) {
|
|
1191
|
-
const key = pair.child(0)?.text?.replace(/^:/, "");
|
|
1192
|
-
const value = pair.child(2);
|
|
1193
|
-
if (!key || !value) continue;
|
|
1194
|
-
switch (key) {
|
|
1195
|
-
case "class_name":
|
|
1196
|
-
association.className = value.text.replace(/^["']|["']$/g, "");
|
|
1197
|
-
break;
|
|
1198
|
-
case "foreign_key":
|
|
1199
|
-
association.foreignKey = value.text.replace(/^["']|["']$/g, "").replace(/^:/, "");
|
|
1200
|
-
break;
|
|
1201
|
-
case "through":
|
|
1202
|
-
association.through = value.text.replace(/^:/, "");
|
|
1203
|
-
break;
|
|
1204
|
-
case "polymorphic":
|
|
1205
|
-
association.polymorphic = value.text === "true";
|
|
1206
|
-
break;
|
|
1207
|
-
case "dependent":
|
|
1208
|
-
association.dependent = value.text.replace(/^:/, "");
|
|
1209
|
-
break;
|
|
1210
|
-
case "optional":
|
|
1211
|
-
association.optional = value.text === "true";
|
|
1212
|
-
break;
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
associations.push(association);
|
|
1218
|
-
}
|
|
1219
|
-
parseValidation(call, methodName, validations, line) {
|
|
1220
|
-
const args = this.getCallArguments(call);
|
|
1221
|
-
if (args.length === 0) return;
|
|
1222
|
-
const attributes = [];
|
|
1223
|
-
const options = {};
|
|
1224
|
-
let validationType = methodName;
|
|
1225
|
-
for (const arg of args) {
|
|
1226
|
-
if (arg.type === "simple_symbol" || arg.type === "symbol") {
|
|
1227
|
-
attributes.push(arg.text.replace(/^:/, ""));
|
|
1228
|
-
} else if (arg.type === "hash") {
|
|
1229
|
-
const pairs = getChildrenByType(arg, "pair");
|
|
1230
|
-
for (const pair of pairs) {
|
|
1231
|
-
const key = pair.child(0)?.text?.replace(/^:/, "");
|
|
1232
|
-
const value = pair.child(2);
|
|
1233
|
-
if (key && value) {
|
|
1234
|
-
if ([
|
|
1235
|
-
"presence",
|
|
1236
|
-
"uniqueness",
|
|
1237
|
-
"numericality",
|
|
1238
|
-
"length",
|
|
1239
|
-
"format",
|
|
1240
|
-
"inclusion",
|
|
1241
|
-
"exclusion",
|
|
1242
|
-
"acceptance",
|
|
1243
|
-
"confirmation"
|
|
1244
|
-
].includes(key)) {
|
|
1245
|
-
validationType = key;
|
|
1246
|
-
options[key] = value.text;
|
|
1247
|
-
} else {
|
|
1248
|
-
options[key] = value.text;
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
if (attributes.length > 0 || methodName === "validate") {
|
|
1255
|
-
validations.push({
|
|
1256
|
-
type: validationType,
|
|
1257
|
-
attributes,
|
|
1258
|
-
options: Object.keys(options).length > 0 ? options : void 0,
|
|
1259
|
-
line
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
isCallback(methodName) {
|
|
1264
|
-
const callbacks = [
|
|
1265
|
-
"before_validation",
|
|
1266
|
-
"after_validation",
|
|
1267
|
-
"before_save",
|
|
1268
|
-
"around_save",
|
|
1269
|
-
"after_save",
|
|
1270
|
-
"before_create",
|
|
1271
|
-
"around_create",
|
|
1272
|
-
"after_create",
|
|
1273
|
-
"before_update",
|
|
1274
|
-
"around_update",
|
|
1275
|
-
"after_update",
|
|
1276
|
-
"before_destroy",
|
|
1277
|
-
"around_destroy",
|
|
1278
|
-
"after_destroy",
|
|
1279
|
-
"after_commit",
|
|
1280
|
-
"after_rollback",
|
|
1281
|
-
"after_initialize",
|
|
1282
|
-
"after_find",
|
|
1283
|
-
"after_touch"
|
|
1284
|
-
];
|
|
1285
|
-
return callbacks.includes(methodName);
|
|
1286
|
-
}
|
|
1287
|
-
parseCallback(call, type, callbacks, line) {
|
|
1288
|
-
const args = this.getCallArguments(call);
|
|
1289
|
-
if (args.length === 0) return;
|
|
1290
|
-
const methodArg = args[0];
|
|
1291
|
-
const methodName = methodArg.text.replace(/^:/, "");
|
|
1292
|
-
const callback = {
|
|
1293
|
-
type,
|
|
1294
|
-
method: methodName,
|
|
1295
|
-
line
|
|
1296
|
-
};
|
|
1297
|
-
for (const arg of args.slice(1)) {
|
|
1298
|
-
if (arg.type === "hash") {
|
|
1299
|
-
const pairs = getChildrenByType(arg, "pair");
|
|
1300
|
-
for (const pair of pairs) {
|
|
1301
|
-
const key = pair.child(0)?.text?.replace(/^:/, "");
|
|
1302
|
-
const value = pair.child(2);
|
|
1303
|
-
if (key && value && ["if", "unless"].includes(key)) {
|
|
1304
|
-
callback.conditions = `${key}: ${value.text}`;
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
|
-
callbacks.push(callback);
|
|
1310
|
-
}
|
|
1311
|
-
parseScope(call, scopes, line) {
|
|
1312
|
-
const args = this.getCallArguments(call);
|
|
1313
|
-
if (args.length === 0) return;
|
|
1314
|
-
const nameArg = args[0];
|
|
1315
|
-
const scopeName = nameArg.text.replace(/^:/, "");
|
|
1316
|
-
const isLambda = args.length > 1 && (args[1].type === "lambda" || args[1].text.includes("->"));
|
|
1317
|
-
scopes.push({
|
|
1318
|
-
name: scopeName,
|
|
1319
|
-
lambda: isLambda,
|
|
1320
|
-
line
|
|
1321
|
-
});
|
|
1322
|
-
}
|
|
1323
|
-
parseInclude(call, concerns) {
|
|
1324
|
-
const args = this.getCallArguments(call);
|
|
1325
|
-
for (const arg of args) {
|
|
1326
|
-
if (arg.type === "constant" || arg.type === "scope_resolution") {
|
|
1327
|
-
concerns.push(arg.text);
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
parseEnum(call, enums, line) {
|
|
1332
|
-
const args = this.getCallArguments(call);
|
|
1333
|
-
if (args.length === 0) return;
|
|
1334
|
-
for (const arg of args) {
|
|
1335
|
-
if (arg.type === "hash") {
|
|
1336
|
-
const pairs = getChildrenByType(arg, "pair");
|
|
1337
|
-
for (const pair of pairs) {
|
|
1338
|
-
const key = pair.child(0)?.text?.replace(/^:/, "");
|
|
1339
|
-
const value = pair.child(2);
|
|
1340
|
-
if (key && value && value.type === "hash") {
|
|
1341
|
-
const enumValues = [];
|
|
1342
|
-
const valuePairs = getChildrenByType(value, "pair");
|
|
1343
|
-
for (const vp of valuePairs) {
|
|
1344
|
-
const vKey = vp.child(0)?.text?.replace(/^:/, "");
|
|
1345
|
-
if (vKey) enumValues.push(vKey);
|
|
1346
|
-
}
|
|
1347
|
-
enums.push({
|
|
1348
|
-
name: key,
|
|
1349
|
-
values: enumValues,
|
|
1350
|
-
line
|
|
1351
|
-
});
|
|
1352
|
-
} else if (key && value && value.type === "array") {
|
|
1353
|
-
const enumValues = [];
|
|
1354
|
-
for (let i = 0; i < value.childCount; i++) {
|
|
1355
|
-
const child = value.child(i);
|
|
1356
|
-
if (child && child.type !== "[" && child.type !== "]" && child.type !== ",") {
|
|
1357
|
-
enumValues.push(child.text.replace(/^:/, ""));
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
enums.push({
|
|
1361
|
-
name: key,
|
|
1362
|
-
values: enumValues,
|
|
1363
|
-
line
|
|
1364
|
-
});
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
parseAttribute(call, attributes, line) {
|
|
1371
|
-
const args = this.getCallArguments(call);
|
|
1372
|
-
if (args.length === 0) return;
|
|
1373
|
-
const nameArg = args[0];
|
|
1374
|
-
const attrName = nameArg.text.replace(/^:/, "");
|
|
1375
|
-
const attr = {
|
|
1376
|
-
name: attrName,
|
|
1377
|
-
line
|
|
1378
|
-
};
|
|
1379
|
-
if (args.length > 1) {
|
|
1380
|
-
const typeArg = args[1];
|
|
1381
|
-
attr.type = typeArg.text.replace(/^:/, "");
|
|
1382
|
-
}
|
|
1383
|
-
for (const arg of args) {
|
|
1384
|
-
if (arg.type === "hash") {
|
|
1385
|
-
const pairs = getChildrenByType(arg, "pair");
|
|
1386
|
-
for (const pair of pairs) {
|
|
1387
|
-
const key = pair.child(0)?.text?.replace(/^:/, "");
|
|
1388
|
-
const value = pair.child(2);
|
|
1389
|
-
if (key === "default" && value) {
|
|
1390
|
-
attr.default = value.text;
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
attributes.push(attr);
|
|
1396
|
-
}
|
|
1397
|
-
getCallArguments(call) {
|
|
1398
|
-
const args = call.childForFieldName("arguments");
|
|
1399
|
-
if (!args) {
|
|
1400
|
-
const results2 = [];
|
|
1401
|
-
for (let i = 0; i < call.childCount; i++) {
|
|
1402
|
-
const child = call.child(i);
|
|
1403
|
-
if (child && !["identifier", "(", ")", ",", "call"].includes(child.type)) {
|
|
1404
|
-
if (child !== call.childForFieldName("method") && child !== call.childForFieldName("receiver")) {
|
|
1405
|
-
results2.push(child);
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
return results2;
|
|
1410
|
-
}
|
|
1411
|
-
const results = [];
|
|
1412
|
-
for (let i = 0; i < args.childCount; i++) {
|
|
1413
|
-
const child = args.child(i);
|
|
1414
|
-
if (child && child.type !== "(" && child.type !== ")" && child.type !== ",") {
|
|
1415
|
-
results.push(child);
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
return results;
|
|
1419
|
-
}
|
|
1420
|
-
};
|
|
1421
|
-
async function main3() {
|
|
1422
|
-
const targetPath = process.argv[2] || process.cwd();
|
|
1423
|
-
console.log(`Analyzing models in: ${targetPath}`);
|
|
1424
|
-
const analyzer = new RailsModelAnalyzer(targetPath);
|
|
1425
|
-
const result = await analyzer.analyze();
|
|
1426
|
-
console.log("\n=== Rails Models Analysis ===\n");
|
|
1427
|
-
console.log(`Total models: ${result.models.length}`);
|
|
1428
|
-
console.log(`Total associations: ${result.totalAssociations}`);
|
|
1429
|
-
console.log(`Total validations: ${result.totalValidations}`);
|
|
1430
|
-
console.log(`Shared concerns: ${result.concerns.length}`);
|
|
1431
|
-
console.log(`Namespaces: ${result.namespaces.join(", ") || "(none)"}`);
|
|
1432
|
-
if (result.errors.length > 0) {
|
|
1433
|
-
console.log(`
|
|
1434
|
-
--- Errors (${result.errors.length}) ---`);
|
|
1435
|
-
for (const error of result.errors.slice(0, 5)) {
|
|
1436
|
-
console.log(` \u274C ${error}`);
|
|
1437
|
-
}
|
|
1438
|
-
if (result.errors.length > 5) {
|
|
1439
|
-
console.log(` ... and ${result.errors.length - 5} more`);
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
console.log("\n--- Sample Models (first 15) ---");
|
|
1443
|
-
for (const model of result.models.slice(0, 15)) {
|
|
1444
|
-
console.log(`
|
|
1445
|
-
\u{1F4E6} ${model.className} (${model.filePath})`);
|
|
1446
|
-
console.log(` Parent: ${model.parentClass}`);
|
|
1447
|
-
if (model.associations.length > 0) {
|
|
1448
|
-
const assocs = model.associations.slice(0, 3).map((a) => `${a.type} :${a.name}`);
|
|
1449
|
-
console.log(
|
|
1450
|
-
` Associations: ${assocs.join(", ")}${model.associations.length > 3 ? "..." : ""}`
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
if (model.validations.length > 0) {
|
|
1454
|
-
console.log(` Validations: ${model.validations.length}`);
|
|
1455
|
-
}
|
|
1456
|
-
if (model.scopes.length > 0) {
|
|
1457
|
-
const scopeNames = model.scopes.slice(0, 3).map((s) => s.name);
|
|
1458
|
-
console.log(` Scopes: ${scopeNames.join(", ")}${model.scopes.length > 3 ? "..." : ""}`);
|
|
1459
|
-
}
|
|
1460
|
-
if (model.enums.length > 0) {
|
|
1461
|
-
const enumInfo = model.enums.map((e) => `${e.name}(${e.values.length})`);
|
|
1462
|
-
console.log(` Enums: ${enumInfo.join(", ")}`);
|
|
1463
|
-
}
|
|
1464
|
-
if (model.concerns.length > 0) {
|
|
1465
|
-
console.log(
|
|
1466
|
-
` Concerns: ${model.concerns.slice(0, 3).join(", ")}${model.concerns.length > 3 ? "..." : ""}`
|
|
1467
|
-
);
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
const allAssociations = result.models.flatMap((m) => m.associations);
|
|
1471
|
-
const belongsTo = allAssociations.filter((a) => a.type === "belongs_to").length;
|
|
1472
|
-
const hasMany = allAssociations.filter((a) => a.type === "has_many").length;
|
|
1473
|
-
const hasOne = allAssociations.filter((a) => a.type === "has_one").length;
|
|
1474
|
-
console.log("\n--- Association Summary ---");
|
|
1475
|
-
console.log(` belongs_to: ${belongsTo}`);
|
|
1476
|
-
console.log(` has_many: ${hasMany}`);
|
|
1477
|
-
console.log(` has_one: ${hasOne}`);
|
|
1478
|
-
}
|
|
1479
|
-
var isMainModule3 = import.meta.url === `file://${process.argv[1]}`;
|
|
1480
|
-
if (isMainModule3) {
|
|
1481
|
-
main3().catch(console.error);
|
|
1482
|
-
}
|
|
1483
|
-
var RailsGrpcAnalyzer = class {
|
|
1484
|
-
constructor(rootPath) {
|
|
1485
|
-
this.rootPath = rootPath;
|
|
1486
|
-
this.grpcDir = path7.join(rootPath, "app", "grpc_services");
|
|
1487
|
-
}
|
|
1488
|
-
grpcDir;
|
|
1489
|
-
services = [];
|
|
1490
|
-
errors = [];
|
|
1491
|
-
async analyze() {
|
|
1492
|
-
if (!fs.existsSync(this.grpcDir)) {
|
|
1493
|
-
return {
|
|
1494
|
-
services: [],
|
|
1495
|
-
totalRpcs: 0,
|
|
1496
|
-
namespaces: [],
|
|
1497
|
-
errors: [`gRPC services directory not found at ${this.grpcDir}`]
|
|
1498
|
-
};
|
|
1499
|
-
}
|
|
1500
|
-
const serviceFiles = await glob("**/*_grpc_service.rb", {
|
|
1501
|
-
cwd: this.grpcDir
|
|
1502
|
-
});
|
|
1503
|
-
for (const file of serviceFiles) {
|
|
1504
|
-
const fullPath = path7.join(this.grpcDir, file);
|
|
1505
|
-
try {
|
|
1506
|
-
const service = await this.parseServiceFile(fullPath, file);
|
|
1507
|
-
if (service) {
|
|
1508
|
-
this.services.push(service);
|
|
1509
|
-
}
|
|
1510
|
-
} catch (error) {
|
|
1511
|
-
this.errors.push(`Error parsing ${file}: ${error}`);
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
const namespaces = [
|
|
1515
|
-
...new Set(this.services.filter((s) => s.namespace).map((s) => s.namespace))
|
|
1516
|
-
];
|
|
1517
|
-
const totalRpcs = this.services.reduce((sum, s) => sum + s.rpcs.length, 0);
|
|
1518
|
-
return {
|
|
1519
|
-
services: this.services,
|
|
1520
|
-
totalRpcs,
|
|
1521
|
-
namespaces,
|
|
1522
|
-
errors: this.errors
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1525
|
-
async parseServiceFile(filePath, relativePath) {
|
|
1526
|
-
const tree = await parseRubyFile(filePath);
|
|
1527
|
-
const rootNode = tree.rootNode;
|
|
1528
|
-
const pathParts = relativePath.replace(/_grpc_service\.rb$/, "").split("/");
|
|
1529
|
-
const namespace = pathParts.length > 1 ? pathParts.slice(0, -1).join("/") : void 0;
|
|
1530
|
-
const serviceName = pathParts[pathParts.length - 1];
|
|
1531
|
-
const classNodes = findNodes(rootNode, "class");
|
|
1532
|
-
if (classNodes.length === 0) return null;
|
|
1533
|
-
const classNode = classNodes[0];
|
|
1534
|
-
const className = getClassName(classNode);
|
|
1535
|
-
const parentClass = getSuperclass(classNode);
|
|
1536
|
-
if (!className) return null;
|
|
1537
|
-
const service = {
|
|
1538
|
-
name: serviceName,
|
|
1539
|
-
filePath: relativePath,
|
|
1540
|
-
className,
|
|
1541
|
-
parentClass: parentClass || "Unknown",
|
|
1542
|
-
namespace,
|
|
1543
|
-
rpcs: [],
|
|
1544
|
-
policies: [],
|
|
1545
|
-
serializers: [],
|
|
1546
|
-
concerns: [],
|
|
1547
|
-
line: classNode.startPosition.row + 1
|
|
1548
|
-
};
|
|
1549
|
-
if (parentClass) {
|
|
1550
|
-
const protoMatch = parentClass.match(/(\w+)::Service$/);
|
|
1551
|
-
if (protoMatch) {
|
|
1552
|
-
service.protoService = parentClass.replace("::Service", "");
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
const calls = findNodes(classNode, "call");
|
|
1556
|
-
for (const call of calls) {
|
|
1557
|
-
const methodNode = call.childForFieldName("method");
|
|
1558
|
-
if (methodNode?.text === "include") {
|
|
1559
|
-
const args = this.getCallArguments(call);
|
|
1560
|
-
for (const arg of args) {
|
|
1561
|
-
if (arg.type === "constant" || arg.type === "scope_resolution") {
|
|
1562
|
-
service.concerns.push(arg.text);
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
findNodes(classNode, "method");
|
|
1568
|
-
let currentVisibility = "public";
|
|
1569
|
-
const bodyStatement = classNode.childForFieldName("body");
|
|
1570
|
-
if (bodyStatement) {
|
|
1571
|
-
for (let i = 0; i < bodyStatement.childCount; i++) {
|
|
1572
|
-
const child = bodyStatement.child(i);
|
|
1573
|
-
if (!child) continue;
|
|
1574
|
-
if (child.type === "identifier") {
|
|
1575
|
-
const text = child.text;
|
|
1576
|
-
if (text === "private") currentVisibility = "private";
|
|
1577
|
-
else if (text === "protected") currentVisibility = "protected";
|
|
1578
|
-
else if (text === "public") currentVisibility = "public";
|
|
1579
|
-
} else if (child.type === "method" && currentVisibility === "public") {
|
|
1580
|
-
const rpc = this.parseRpcMethod(child);
|
|
1581
|
-
if (rpc) {
|
|
1582
|
-
const methodCalls = findNodes(child, "call");
|
|
1583
|
-
for (const mc of methodCalls) {
|
|
1584
|
-
const methodName = mc.childForFieldName("method")?.text;
|
|
1585
|
-
const receiver = mc.childForFieldName("receiver")?.text;
|
|
1586
|
-
if (methodName === "authorize!" || methodName === "new") {
|
|
1587
|
-
if (receiver?.includes("Policy")) {
|
|
1588
|
-
if (!service.policies.includes(receiver)) {
|
|
1589
|
-
service.policies.push(receiver);
|
|
1590
|
-
}
|
|
1591
|
-
rpc.policyMethod = "authorize!";
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
if (receiver?.includes("Serializer") && methodName === "new") {
|
|
1595
|
-
if (!service.serializers.includes(receiver)) {
|
|
1596
|
-
service.serializers.push(receiver);
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
service.rpcs.push(rpc);
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
return service;
|
|
1606
|
-
}
|
|
1607
|
-
parseRpcMethod(methodNode) {
|
|
1608
|
-
const name = getMethodName(methodNode);
|
|
1609
|
-
if (!name) return null;
|
|
1610
|
-
const skipMethods = ["initialize", "to_s", "inspect", "call", "perform", "execute"];
|
|
1611
|
-
if (skipMethods.includes(name)) return null;
|
|
1612
|
-
getMethodParameters(methodNode);
|
|
1613
|
-
const methodBody = methodNode.text;
|
|
1614
|
-
const rpc = {
|
|
1615
|
-
name,
|
|
1616
|
-
line: methodNode.startPosition.row + 1,
|
|
1617
|
-
streaming: "none",
|
|
1618
|
-
modelsUsed: [],
|
|
1619
|
-
servicesUsed: []
|
|
1620
|
-
};
|
|
1621
|
-
const requestMatch = methodBody.match(/@param\s+\[([^\]]+)\]\s+req/);
|
|
1622
|
-
if (requestMatch) {
|
|
1623
|
-
rpc.requestType = requestMatch[1];
|
|
1624
|
-
}
|
|
1625
|
-
const responseMatch = methodBody.match(/@return\s+\[([^\]]+)\]/);
|
|
1626
|
-
if (responseMatch) {
|
|
1627
|
-
rpc.responseType = responseMatch[1];
|
|
1628
|
-
}
|
|
1629
|
-
const modelMatches = methodBody.matchAll(
|
|
1630
|
-
/\b([A-Z][a-zA-Z]+)\.(find|find_by|where|all|first|last|create|joins|includes)\b/g
|
|
1631
|
-
);
|
|
1632
|
-
for (const match of modelMatches) {
|
|
1633
|
-
const modelName = match[1];
|
|
1634
|
-
if (!["Rails", "ActiveRecord", "GRPC", "Visit", "Google"].includes(modelName) && !rpc.modelsUsed.includes(modelName)) {
|
|
1635
|
-
rpc.modelsUsed.push(modelName);
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
const serviceMatches = methodBody.matchAll(/\b(\w+Service)\.(call|new|perform)\b/g);
|
|
1639
|
-
for (const match of serviceMatches) {
|
|
1640
|
-
const serviceName = match[1];
|
|
1641
|
-
if (!rpc.servicesUsed.includes(serviceName)) {
|
|
1642
|
-
rpc.servicesUsed.push(serviceName);
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
return rpc;
|
|
1646
|
-
}
|
|
1647
|
-
getCallArguments(call) {
|
|
1648
|
-
const args = call.childForFieldName("arguments");
|
|
1649
|
-
if (!args) {
|
|
1650
|
-
const results2 = [];
|
|
1651
|
-
for (let i = 0; i < call.childCount; i++) {
|
|
1652
|
-
const child = call.child(i);
|
|
1653
|
-
if (child && !["identifier", "(", ")", ",", "call"].includes(child.type)) {
|
|
1654
|
-
if (child !== call.childForFieldName("method") && child !== call.childForFieldName("receiver")) {
|
|
1655
|
-
results2.push(child);
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
return results2;
|
|
1660
|
-
}
|
|
1661
|
-
const results = [];
|
|
1662
|
-
for (let i = 0; i < args.childCount; i++) {
|
|
1663
|
-
const child = args.child(i);
|
|
1664
|
-
if (child && child.type !== "(" && child.type !== ")" && child.type !== ",") {
|
|
1665
|
-
results.push(child);
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
return results;
|
|
1669
|
-
}
|
|
1670
|
-
};
|
|
1671
|
-
async function main4() {
|
|
1672
|
-
const targetPath = process.argv[2] || process.cwd();
|
|
1673
|
-
console.log(`Analyzing gRPC services in: ${targetPath}`);
|
|
1674
|
-
const analyzer = new RailsGrpcAnalyzer(targetPath);
|
|
1675
|
-
const result = await analyzer.analyze();
|
|
1676
|
-
console.log("\n=== Rails gRPC Services Analysis ===\n");
|
|
1677
|
-
console.log(`Total services: ${result.services.length}`);
|
|
1678
|
-
console.log(`Total RPCs: ${result.totalRpcs}`);
|
|
1679
|
-
console.log(`Namespaces: ${result.namespaces.join(", ") || "(none)"}`);
|
|
1680
|
-
if (result.errors.length > 0) {
|
|
1681
|
-
console.log(`
|
|
1682
|
-
--- Errors (${result.errors.length}) ---`);
|
|
1683
|
-
for (const error of result.errors.slice(0, 5)) {
|
|
1684
|
-
console.log(` \u274C ${error}`);
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
console.log("\n--- Sample Services (first 15) ---");
|
|
1688
|
-
for (const service of result.services.slice(0, 15)) {
|
|
1689
|
-
console.log(`
|
|
1690
|
-
\u{1F4E1} ${service.className} (${service.filePath})`);
|
|
1691
|
-
console.log(` Proto: ${service.protoService || "unknown"}`);
|
|
1692
|
-
console.log(
|
|
1693
|
-
` RPCs (${service.rpcs.length}): ${service.rpcs.map((r) => r.name).join(", ")}`
|
|
1694
|
-
);
|
|
1695
|
-
if (service.policies.length > 0) {
|
|
1696
|
-
console.log(` Policies: ${service.policies.join(", ")}`);
|
|
1697
|
-
}
|
|
1698
|
-
if (service.serializers.length > 0) {
|
|
1699
|
-
console.log(` Serializers: ${service.serializers.join(", ")}`);
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
const allRpcs = result.services.flatMap((s) => s.rpcs);
|
|
1703
|
-
const rpcsWithModels = allRpcs.filter((r) => r.modelsUsed.length > 0);
|
|
1704
|
-
console.log("\n--- RPC Summary ---");
|
|
1705
|
-
console.log(` Total RPCs: ${allRpcs.length}`);
|
|
1706
|
-
console.log(` RPCs using models: ${rpcsWithModels.length}`);
|
|
1707
|
-
}
|
|
1708
|
-
var isMainModule4 = import.meta.url === `file://${process.argv[1]}`;
|
|
1709
|
-
if (isMainModule4) {
|
|
1710
|
-
main4().catch(console.error);
|
|
1711
|
-
}
|
|
1712
|
-
async function analyzeRailsViews(rootPath) {
|
|
1713
|
-
const viewsPath = path7.join(rootPath, "app/views");
|
|
1714
|
-
const controllersPath = path7.join(rootPath, "app/controllers");
|
|
1715
|
-
try {
|
|
1716
|
-
await fs7.access(viewsPath);
|
|
1717
|
-
} catch {
|
|
1718
|
-
return {
|
|
1719
|
-
views: [],
|
|
1720
|
-
pages: [],
|
|
1721
|
-
summary: { totalViews: 0, totalPages: 0, byController: {}, byTemplate: {} }
|
|
1722
|
-
};
|
|
1723
|
-
}
|
|
1724
|
-
const viewFiles = await glob("**/*.{haml,erb,html.haml,html.erb,yml}", {
|
|
1725
|
-
cwd: viewsPath,
|
|
1726
|
-
nodir: true
|
|
1727
|
-
});
|
|
1728
|
-
const views = [];
|
|
1729
|
-
const byController = {};
|
|
1730
|
-
const byTemplate = {};
|
|
1731
|
-
for (const file of viewFiles) {
|
|
1732
|
-
const view = await parseViewFile(viewsPath, file);
|
|
1733
|
-
if (view) {
|
|
1734
|
-
views.push(view);
|
|
1735
|
-
byController[view.controller] = (byController[view.controller] || 0) + 1;
|
|
1736
|
-
byTemplate[view.template] = (byTemplate[view.template] || 0) + 1;
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
const pages = await analyzeControllersForPages(controllersPath, views, rootPath);
|
|
1740
|
-
return {
|
|
1741
|
-
views,
|
|
1742
|
-
pages,
|
|
1743
|
-
summary: {
|
|
1744
|
-
totalViews: views.length,
|
|
1745
|
-
totalPages: pages.length,
|
|
1746
|
-
byController,
|
|
1747
|
-
byTemplate
|
|
1748
|
-
}
|
|
1749
|
-
};
|
|
1750
|
-
}
|
|
1751
|
-
async function parseViewFile(viewsPath, relativePath) {
|
|
1752
|
-
const fullPath = path7.join(viewsPath, relativePath);
|
|
1753
|
-
try {
|
|
1754
|
-
const content = await fs7.readFile(fullPath, "utf-8");
|
|
1755
|
-
const parts = relativePath.split("/");
|
|
1756
|
-
if (parts.some(
|
|
1757
|
-
(p) => p.endsWith("_mailer") || p === "layouts" || p === "shared" || p === "devise"
|
|
1758
|
-
)) {
|
|
1759
|
-
return null;
|
|
1760
|
-
}
|
|
1761
|
-
const fileName = parts.pop() || "";
|
|
1762
|
-
const controller = parts.join("/") || "application";
|
|
1763
|
-
const nameParts = fileName.split(".");
|
|
1764
|
-
const action = nameParts[0].replace(/^_/, "");
|
|
1765
|
-
const isPartial = fileName.startsWith("_");
|
|
1766
|
-
let template;
|
|
1767
|
-
if (fileName.endsWith(".yml")) {
|
|
1768
|
-
template = "yml";
|
|
1769
|
-
} else if (fileName.endsWith(".haml")) {
|
|
1770
|
-
template = "haml";
|
|
1771
|
-
} else if (fileName.endsWith(".erb")) {
|
|
1772
|
-
template = "erb";
|
|
1773
|
-
} else {
|
|
1774
|
-
template = "other";
|
|
1775
|
-
}
|
|
1776
|
-
const format = nameParts.length > 2 ? nameParts[1] : "html";
|
|
1777
|
-
if (isPartial) return null;
|
|
1778
|
-
let partials = [];
|
|
1779
|
-
let helpers = [];
|
|
1780
|
-
let instanceVars = [];
|
|
1781
|
-
let reactComponents = [];
|
|
1782
|
-
if (template === "yml") {
|
|
1783
|
-
instanceVars = extractYmlFields(content);
|
|
1784
|
-
reactComponents = [{ name: "App", ssr: true, propsVar: "@yml_data" }];
|
|
1785
|
-
} else {
|
|
1786
|
-
partials = extractPartials(content, template);
|
|
1787
|
-
helpers = extractHelpers(content, template);
|
|
1788
|
-
instanceVars = extractInstanceVars(content);
|
|
1789
|
-
reactComponents = extractReactComponents(content, template);
|
|
1790
|
-
}
|
|
1791
|
-
return {
|
|
1792
|
-
name: action,
|
|
1793
|
-
path: relativePath,
|
|
1794
|
-
controller,
|
|
1795
|
-
action,
|
|
1796
|
-
format,
|
|
1797
|
-
template,
|
|
1798
|
-
partials,
|
|
1799
|
-
helpers,
|
|
1800
|
-
instanceVars,
|
|
1801
|
-
reactComponents
|
|
1802
|
-
};
|
|
1803
|
-
} catch {
|
|
1804
|
-
return null;
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
function extractPartials(content, template) {
|
|
1808
|
-
const partials = [];
|
|
1809
|
-
if (template === "haml") {
|
|
1810
|
-
const matches = content.matchAll(/=\s*render\s+(?:partial:\s*)?['"]([^'"]+)['"]/g);
|
|
1811
|
-
for (const match of matches) {
|
|
1812
|
-
partials.push(match[1]);
|
|
1813
|
-
}
|
|
1814
|
-
} else if (template === "erb") {
|
|
1815
|
-
const matches = content.matchAll(/<%=?\s*render\s+(?:partial:\s*)?['"]([^'"]+)['"]/g);
|
|
1816
|
-
for (const match of matches) {
|
|
1817
|
-
partials.push(match[1]);
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
return [...new Set(partials)];
|
|
1821
|
-
}
|
|
1822
|
-
function extractHelpers(content, _template) {
|
|
1823
|
-
const helpers = [];
|
|
1824
|
-
const helperPattern = /\b(link_to|form_for|form_with|image_tag|content_for|yield|render|t|l|raw|html_safe|simple_form_for)\b/g;
|
|
1825
|
-
const matches = content.matchAll(helperPattern);
|
|
1826
|
-
for (const match of matches) {
|
|
1827
|
-
helpers.push(match[1]);
|
|
1828
|
-
}
|
|
1829
|
-
return [...new Set(helpers)];
|
|
1830
|
-
}
|
|
1831
|
-
function extractReactComponents(content, _template) {
|
|
1832
|
-
const components = [];
|
|
1833
|
-
const renderReactPattern = /render_react_component\s*\(?\s*["']([^"']+)["'](?:,\s*\{[^}]*\})?\s*(?:,\s*ssr:\s*(true|false))?\)?/g;
|
|
1834
|
-
let match;
|
|
1835
|
-
while ((match = renderReactPattern.exec(content)) !== null) {
|
|
1836
|
-
components.push({
|
|
1837
|
-
name: match[1],
|
|
1838
|
-
ssr: match[2] === "true"
|
|
1839
|
-
});
|
|
1840
|
-
}
|
|
1841
|
-
const dataReactPattern = /data:\s*\{\s*react_component:\s*["']([^"']+)["'](?:,\s*react_component_props:\s*(@?\w+)(?:\.to_json)?)?/g;
|
|
1842
|
-
while ((match = dataReactPattern.exec(content)) !== null) {
|
|
1843
|
-
components.push({
|
|
1844
|
-
name: match[1],
|
|
1845
|
-
propsVar: match[2],
|
|
1846
|
-
ssr: false
|
|
1847
|
-
});
|
|
1848
|
-
}
|
|
1849
|
-
const reactComponentPattern = /ReactComponent\s+name=["']([^"']+)["']/g;
|
|
1850
|
-
while ((match = reactComponentPattern.exec(content)) !== null) {
|
|
1851
|
-
components.push({
|
|
1852
|
-
name: match[1],
|
|
1853
|
-
ssr: false
|
|
1854
|
-
});
|
|
1855
|
-
}
|
|
1856
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1857
|
-
return components.filter((c) => {
|
|
1858
|
-
if (seen.has(c.name)) return false;
|
|
1859
|
-
seen.add(c.name);
|
|
1860
|
-
return true;
|
|
1861
|
-
});
|
|
1862
|
-
}
|
|
1863
|
-
function extractYmlFields(content) {
|
|
1864
|
-
const fields = [];
|
|
1865
|
-
const lines = content.split("\n");
|
|
1866
|
-
for (const line of lines) {
|
|
1867
|
-
const topMatch = line.match(/^([a-z_]+):/);
|
|
1868
|
-
if (topMatch && !topMatch[1].startsWith("_")) {
|
|
1869
|
-
fields.push(topMatch[1]);
|
|
1870
|
-
}
|
|
1871
|
-
const fieldMatch = line.match(/^\s+-\s+(\w+)/);
|
|
1872
|
-
if (fieldMatch) {
|
|
1873
|
-
fields.push(fieldMatch[1]);
|
|
1874
|
-
}
|
|
1875
|
-
}
|
|
1876
|
-
return [...new Set(fields)].slice(0, 50);
|
|
1877
|
-
}
|
|
1878
|
-
function extractInstanceVars(content) {
|
|
1879
|
-
const vars = [];
|
|
1880
|
-
const matches = content.matchAll(/@(\w+)/g);
|
|
1881
|
-
for (const match of matches) {
|
|
1882
|
-
if (!["import", "media", "keyframes", "charset"].includes(match[1])) {
|
|
1883
|
-
vars.push(match[1]);
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
return [...new Set(vars)];
|
|
1887
|
-
}
|
|
1888
|
-
async function analyzeControllersForPages(controllersPath, views, rootPath) {
|
|
1889
|
-
const pages = [];
|
|
1890
|
-
try {
|
|
1891
|
-
await fs7.access(controllersPath);
|
|
1892
|
-
} catch {
|
|
1893
|
-
return pages;
|
|
1894
|
-
}
|
|
1895
|
-
const controllerFiles = await glob("**/*_controller.rb", {
|
|
1896
|
-
cwd: controllersPath,
|
|
1897
|
-
nodir: true
|
|
1898
|
-
});
|
|
1899
|
-
const routesMap = await loadRoutesMap(rootPath);
|
|
1900
|
-
for (const file of controllerFiles) {
|
|
1901
|
-
const fullPath = path7.join(controllersPath, file);
|
|
1902
|
-
const content = await fs7.readFile(fullPath, "utf-8");
|
|
1903
|
-
const controllerName = file.replace(/_controller\.rb$/, "").replace(/\//g, "/");
|
|
1904
|
-
const controllerPages = parseControllerActions(content, controllerName, file, views, routesMap);
|
|
1905
|
-
pages.push(...controllerPages);
|
|
1906
|
-
}
|
|
1907
|
-
return pages;
|
|
1908
|
-
}
|
|
1909
|
-
async function loadRoutesMap(rootPath) {
|
|
1910
|
-
const routesMap = /* @__PURE__ */ new Map();
|
|
1911
|
-
try {
|
|
1912
|
-
const railsDataPath = path7.join(rootPath, ".repomap", "rails-routes.json");
|
|
1913
|
-
const data = await fs7.readFile(railsDataPath, "utf-8");
|
|
1914
|
-
const routes = JSON.parse(data);
|
|
1915
|
-
for (const route of routes) {
|
|
1916
|
-
const key = `${route.controller}#${route.action}`;
|
|
1917
|
-
routesMap.set(key, { path: route.path, method: route.method });
|
|
1918
|
-
}
|
|
1919
|
-
} catch {
|
|
1920
|
-
}
|
|
1921
|
-
return routesMap;
|
|
1922
|
-
}
|
|
1923
|
-
function parseControllerActions(content, controllerName, sourceFile, views, routesMap) {
|
|
1924
|
-
const pages = [];
|
|
1925
|
-
const publicSection = content.split(/\n\s*(private|protected)\b/)[0];
|
|
1926
|
-
const actionPattern = /def\s+(\w+)/g;
|
|
1927
|
-
let match;
|
|
1928
|
-
while ((match = actionPattern.exec(publicSection)) !== null) {
|
|
1929
|
-
const action = match[1];
|
|
1930
|
-
if (["initialize", "new", "create", "update", "destroy", "index", "show", "edit"].includes(
|
|
1931
|
-
action
|
|
1932
|
-
) || action.startsWith("set_") || action.startsWith("_")) ;
|
|
1933
|
-
const actionStart = match.index;
|
|
1934
|
-
const actionBody = extractActionBody(publicSection, actionStart);
|
|
1935
|
-
const apis = extractApiCalls(actionBody, sourceFile);
|
|
1936
|
-
const services = extractServiceCalls(actionBody);
|
|
1937
|
-
const grpcCalls = extractGrpcCalls(actionBody);
|
|
1938
|
-
const modelAccess = extractModelAccess(actionBody);
|
|
1939
|
-
const view = views.find((v) => v.controller === controllerName && v.action === action);
|
|
1940
|
-
const routeKey = `${controllerName}#${action}`;
|
|
1941
|
-
const routeInfo = routesMap.get(routeKey);
|
|
1942
|
-
pages.push({
|
|
1943
|
-
route: routeInfo?.path || `/${controllerName}/${action}`,
|
|
1944
|
-
method: routeInfo?.method || "GET",
|
|
1945
|
-
controller: controllerName,
|
|
1946
|
-
action,
|
|
1947
|
-
view,
|
|
1948
|
-
apis,
|
|
1949
|
-
services,
|
|
1950
|
-
grpcCalls,
|
|
1951
|
-
modelAccess
|
|
1952
|
-
});
|
|
1953
|
-
}
|
|
1954
|
-
return pages;
|
|
1955
|
-
}
|
|
1956
|
-
function extractActionBody(content, startIndex) {
|
|
1957
|
-
let depth = 0;
|
|
1958
|
-
let started = false;
|
|
1959
|
-
let body = "";
|
|
1960
|
-
for (let i = startIndex; i < content.length; i++) {
|
|
1961
|
-
const line = content.slice(i, content.indexOf("\n", i) + 1 || content.length);
|
|
1962
|
-
if (line.match(/^\s*(def|class|module|if|unless|case|while|until|for|begin|do)\b/)) {
|
|
1963
|
-
depth++;
|
|
1964
|
-
started = true;
|
|
1965
|
-
}
|
|
1966
|
-
if (line.match(/^\s*end\b/)) {
|
|
1967
|
-
depth--;
|
|
1968
|
-
if (started && depth === 0) {
|
|
1969
|
-
break;
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
body += line;
|
|
1973
|
-
i = content.indexOf("\n", i);
|
|
1974
|
-
if (i === -1) break;
|
|
1975
|
-
}
|
|
1976
|
-
return body;
|
|
1977
|
-
}
|
|
1978
|
-
function extractApiCalls(content, sourceFile) {
|
|
1979
|
-
const apis = [];
|
|
1980
|
-
const httpPatterns = [
|
|
1981
|
-
/HTTPClient\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
1982
|
-
/RestClient\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
1983
|
-
/Faraday\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
1984
|
-
/Net::HTTP\.(get|post)\s*\(/gi
|
|
1985
|
-
];
|
|
1986
|
-
for (const pattern of httpPatterns) {
|
|
1987
|
-
let match;
|
|
1988
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
1989
|
-
apis.push({
|
|
1990
|
-
type: "http",
|
|
1991
|
-
name: match[2] || "HTTP call",
|
|
1992
|
-
method: match[1]?.toUpperCase(),
|
|
1993
|
-
source: sourceFile
|
|
1994
|
-
});
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
return apis;
|
|
1998
|
-
}
|
|
1999
|
-
function extractServiceCalls(content) {
|
|
2000
|
-
const services = [];
|
|
2001
|
-
const pattern = /(\w+(?:::\w+)*Service)\.(?:call!?|new)/g;
|
|
2002
|
-
let match;
|
|
2003
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
2004
|
-
services.push(match[1]);
|
|
2005
|
-
}
|
|
2006
|
-
return [...new Set(services)];
|
|
2007
|
-
}
|
|
2008
|
-
function extractGrpcCalls(content) {
|
|
2009
|
-
const grpcCalls = [];
|
|
2010
|
-
const patterns = [
|
|
2011
|
-
/(\w+(?:::\w+)*Grpc(?:::\w+)?)\./g,
|
|
2012
|
-
/Grpc::(\w+(?:::\w+)*)/g,
|
|
2013
|
-
/(\w+GrpcService)\./g
|
|
2014
|
-
];
|
|
2015
|
-
for (const pattern of patterns) {
|
|
2016
|
-
let match;
|
|
2017
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
2018
|
-
grpcCalls.push(match[1]);
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
return [...new Set(grpcCalls)];
|
|
2022
|
-
}
|
|
2023
|
-
function extractModelAccess(content) {
|
|
2024
|
-
const models = [];
|
|
2025
|
-
const pattern = /([A-Z][a-zA-Z0-9]+)\.(?:find|where|find_by|first|last|all|create|update|destroy|new)/g;
|
|
2026
|
-
let match;
|
|
2027
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
2028
|
-
if (!["File", "Dir", "Time", "Date", "DateTime", "JSON", "YAML", "CSV", "Logger"].includes(
|
|
2029
|
-
match[1]
|
|
2030
|
-
)) {
|
|
2031
|
-
models.push(match[1]);
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
return [...new Set(models)];
|
|
2035
|
-
}
|
|
2036
|
-
var COMMON_ENTRY_PATTERNS = [
|
|
2037
|
-
// Webpacker (Rails 5.1+)
|
|
2038
|
-
"app/javascript/packs",
|
|
2039
|
-
"app/javascript/entrypoints",
|
|
2040
|
-
// Vite Rails
|
|
2041
|
-
"app/frontend/entrypoints",
|
|
2042
|
-
// jsbundling-rails
|
|
2043
|
-
"app/javascript/application",
|
|
2044
|
-
// Custom patterns
|
|
2045
|
-
"frontend/assets/javascripts/entries",
|
|
2046
|
-
"frontend/entries",
|
|
2047
|
-
"app/assets/javascripts/entries",
|
|
2048
|
-
"client/entries",
|
|
2049
|
-
"src/entries"
|
|
2050
|
-
];
|
|
2051
|
-
var COMMON_COMPONENT_PATTERNS = [
|
|
2052
|
-
// Standard Rails
|
|
2053
|
-
"app/javascript/components",
|
|
2054
|
-
"app/javascript/react",
|
|
2055
|
-
"app/javascript/src/components",
|
|
2056
|
-
// Webpacker
|
|
2057
|
-
"app/javascript/bundles",
|
|
2058
|
-
// Vite
|
|
2059
|
-
"app/frontend/components",
|
|
2060
|
-
"app/frontend/react",
|
|
2061
|
-
// Custom patterns
|
|
2062
|
-
"frontend/assets/javascripts/react",
|
|
2063
|
-
"frontend/assets/javascripts/components",
|
|
2064
|
-
"frontend/src",
|
|
2065
|
-
"frontend/src/components",
|
|
2066
|
-
"frontend/components",
|
|
2067
|
-
"client/components",
|
|
2068
|
-
"src/components"
|
|
2069
|
-
];
|
|
2070
|
-
var VIEW_PATTERNS = {
|
|
2071
|
-
// react-rails gem
|
|
2072
|
-
dataReactComponent: /react_component:\s*["']([A-Za-z0-9_/]+)["']/g,
|
|
2073
|
-
renderReactComponent: /render_react_component\s*\(?\s*["']([A-Za-z0-9_/]+)["']/g,
|
|
2074
|
-
// react_on_rails gem
|
|
2075
|
-
reactComponent: /<%=?\s*react_component\s*\(\s*["']([A-Za-z0-9_/]+)["']/g,
|
|
2076
|
-
reduxStore: /<%=?\s*redux_store\s*\(\s*["']([A-Za-z0-9_/]+)["']/g,
|
|
2077
|
-
// Generic patterns
|
|
2078
|
-
dataComponent: /data-component\s*[=:]\s*["']([A-Za-z0-9_/]+)["']/g,
|
|
2079
|
-
dataReactClass: /data-react-class\s*[=:]\s*["']([A-Za-z0-9_/]+)["']/g
|
|
2080
|
-
};
|
|
2081
|
-
async function analyzeReactComponents(rootPath) {
|
|
2082
|
-
const components = /* @__PURE__ */ new Map();
|
|
2083
|
-
const entryPoints = [];
|
|
2084
|
-
const detectedPaths = await detectProjectStructure(rootPath);
|
|
2085
|
-
console.log(
|
|
2086
|
-
` \u{1F4C2} Detected paths: ${detectedPaths.entryDirs.length} entry dirs, ${detectedPaths.componentDirs.length} component dirs (${detectedPaths.integrationPattern})`
|
|
2087
|
-
);
|
|
2088
|
-
for (const entryDir of detectedPaths.entryDirs) {
|
|
2089
|
-
const fullEntryDir = path7.join(rootPath, entryDir);
|
|
2090
|
-
try {
|
|
2091
|
-
await fs7.access(fullEntryDir);
|
|
2092
|
-
const entryFiles = await glob("**/*.{tsx,ts,jsx,js}", {
|
|
2093
|
-
cwd: fullEntryDir,
|
|
2094
|
-
nodir: true,
|
|
2095
|
-
ignore: ["**/*.d.ts", "**/*.test.*", "**/*.spec.*"]
|
|
2096
|
-
});
|
|
2097
|
-
for (const file of entryFiles) {
|
|
2098
|
-
const entryInfo = await parseEntryPoint(path7.join(fullEntryDir, file), file, entryDir);
|
|
2099
|
-
if (entryInfo) {
|
|
2100
|
-
entryPoints.push(entryInfo);
|
|
2101
|
-
if (entryInfo.componentName) {
|
|
2102
|
-
const existing = components.get(entryInfo.componentName);
|
|
2103
|
-
if (existing) {
|
|
2104
|
-
existing.entryFile = path7.join(entryDir, file);
|
|
2105
|
-
existing.importPath = entryInfo.imports[0];
|
|
2106
|
-
} else {
|
|
2107
|
-
components.set(entryInfo.componentName, {
|
|
2108
|
-
name: entryInfo.componentName,
|
|
2109
|
-
entryFile: path7.join(entryDir, file),
|
|
2110
|
-
importPath: entryInfo.imports[0],
|
|
2111
|
-
ssr: false,
|
|
2112
|
-
usedIn: []
|
|
2113
|
-
});
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
} catch {
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
const viewsPath = path7.join(rootPath, "app/views");
|
|
2122
|
-
try {
|
|
2123
|
-
await fs7.access(viewsPath);
|
|
2124
|
-
const viewFiles = await glob("**/*.{haml,erb,html.haml,html.erb,slim}", {
|
|
2125
|
-
cwd: viewsPath,
|
|
2126
|
-
nodir: true
|
|
2127
|
-
});
|
|
2128
|
-
for (const viewFile of viewFiles) {
|
|
2129
|
-
const usages = await findReactUsageInView(path7.join(viewsPath, viewFile), viewFile);
|
|
2130
|
-
for (const usage of usages) {
|
|
2131
|
-
const existing = components.get(usage.componentName);
|
|
2132
|
-
if (existing) {
|
|
2133
|
-
existing.usedIn.push({
|
|
2134
|
-
viewPath: viewFile,
|
|
2135
|
-
controller: usage.controller,
|
|
2136
|
-
action: usage.action,
|
|
2137
|
-
propsVar: usage.propsVar,
|
|
2138
|
-
line: usage.line,
|
|
2139
|
-
pattern: usage.pattern
|
|
2140
|
-
});
|
|
2141
|
-
if (usage.ssr) existing.ssr = true;
|
|
2142
|
-
} else {
|
|
2143
|
-
components.set(usage.componentName, {
|
|
2144
|
-
name: usage.componentName,
|
|
2145
|
-
ssr: usage.ssr,
|
|
2146
|
-
usedIn: [
|
|
2147
|
-
{
|
|
2148
|
-
viewPath: viewFile,
|
|
2149
|
-
controller: usage.controller,
|
|
2150
|
-
action: usage.action,
|
|
2151
|
-
propsVar: usage.propsVar,
|
|
2152
|
-
line: usage.line,
|
|
2153
|
-
pattern: usage.pattern
|
|
2154
|
-
}
|
|
2155
|
-
]
|
|
2156
|
-
});
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
} catch {
|
|
2161
|
-
}
|
|
2162
|
-
await resolveSourceFiles(rootPath, components, detectedPaths.componentDirs);
|
|
2163
|
-
const componentList = Array.from(components.values());
|
|
2164
|
-
const ssrCount = componentList.filter((c) => c.ssr).length;
|
|
2165
|
-
return {
|
|
2166
|
-
components: componentList,
|
|
2167
|
-
entryPoints,
|
|
2168
|
-
detectedPaths,
|
|
2169
|
-
summary: {
|
|
2170
|
-
totalComponents: componentList.length,
|
|
2171
|
-
totalEntryPoints: entryPoints.length,
|
|
2172
|
-
ssrComponents: ssrCount,
|
|
2173
|
-
clientComponents: componentList.length - ssrCount
|
|
2174
|
-
}
|
|
2175
|
-
};
|
|
2176
|
-
}
|
|
2177
|
-
async function detectProjectStructure(rootPath) {
|
|
2178
|
-
const entryDirs = [];
|
|
2179
|
-
const componentDirs = [];
|
|
2180
|
-
let integrationPattern = "unknown";
|
|
2181
|
-
for (const pattern of COMMON_ENTRY_PATTERNS) {
|
|
2182
|
-
const fullPath = path7.join(rootPath, pattern);
|
|
2183
|
-
try {
|
|
2184
|
-
const stat2 = await fs7.stat(fullPath);
|
|
2185
|
-
if (stat2.isDirectory()) {
|
|
2186
|
-
entryDirs.push(pattern);
|
|
2187
|
-
}
|
|
2188
|
-
} catch {
|
|
2189
|
-
}
|
|
2190
|
-
}
|
|
2191
|
-
for (const pattern of COMMON_COMPONENT_PATTERNS) {
|
|
2192
|
-
const fullPath = path7.join(rootPath, pattern);
|
|
2193
|
-
try {
|
|
2194
|
-
const stat2 = await fs7.stat(fullPath);
|
|
2195
|
-
if (stat2.isDirectory()) {
|
|
2196
|
-
componentDirs.push(pattern);
|
|
2197
|
-
}
|
|
2198
|
-
} catch {
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
integrationPattern = await detectIntegrationPattern(rootPath);
|
|
2202
|
-
if (entryDirs.length === 0) {
|
|
2203
|
-
const fallbackDirs = await findEntryPointDirectories(rootPath);
|
|
2204
|
-
entryDirs.push(...fallbackDirs);
|
|
2205
|
-
}
|
|
2206
|
-
if (componentDirs.length === 0 && entryDirs.length > 0) {
|
|
2207
|
-
componentDirs.push(...entryDirs);
|
|
2208
|
-
}
|
|
2209
|
-
return { entryDirs, componentDirs, integrationPattern };
|
|
2210
|
-
}
|
|
2211
|
-
async function detectIntegrationPattern(rootPath) {
|
|
2212
|
-
try {
|
|
2213
|
-
const gemfilePath = path7.join(rootPath, "Gemfile");
|
|
2214
|
-
const gemfile = await fs7.readFile(gemfilePath, "utf-8");
|
|
2215
|
-
if (gemfile.includes("react_on_rails")) return "react_on_rails";
|
|
2216
|
-
if (gemfile.includes("react-rails")) return "react-rails";
|
|
2217
|
-
if (gemfile.includes("vite_rails") || gemfile.includes("vite_ruby")) return "vite";
|
|
2218
|
-
if (gemfile.includes("webpacker")) return "webpacker";
|
|
2219
|
-
try {
|
|
2220
|
-
await fs7.access(path7.join(rootPath, "vite.config.ts"));
|
|
2221
|
-
return "vite";
|
|
2222
|
-
} catch {
|
|
2223
|
-
}
|
|
2224
|
-
try {
|
|
2225
|
-
await fs7.access(path7.join(rootPath, "vite.config.js"));
|
|
2226
|
-
return "vite";
|
|
2227
|
-
} catch {
|
|
2228
|
-
}
|
|
2229
|
-
return "custom";
|
|
2230
|
-
} catch {
|
|
2231
|
-
return "unknown";
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
async function findEntryPointDirectories(rootPath) {
|
|
2235
|
-
const dirs = [];
|
|
2236
|
-
const searchPatterns = [
|
|
2237
|
-
"app/**/entries",
|
|
2238
|
-
"app/**/packs",
|
|
2239
|
-
"frontend/**/entries",
|
|
2240
|
-
"client/**/entries",
|
|
2241
|
-
"src/**/entries"
|
|
2242
|
-
];
|
|
2243
|
-
for (const pattern of searchPatterns) {
|
|
2244
|
-
try {
|
|
2245
|
-
const matches = await glob(pattern, {
|
|
2246
|
-
cwd: rootPath,
|
|
2247
|
-
nodir: false
|
|
2248
|
-
});
|
|
2249
|
-
for (const match of matches) {
|
|
2250
|
-
const fullPath = path7.join(rootPath, match);
|
|
2251
|
-
try {
|
|
2252
|
-
const stat2 = await fs7.stat(fullPath);
|
|
2253
|
-
if (stat2.isDirectory()) {
|
|
2254
|
-
dirs.push(match);
|
|
2255
|
-
}
|
|
2256
|
-
} catch {
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
} catch {
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
return dirs;
|
|
2263
|
-
}
|
|
2264
|
-
async function parseEntryPoint(fullPath, relativePath, entryDir) {
|
|
2265
|
-
try {
|
|
2266
|
-
const content = await fs7.readFile(fullPath, "utf-8");
|
|
2267
|
-
const selectorPatterns = [
|
|
2268
|
-
/\[data-react-component[=:][\s]*["']?([A-Za-z0-9_]+)["']?\]/,
|
|
2269
|
-
/\[data-component[=:][\s]*["']?([A-Za-z0-9_]+)["']?\]/,
|
|
2270
|
-
/\[data-react-class[=:][\s]*["']?([A-Za-z0-9_]+)["']?\]/,
|
|
2271
|
-
/getElementById\s*\(\s*["']([A-Za-z0-9_-]+)["']\s*\)/,
|
|
2272
|
-
/querySelector\s*\(\s*["']#([A-Za-z0-9_-]+)["']\s*\)/
|
|
2273
|
-
];
|
|
2274
|
-
let componentName = null;
|
|
2275
|
-
let selector;
|
|
2276
|
-
for (const pattern of selectorPatterns) {
|
|
2277
|
-
const match = content.match(pattern);
|
|
2278
|
-
if (match) {
|
|
2279
|
-
componentName = match[1];
|
|
2280
|
-
selector = match[0];
|
|
2281
|
-
break;
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
if (!componentName) {
|
|
2285
|
-
const baseName = path7.basename(relativePath, path7.extname(relativePath));
|
|
2286
|
-
componentName = baseName.split(/[-_]/).map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
2287
|
-
}
|
|
2288
|
-
const imports = [];
|
|
2289
|
-
const importMatches = content.matchAll(
|
|
2290
|
-
/import\s+(?:\{[^}]+\}|\*\s+as\s+\w+|\w+)\s+from\s+["']([^"']+)["']/g
|
|
2291
|
-
);
|
|
2292
|
-
for (const match of importMatches) {
|
|
2293
|
-
const importPath = match[1];
|
|
2294
|
-
if (importPath.includes("/react/") || importPath.includes("/components/") || importPath.includes("/containers/") || importPath.includes("/bundles/") || importPath.includes("/pages/") || importPath.match(/\/[A-Z][a-zA-Z0-9]*/)) {
|
|
2295
|
-
imports.push(importPath);
|
|
2296
|
-
}
|
|
2297
|
-
}
|
|
2298
|
-
const requireMatches = content.matchAll(/require\s*\(\s*["']([^"']+)["']\s*\)/g);
|
|
2299
|
-
for (const match of requireMatches) {
|
|
2300
|
-
const requirePath = match[1];
|
|
2301
|
-
if (requirePath.includes("/react/") || requirePath.includes("/components/") || requirePath.includes("/containers/")) {
|
|
2302
|
-
imports.push(requirePath);
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
if (!componentName && imports.length === 0) return null;
|
|
2306
|
-
return {
|
|
2307
|
-
file: relativePath,
|
|
2308
|
-
fullPath: path7.join(entryDir, relativePath),
|
|
2309
|
-
componentName: componentName || "",
|
|
2310
|
-
imports,
|
|
2311
|
-
selector
|
|
2312
|
-
};
|
|
2313
|
-
} catch {
|
|
2314
|
-
return null;
|
|
2315
|
-
}
|
|
2316
|
-
}
|
|
2317
|
-
async function findReactUsageInView(fullPath, relativePath) {
|
|
2318
|
-
const usages = [];
|
|
2319
|
-
try {
|
|
2320
|
-
const content = await fs7.readFile(fullPath, "utf-8");
|
|
2321
|
-
const lines = content.split("\n");
|
|
2322
|
-
const parts = relativePath.split("/");
|
|
2323
|
-
const fileName = parts.pop() || "";
|
|
2324
|
-
const controller = parts.join("/") || "application";
|
|
2325
|
-
const action = fileName.split(".")[0].replace(/^_/, "");
|
|
2326
|
-
let lineNum = 0;
|
|
2327
|
-
for (const line of lines) {
|
|
2328
|
-
lineNum++;
|
|
2329
|
-
const dataMatches = line.matchAll(VIEW_PATTERNS.dataReactComponent);
|
|
2330
|
-
for (const match of dataMatches) {
|
|
2331
|
-
const propsMatch = line.match(/react_component_props:\s*(@?\w+(?:\.\w+)*)/);
|
|
2332
|
-
usages.push({
|
|
2333
|
-
componentName: match[1],
|
|
2334
|
-
controller,
|
|
2335
|
-
action,
|
|
2336
|
-
propsVar: propsMatch ? propsMatch[1] : void 0,
|
|
2337
|
-
line: lineNum,
|
|
2338
|
-
pattern: "data-react-component",
|
|
2339
|
-
ssr: false
|
|
2340
|
-
});
|
|
2341
|
-
}
|
|
2342
|
-
const renderMatch = line.match(VIEW_PATTERNS.renderReactComponent);
|
|
2343
|
-
if (renderMatch) {
|
|
2344
|
-
const ssrMatch = line.match(/ssr:\s*(true|false|@\w+)/);
|
|
2345
|
-
usages.push({
|
|
2346
|
-
componentName: renderMatch[1],
|
|
2347
|
-
controller,
|
|
2348
|
-
action,
|
|
2349
|
-
line: lineNum,
|
|
2350
|
-
pattern: "render_react_component",
|
|
2351
|
-
ssr: ssrMatch ? ssrMatch[1] === "true" || ssrMatch[1].startsWith("@") : false
|
|
2352
|
-
});
|
|
2353
|
-
}
|
|
2354
|
-
const reactOnRailsMatch = line.match(VIEW_PATTERNS.reactComponent);
|
|
2355
|
-
if (reactOnRailsMatch) {
|
|
2356
|
-
const propsMatch = line.match(/props:\s*(@?\w+(?:\.\w+)*)/);
|
|
2357
|
-
const prerenderMatch = line.match(/prerender:\s*(true|false)/);
|
|
2358
|
-
usages.push({
|
|
2359
|
-
componentName: reactOnRailsMatch[1],
|
|
2360
|
-
controller,
|
|
2361
|
-
action,
|
|
2362
|
-
propsVar: propsMatch ? propsMatch[1] : void 0,
|
|
2363
|
-
line: lineNum,
|
|
2364
|
-
pattern: "react_component",
|
|
2365
|
-
ssr: prerenderMatch ? prerenderMatch[1] === "true" : false
|
|
2366
|
-
});
|
|
2367
|
-
}
|
|
2368
|
-
const reduxMatch = line.match(VIEW_PATTERNS.reduxStore);
|
|
2369
|
-
if (reduxMatch) {
|
|
2370
|
-
usages.push({
|
|
2371
|
-
componentName: reduxMatch[1],
|
|
2372
|
-
controller,
|
|
2373
|
-
action,
|
|
2374
|
-
line: lineNum,
|
|
2375
|
-
pattern: "redux_store",
|
|
2376
|
-
ssr: false
|
|
2377
|
-
});
|
|
2378
|
-
}
|
|
2379
|
-
const dataCompMatch = line.match(VIEW_PATTERNS.dataComponent);
|
|
2380
|
-
if (dataCompMatch) {
|
|
2381
|
-
usages.push({
|
|
2382
|
-
componentName: dataCompMatch[1],
|
|
2383
|
-
controller,
|
|
2384
|
-
action,
|
|
2385
|
-
line: lineNum,
|
|
2386
|
-
pattern: "data-react-component",
|
|
2387
|
-
ssr: false
|
|
2388
|
-
});
|
|
2389
|
-
}
|
|
2390
|
-
const reactClassMatch = line.match(VIEW_PATTERNS.dataReactClass);
|
|
2391
|
-
if (reactClassMatch) {
|
|
2392
|
-
usages.push({
|
|
2393
|
-
componentName: reactClassMatch[1],
|
|
2394
|
-
controller,
|
|
2395
|
-
action,
|
|
2396
|
-
line: lineNum,
|
|
2397
|
-
pattern: "data-react-component",
|
|
2398
|
-
ssr: false
|
|
2399
|
-
});
|
|
2400
|
-
}
|
|
2401
|
-
}
|
|
2402
|
-
} catch {
|
|
2403
|
-
}
|
|
2404
|
-
return usages;
|
|
2405
|
-
}
|
|
2406
|
-
async function resolveSourceFiles(rootPath, components, componentDirs) {
|
|
2407
|
-
for (const [name, component] of components) {
|
|
2408
|
-
if (!name || typeof name !== "string") continue;
|
|
2409
|
-
if (component.importPath && typeof component.importPath === "string") {
|
|
2410
|
-
const cleanPath = component.importPath.replace(/\.js$/, "").replace(/\.tsx?$/, "").replace(/^\.\.\//, "").replace(/^\.\//, "");
|
|
2411
|
-
component.sourceFile = cleanPath;
|
|
2412
|
-
} else if (!component.sourceFile) {
|
|
2413
|
-
const extensions = [".tsx", ".ts", ".jsx", ".js"];
|
|
2414
|
-
const namingPatterns = [
|
|
2415
|
-
name,
|
|
2416
|
-
// PascalCase
|
|
2417
|
-
toSnakeCase(name),
|
|
2418
|
-
// snake_case
|
|
2419
|
-
toKebabCase(name)
|
|
2420
|
-
// kebab-case
|
|
2421
|
-
].filter(Boolean);
|
|
2422
|
-
let found = false;
|
|
2423
|
-
for (const dir of componentDirs) {
|
|
2424
|
-
if (found) break;
|
|
2425
|
-
for (const naming of namingPatterns) {
|
|
2426
|
-
if (found) break;
|
|
2427
|
-
for (const ext of extensions) {
|
|
2428
|
-
const possiblePaths = [
|
|
2429
|
-
path7.join(rootPath, dir, naming, `index${ext}`),
|
|
2430
|
-
path7.join(rootPath, dir, naming, `${naming}${ext}`),
|
|
2431
|
-
path7.join(rootPath, dir, `${naming}${ext}`),
|
|
2432
|
-
path7.join(rootPath, dir, "components", `${naming}${ext}`),
|
|
2433
|
-
path7.join(rootPath, dir, "containers", `${naming}${ext}`)
|
|
2434
|
-
];
|
|
2435
|
-
for (const possiblePath of possiblePaths) {
|
|
2436
|
-
try {
|
|
2437
|
-
await fs7.access(possiblePath);
|
|
2438
|
-
component.sourceFile = path7.relative(rootPath, possiblePath);
|
|
2439
|
-
found = true;
|
|
2440
|
-
break;
|
|
2441
|
-
} catch {
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
}
|
|
2445
|
-
}
|
|
2446
|
-
}
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
function toSnakeCase(str) {
|
|
2451
|
-
if (!str) return "";
|
|
2452
|
-
return str.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "");
|
|
2453
|
-
}
|
|
2454
|
-
function toKebabCase(str) {
|
|
2455
|
-
if (!str) return "";
|
|
2456
|
-
return str.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
|
-
// src/analyzers/rails/index.ts
|
|
2460
|
-
async function analyzeRailsApp(rootPath) {
|
|
2461
|
-
console.log(`
|
|
2462
|
-
\u{1F4E6} Analyzing Rails application at: ${rootPath}
|
|
2463
|
-
`);
|
|
2464
|
-
console.log("\u{1F504} Analyzing routes...");
|
|
2465
|
-
const routesAnalyzer = new RailsRoutesAnalyzer(rootPath);
|
|
2466
|
-
const routes = await routesAnalyzer.analyze();
|
|
2467
|
-
console.log(` \u2705 Found ${routes.routes.length} routes`);
|
|
2468
|
-
console.log("\u{1F504} Analyzing controllers...");
|
|
2469
|
-
const controllersAnalyzer = new RailsControllerAnalyzer(rootPath);
|
|
2470
|
-
const controllers = await controllersAnalyzer.analyze();
|
|
2471
|
-
console.log(
|
|
2472
|
-
` \u2705 Found ${controllers.controllers.length} controllers with ${controllers.totalActions} actions`
|
|
2473
|
-
);
|
|
2474
|
-
console.log("\u{1F504} Analyzing models...");
|
|
2475
|
-
const modelsAnalyzer = new RailsModelAnalyzer(rootPath);
|
|
2476
|
-
const models = await modelsAnalyzer.analyze();
|
|
2477
|
-
console.log(
|
|
2478
|
-
` \u2705 Found ${models.models.length} models with ${models.totalAssociations} associations`
|
|
2479
|
-
);
|
|
2480
|
-
console.log("\u{1F504} Analyzing gRPC services...");
|
|
2481
|
-
const grpcAnalyzer = new RailsGrpcAnalyzer(rootPath);
|
|
2482
|
-
const grpc = await grpcAnalyzer.analyze();
|
|
2483
|
-
console.log(` \u2705 Found ${grpc.services.length} gRPC services with ${grpc.totalRpcs} RPCs`);
|
|
2484
|
-
console.log("\u{1F504} Analyzing views...");
|
|
2485
|
-
const views = await analyzeRailsViews(rootPath);
|
|
2486
|
-
console.log(
|
|
2487
|
-
` \u2705 Found ${views.summary.totalViews} views and ${views.summary.totalPages} pages`
|
|
2488
|
-
);
|
|
2489
|
-
console.log("\u{1F504} Analyzing React components...");
|
|
2490
|
-
const react = await analyzeReactComponents(rootPath);
|
|
2491
|
-
console.log(
|
|
2492
|
-
` \u2705 Found ${react.summary.totalComponents} React components (${react.summary.ssrComponents} SSR, ${react.summary.clientComponents} client)`
|
|
2493
|
-
);
|
|
2494
|
-
const allNamespaces = [
|
|
2495
|
-
.../* @__PURE__ */ new Set([
|
|
2496
|
-
...routes.namespaces,
|
|
2497
|
-
...controllers.namespaces,
|
|
2498
|
-
...models.namespaces,
|
|
2499
|
-
...grpc.namespaces
|
|
2500
|
-
])
|
|
2501
|
-
];
|
|
2502
|
-
const allConcerns = [.../* @__PURE__ */ new Set([...controllers.concerns, ...models.concerns])];
|
|
2503
|
-
const summary = {
|
|
2504
|
-
totalRoutes: routes.routes.length,
|
|
2505
|
-
totalControllers: controllers.controllers.length,
|
|
2506
|
-
totalActions: controllers.totalActions,
|
|
2507
|
-
totalModels: models.models.length,
|
|
2508
|
-
totalAssociations: models.totalAssociations,
|
|
2509
|
-
totalValidations: models.totalValidations,
|
|
2510
|
-
totalGrpcServices: grpc.services.length,
|
|
2511
|
-
totalRpcs: grpc.totalRpcs,
|
|
2512
|
-
totalViews: views.summary.totalViews,
|
|
2513
|
-
totalPages: views.summary.totalPages,
|
|
2514
|
-
totalReactComponents: react.summary.totalComponents,
|
|
2515
|
-
ssrReactComponents: react.summary.ssrComponents,
|
|
2516
|
-
namespaces: allNamespaces,
|
|
2517
|
-
concerns: allConcerns
|
|
2518
|
-
};
|
|
2519
|
-
return {
|
|
2520
|
-
routes,
|
|
2521
|
-
controllers,
|
|
2522
|
-
models,
|
|
2523
|
-
grpc,
|
|
2524
|
-
views,
|
|
2525
|
-
react,
|
|
2526
|
-
summary
|
|
2527
|
-
};
|
|
2528
|
-
}
|
|
2529
|
-
async function main5() {
|
|
2530
|
-
const targetPath = process.argv[2] || process.cwd();
|
|
2531
|
-
const result = await analyzeRailsApp(targetPath);
|
|
2532
|
-
console.log("\n" + "=".repeat(60));
|
|
2533
|
-
console.log("\u{1F4CA} RAILS APPLICATION ANALYSIS SUMMARY");
|
|
2534
|
-
console.log("=".repeat(60) + "\n");
|
|
2535
|
-
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
2536
|
-
console.log("\u2502 Routes \u2502");
|
|
2537
|
-
console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
2538
|
-
console.log(
|
|
2539
|
-
`\u2502 Total routes: ${String(result.summary.totalRoutes).padStart(6)} \u2502`
|
|
2540
|
-
);
|
|
2541
|
-
console.log(
|
|
2542
|
-
`\u2502 Resources: ${String(result.routes.resources.length).padStart(6)} \u2502`
|
|
2543
|
-
);
|
|
2544
|
-
console.log(
|
|
2545
|
-
`\u2502 Mounted engines: ${String(result.routes.mountedEngines.length).padStart(6)} \u2502`
|
|
2546
|
-
);
|
|
2547
|
-
console.log(
|
|
2548
|
-
`\u2502 External files: ${String(result.routes.drawnFiles.length).padStart(6)} \u2502`
|
|
2549
|
-
);
|
|
2550
|
-
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
2551
|
-
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
2552
|
-
console.log("\u2502 Controllers \u2502");
|
|
2553
|
-
console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
2554
|
-
console.log(
|
|
2555
|
-
`\u2502 Total controllers: ${String(result.summary.totalControllers).padStart(6)} \u2502`
|
|
2556
|
-
);
|
|
2557
|
-
console.log(
|
|
2558
|
-
`\u2502 Total actions: ${String(result.summary.totalActions).padStart(6)} \u2502`
|
|
2559
|
-
);
|
|
2560
|
-
console.log(
|
|
2561
|
-
`\u2502 Namespaces: ${String(result.controllers.namespaces.length).padStart(6)} \u2502`
|
|
2562
|
-
);
|
|
2563
|
-
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
2564
|
-
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
2565
|
-
console.log("\u2502 Models \u2502");
|
|
2566
|
-
console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
2567
|
-
console.log(
|
|
2568
|
-
`\u2502 Total models: ${String(result.summary.totalModels).padStart(6)} \u2502`
|
|
2569
|
-
);
|
|
2570
|
-
console.log(
|
|
2571
|
-
`\u2502 Associations: ${String(result.summary.totalAssociations).padStart(6)} \u2502`
|
|
2572
|
-
);
|
|
2573
|
-
console.log(
|
|
2574
|
-
`\u2502 Validations: ${String(result.summary.totalValidations).padStart(6)} \u2502`
|
|
2575
|
-
);
|
|
2576
|
-
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
2577
|
-
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
2578
|
-
console.log("\u2502 gRPC Services \u2502");
|
|
2579
|
-
console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
2580
|
-
console.log(
|
|
2581
|
-
`\u2502 Total services: ${String(result.summary.totalGrpcServices).padStart(6)} \u2502`
|
|
2582
|
-
);
|
|
2583
|
-
console.log(
|
|
2584
|
-
`\u2502 Total RPCs: ${String(result.summary.totalRpcs).padStart(6)} \u2502`
|
|
2585
|
-
);
|
|
2586
|
-
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
2587
|
-
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
2588
|
-
console.log("\u2502 Shared \u2502");
|
|
2589
|
-
console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
|
|
2590
|
-
console.log(
|
|
2591
|
-
`\u2502 Total namespaces: ${String(result.summary.namespaces.length).padStart(6)} \u2502`
|
|
2592
|
-
);
|
|
2593
|
-
console.log(
|
|
2594
|
-
`\u2502 Total concerns: ${String(result.summary.concerns.length).padStart(6)} \u2502`
|
|
2595
|
-
);
|
|
2596
|
-
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
2597
|
-
const totalErrors = result.routes.errors.length + result.controllers.errors.length + result.models.errors.length + result.grpc.errors.length;
|
|
2598
|
-
if (totalErrors > 0) {
|
|
2599
|
-
console.log(`
|
|
2600
|
-
\u26A0\uFE0F Total errors: ${totalErrors}`);
|
|
2601
|
-
} else {
|
|
2602
|
-
console.log("\n\u2705 Analysis completed without errors!");
|
|
2603
|
-
}
|
|
2604
|
-
}
|
|
2605
|
-
var isMainModule5 = import.meta.url === `file://${process.argv[1]}`;
|
|
2606
|
-
if (isMainModule5) {
|
|
2607
|
-
main5().catch(console.error);
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
export { RailsControllerAnalyzer, RailsGrpcAnalyzer, RailsModelAnalyzer, RailsRoutesAnalyzer, analyzeRailsApp, analyzeRailsViews, analyzeReactComponents, findNodes, initRubyParser, parseRuby, parseRubyFile };
|