@lakindu_perera/toren 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +354 -0
- package/bin/toren.js +203 -0
- package/package.json +39 -0
- package/src/lifecycle.js +124 -0
- package/src/renderers/console-renderer.js +218 -0
- package/src/renderers/html-renderer.js +913 -0
- package/src/renderers/index.js +41 -0
- package/src/renderers/json-renderer.js +54 -0
- package/src/renderers/markdown-renderer.js +328 -0
- package/src/renderers/tree-renderer.js +67 -0
- package/src/scanner/scan.js +341 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Toren CLI — Codebase Scanner (Core Logic)
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Recursively walk a project directory
|
|
6
|
+
* - Ignore irrelevant paths (node_modules, .git, dist, build, etc.)
|
|
7
|
+
* - Build an in-memory file-tree representation
|
|
8
|
+
* - Detect the project type from marker files
|
|
9
|
+
* - Identify known entry-point files
|
|
10
|
+
*
|
|
11
|
+
* This module is intentionally free of side-effects (no console.log).
|
|
12
|
+
* All output concerns live in bin/toren.js.
|
|
13
|
+
*
|
|
14
|
+
* Designed to scale into:
|
|
15
|
+
* - Module / dependency graph analysis
|
|
16
|
+
* - Architecture visualisation layers
|
|
17
|
+
* - AI explanation integrations
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Constants
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Directory / file names that are never walked.
|
|
29
|
+
* Stored as a Set for O(1) membership checks.
|
|
30
|
+
* @type {Set<string>}
|
|
31
|
+
*/
|
|
32
|
+
const IGNORED_DIRS = new Set([
|
|
33
|
+
'node_modules',
|
|
34
|
+
'.git',
|
|
35
|
+
'dist',
|
|
36
|
+
'build',
|
|
37
|
+
'.cache',
|
|
38
|
+
'.next',
|
|
39
|
+
'.nuxt',
|
|
40
|
+
'out',
|
|
41
|
+
'coverage',
|
|
42
|
+
'__pycache__',
|
|
43
|
+
'.venv',
|
|
44
|
+
'venv',
|
|
45
|
+
'.idea',
|
|
46
|
+
'.vscode',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Mapping from a marker filename to a human-readable project-type label.
|
|
51
|
+
* Evaluated in iteration order — more specific entries should come first.
|
|
52
|
+
* @type {Array<{ marker: string, label: string }>}
|
|
53
|
+
*/
|
|
54
|
+
const PROJECT_TYPE_MARKERS = [
|
|
55
|
+
{ marker: 'package.json', label: 'Node.js / JavaScript' },
|
|
56
|
+
{ marker: 'pom.xml', label: 'Java / Spring Boot' },
|
|
57
|
+
{ marker: 'build.gradle', label: 'Java / Gradle' },
|
|
58
|
+
{ marker: 'requirements.txt', label: 'Python' },
|
|
59
|
+
{ marker: 'Pipfile', label: 'Python (Pipenv)' },
|
|
60
|
+
{ marker: 'pyproject.toml', label: 'Python (pyproject)' },
|
|
61
|
+
{ marker: 'go.mod', label: 'Go' },
|
|
62
|
+
{ marker: 'Cargo.toml', label: 'Rust' },
|
|
63
|
+
{ marker: 'composer.json', label: 'PHP / Composer' },
|
|
64
|
+
{ marker: 'Gemfile', label: 'Ruby' },
|
|
65
|
+
{ marker: 'mix.exs', label: 'Elixir' },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const ENTRY_POINT_EXACT = new Set([
|
|
69
|
+
'index.js', 'index.ts', 'index.jsx', 'index.tsx',
|
|
70
|
+
'main.js', 'main.ts', 'main.jsx', 'main.tsx', 'main.py',
|
|
71
|
+
'app.js', 'app.ts', 'app.jsx', 'app.tsx', 'App.js', 'App.ts', 'App.jsx', 'App.tsx',
|
|
72
|
+
'server.js', 'server.ts', 'server.jsx', 'server.tsx',
|
|
73
|
+
'Application.java', 'Main.java',
|
|
74
|
+
'page.js', 'page.ts', 'page.jsx', 'page.tsx',
|
|
75
|
+
'layout.js', 'layout.ts', 'layout.jsx', 'layout.tsx',
|
|
76
|
+
'_app.js', '_app.ts', '_app.jsx', '_app.tsx',
|
|
77
|
+
'_document.js', '_document.ts', '_document.jsx', '_document.tsx'
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
function isEntryPoint(filename) {
|
|
81
|
+
if (ENTRY_POINT_EXACT.has(filename)) return true;
|
|
82
|
+
if (filename.endsWith('Application.java')) return true;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Types (JSDoc — no TypeScript dependency required)
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @typedef {Object} FileNode
|
|
92
|
+
* @property {'file'} type
|
|
93
|
+
* @property {string} name - Basename of the file
|
|
94
|
+
* @property {string} fullPath - Absolute path
|
|
95
|
+
* @property {string} relPath - Path relative to the scanned root
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @typedef {Object} DirNode
|
|
100
|
+
* @property {'directory'} type
|
|
101
|
+
* @property {string} name - Basename of the directory
|
|
102
|
+
* @property {string} fullPath - Absolute path
|
|
103
|
+
* @property {string} relPath - Path relative to the scanned root
|
|
104
|
+
* @property {Array<FileNode|DirNode>} children
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @typedef {Object} ScanResult
|
|
109
|
+
* @property {string} rootPath - Absolute path that was scanned
|
|
110
|
+
* @property {string} projectType - Detected project type label
|
|
111
|
+
* @property {Array<string>} entryPoints - Relative paths of detected entry points
|
|
112
|
+
* @property {DirNode} tree - Full in-memory file tree
|
|
113
|
+
* @property {Array<string>} flatFiles - All relative file paths (flat list)
|
|
114
|
+
* @property {number} totalFolders - Total number of directories walked
|
|
115
|
+
* @property {number} scanDurationMs - Wall-clock time of the scan in milliseconds
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Internal helpers
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Determine whether a directory entry should be skipped.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} name - Basename of the entry
|
|
126
|
+
* @param {fs.Dirent} dirent
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
*/
|
|
129
|
+
function shouldIgnore(name, dirent) {
|
|
130
|
+
if (name.startsWith('.') && dirent.isDirectory()) return true;
|
|
131
|
+
return IGNORED_DIRS.has(name);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Recursively walk `dirPath`, building a DirNode tree.
|
|
136
|
+
* Also populates `flatFiles` and `entryPoints` arrays by reference.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} dirPath - Absolute path of the current directory
|
|
139
|
+
* @param {string} rootPath - Absolute path of the scan root (for relative paths)
|
|
140
|
+
* @param {Array<string>} flatFiles - Accumulator for all relative file paths
|
|
141
|
+
* @param {Array<string>} entryPoints - Accumulator for entry-point relative paths
|
|
142
|
+
* @returns {DirNode}
|
|
143
|
+
*/
|
|
144
|
+
function walkDirectory(dirPath, rootPath, flatFiles, entryPoints) {
|
|
145
|
+
const name = path.basename(dirPath);
|
|
146
|
+
const relPath = path.relative(rootPath, dirPath) || '.';
|
|
147
|
+
|
|
148
|
+
/** @type {DirNode} */
|
|
149
|
+
const node = {
|
|
150
|
+
type: 'directory',
|
|
151
|
+
name,
|
|
152
|
+
fullPath: dirPath,
|
|
153
|
+
relPath,
|
|
154
|
+
children: [],
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let entries;
|
|
158
|
+
try {
|
|
159
|
+
entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
160
|
+
} catch {
|
|
161
|
+
// Permission-denied or unreadable directory — skip silently.
|
|
162
|
+
return node;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Sort: directories first, then files — both alphabetically.
|
|
166
|
+
entries.sort((a, b) => {
|
|
167
|
+
const aIsDir = a.isDirectory() ? 0 : 1;
|
|
168
|
+
const bIsDir = b.isDirectory() ? 0 : 1;
|
|
169
|
+
if (aIsDir !== bIsDir) return aIsDir - bIsDir;
|
|
170
|
+
return a.name.localeCompare(b.name);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
for (const dirent of entries) {
|
|
174
|
+
if (shouldIgnore(dirent.name, dirent)) continue;
|
|
175
|
+
|
|
176
|
+
const childPath = path.join(dirPath, dirent.name);
|
|
177
|
+
|
|
178
|
+
if (dirent.isDirectory()) {
|
|
179
|
+
const childNode = walkDirectory(childPath, rootPath, flatFiles, entryPoints);
|
|
180
|
+
node.children.push(childNode);
|
|
181
|
+
} else if (dirent.isFile()) {
|
|
182
|
+
const relFilePath = path.relative(rootPath, childPath);
|
|
183
|
+
|
|
184
|
+
/** @type {FileNode} */
|
|
185
|
+
const fileNode = {
|
|
186
|
+
type: 'file',
|
|
187
|
+
name: dirent.name,
|
|
188
|
+
fullPath: childPath,
|
|
189
|
+
relPath: relFilePath,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
node.children.push(fileNode);
|
|
193
|
+
flatFiles.push(relFilePath);
|
|
194
|
+
|
|
195
|
+
if (isEntryPoint(dirent.name)) {
|
|
196
|
+
entryPoints.push(relFilePath);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return node;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Detect the project type by checking for known marker files in `rootPath`.
|
|
206
|
+
*
|
|
207
|
+
* Returns the label of the first matched marker, or `'Unknown'` if none match.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} rootPath - Absolute path to the project root
|
|
210
|
+
* @returns {string}
|
|
211
|
+
*/
|
|
212
|
+
function detectProjectType(rootPath) {
|
|
213
|
+
for (const { marker, label } of PROJECT_TYPE_MARKERS) {
|
|
214
|
+
const markerPath = path.join(rootPath, marker);
|
|
215
|
+
if (fs.existsSync(markerPath)) {
|
|
216
|
+
// Refine Node.js projects by inspecting package.json dependencies.
|
|
217
|
+
if (marker === 'package.json') {
|
|
218
|
+
return refineNodeProjectType(markerPath);
|
|
219
|
+
}
|
|
220
|
+
return label;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return 'Unknown';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Read `package.json` and return a more specific label when React / Next / Vue
|
|
228
|
+
* etc. are listed as dependencies.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} pkgPath - Absolute path to package.json
|
|
231
|
+
* @returns {string}
|
|
232
|
+
*/
|
|
233
|
+
function refineNodeProjectType(pkgPath) {
|
|
234
|
+
try {
|
|
235
|
+
const raw = fs.readFileSync(pkgPath, 'utf8');
|
|
236
|
+
const pkg = JSON.parse(raw);
|
|
237
|
+
const deps = {
|
|
238
|
+
...pkg.dependencies,
|
|
239
|
+
...pkg.devDependencies,
|
|
240
|
+
...pkg.peerDependencies,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
if (deps['next']) return 'Next.js';
|
|
244
|
+
if (deps['react']) return 'React';
|
|
245
|
+
if (deps['vue']) return 'Vue.js';
|
|
246
|
+
if (deps['@angular/core']) return 'Angular';
|
|
247
|
+
if (deps['svelte']) return 'Svelte';
|
|
248
|
+
if (deps['express']) return 'Node.js / Express';
|
|
249
|
+
if (deps['fastify']) return 'Node.js / Fastify';
|
|
250
|
+
if (deps['koa']) return 'Node.js / Koa';
|
|
251
|
+
if (deps['typescript']) return 'Node.js / TypeScript';
|
|
252
|
+
} catch {
|
|
253
|
+
// Malformed package.json — fall through.
|
|
254
|
+
}
|
|
255
|
+
return 'Node.js / JavaScript';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Tree helpers
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Count the total number of directory nodes in a tree (including the root).
|
|
264
|
+
*
|
|
265
|
+
* @param {DirNode} node
|
|
266
|
+
* @returns {number}
|
|
267
|
+
*/
|
|
268
|
+
function countFolders(node) {
|
|
269
|
+
let count = 1; // count this directory
|
|
270
|
+
for (const child of node.children ?? []) {
|
|
271
|
+
if (child.type === 'directory') {
|
|
272
|
+
count += countFolders(child);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return count;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// Public API
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
export function scan(targetPath) {
|
|
283
|
+
const rootPath = path.resolve(targetPath);
|
|
284
|
+
|
|
285
|
+
// Validate target
|
|
286
|
+
let stat;
|
|
287
|
+
try {
|
|
288
|
+
stat = fs.statSync(rootPath);
|
|
289
|
+
} catch {
|
|
290
|
+
throw new Error(`Path does not exist: ${rootPath}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** @type {Array<string>} */
|
|
294
|
+
const flatFiles = [];
|
|
295
|
+
/** @type {Array<string>} */
|
|
296
|
+
const entryPoints = [];
|
|
297
|
+
|
|
298
|
+
const startTime = performance.now();
|
|
299
|
+
let tree;
|
|
300
|
+
let projectType = 'Unknown';
|
|
301
|
+
let totalFolders = 0;
|
|
302
|
+
|
|
303
|
+
if (stat.isDirectory()) {
|
|
304
|
+
tree = walkDirectory(rootPath, rootPath, flatFiles, entryPoints);
|
|
305
|
+
projectType = detectProjectType(rootPath);
|
|
306
|
+
// Count all directory nodes in the tree (excluding root itself).
|
|
307
|
+
totalFolders = countFolders(tree) - 1;
|
|
308
|
+
} else if (stat.isFile()) {
|
|
309
|
+
const relFilePath = path.basename(rootPath);
|
|
310
|
+
tree = {
|
|
311
|
+
type: 'directory',
|
|
312
|
+
name: path.basename(path.dirname(rootPath)),
|
|
313
|
+
fullPath: path.dirname(rootPath),
|
|
314
|
+
relPath: '.',
|
|
315
|
+
children: [{
|
|
316
|
+
type: 'file',
|
|
317
|
+
name: relFilePath,
|
|
318
|
+
fullPath: rootPath,
|
|
319
|
+
relPath: relFilePath
|
|
320
|
+
}]
|
|
321
|
+
};
|
|
322
|
+
flatFiles.push(relFilePath);
|
|
323
|
+
if (isEntryPoint(relFilePath)) {
|
|
324
|
+
entryPoints.push(relFilePath);
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
throw new Error(`Path is neither a file nor a directory: ${rootPath}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const scanDurationMs = performance.now() - startTime;
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
rootPath,
|
|
334
|
+
projectType,
|
|
335
|
+
entryPoints,
|
|
336
|
+
tree,
|
|
337
|
+
flatFiles,
|
|
338
|
+
totalFolders,
|
|
339
|
+
scanDurationMs,
|
|
340
|
+
};
|
|
341
|
+
}
|