ruvector 0.2.19 → 0.2.20
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/bin/cli.js +139 -0
- package/bin/mcp-server.js +269 -0
- package/package.json +14 -6
- package/src/decompiler/index.js +407 -0
- package/src/decompiler/metrics.js +86 -0
- package/src/decompiler/module-splitter.js +498 -0
- package/src/decompiler/module-tree.js +142 -0
- package/src/decompiler/name-predictor.js +400 -0
- package/src/decompiler/npm-fetch.js +176 -0
- package/src/decompiler/reconstructor.js +499 -0
- package/src/decompiler/reference-tracker.js +285 -0
- package/src/decompiler/statement-parser.js +285 -0
- package/src/decompiler/style-improver.js +438 -0
- package/src/decompiler/subcategories.js +339 -0
- package/src/decompiler/validator.js +379 -0
- package/src/decompiler/witness.js +140 -0
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decompiler/index.js - High-level decompiler API.
|
|
3
|
+
*
|
|
4
|
+
* Exports three main entry points:
|
|
5
|
+
* - decompilePackage(name, version, options)
|
|
6
|
+
* - decompileFile(filePath, options)
|
|
7
|
+
* - decompileUrl(url, options)
|
|
8
|
+
*
|
|
9
|
+
* Each returns a standardized DecompileResult:
|
|
10
|
+
* { modules, metrics, witness, source, packageInfo? }
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const {
|
|
18
|
+
fetchPackageInfo,
|
|
19
|
+
fetchPackageFileList,
|
|
20
|
+
fetchFileContent,
|
|
21
|
+
findMainBundle,
|
|
22
|
+
parseTarget,
|
|
23
|
+
} = require('./npm-fetch');
|
|
24
|
+
const { splitModules } = require('./module-splitter');
|
|
25
|
+
const { buildWitnessChain, verifyWitnessChain } = require('./witness');
|
|
26
|
+
const { computeMetrics, computeModuleMetrics } = require('./metrics');
|
|
27
|
+
const { reconstructCode, reconstructRunnable } = require('./reconstructor');
|
|
28
|
+
const { validateReconstruction } = require('./validator');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Try to beautify source code using js-beautify (optional dep).
|
|
32
|
+
* Falls back to returning the source unchanged if not installed.
|
|
33
|
+
* @param {string} source
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function beautify(source) {
|
|
37
|
+
try {
|
|
38
|
+
const jsBeautify = require('js-beautify');
|
|
39
|
+
const beautifyFn = jsBeautify.js || jsBeautify;
|
|
40
|
+
return beautifyFn(source, {
|
|
41
|
+
indent_size: 2,
|
|
42
|
+
space_in_empty_paren: false,
|
|
43
|
+
preserve_newlines: true,
|
|
44
|
+
max_preserve_newlines: 2,
|
|
45
|
+
end_with_newline: true,
|
|
46
|
+
});
|
|
47
|
+
} catch {
|
|
48
|
+
// js-beautify not installed; return source as-is
|
|
49
|
+
return source;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Try to use the Rust decompiler for full Louvain graph partitioning (878+ modules).
|
|
55
|
+
* Falls back to Node.js keyword splitting if Rust binary not available.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} filePath - path to JS file
|
|
58
|
+
* @param {string} outputDir - output directory
|
|
59
|
+
* @returns {{success: boolean, modules: number, outputDir: string}|null}
|
|
60
|
+
*/
|
|
61
|
+
function tryRustDecompiler(filePath, outputDir) {
|
|
62
|
+
try {
|
|
63
|
+
const { execSync } = require('child_process');
|
|
64
|
+
// Try to find the Rust binary
|
|
65
|
+
const candidates = [
|
|
66
|
+
'cargo run --release -p ruvector-decompiler --example run_on_cli --',
|
|
67
|
+
path.join(__dirname, '../../../../target/release/examples/run_on_cli'),
|
|
68
|
+
];
|
|
69
|
+
for (const bin of candidates) {
|
|
70
|
+
try {
|
|
71
|
+
const cmd = bin.includes('cargo')
|
|
72
|
+
? `${bin} "${filePath}" --output-dir "${outputDir}"`
|
|
73
|
+
: `"${bin}" "${filePath}" --output-dir "${outputDir}"`;
|
|
74
|
+
const result = execSync(cmd, {
|
|
75
|
+
timeout: 120000,
|
|
76
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
77
|
+
cwd: path.join(__dirname, '../../../..'),
|
|
78
|
+
});
|
|
79
|
+
const stderr = result.toString();
|
|
80
|
+
const match = stderr.match(/Wrote (\d+) modules/);
|
|
81
|
+
const moduleCount = match ? parseInt(match[1]) : 0;
|
|
82
|
+
return { success: true, modules: moduleCount, outputDir };
|
|
83
|
+
} catch { continue; }
|
|
84
|
+
}
|
|
85
|
+
} catch {}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Core decompilation pipeline: beautify -> split -> metrics -> witness -> reconstruct.
|
|
91
|
+
*
|
|
92
|
+
* If the Rust decompiler is available (cargo built), uses Louvain graph partitioning
|
|
93
|
+
* for 878+ modules with 100% parse rate. Falls back to Node.js keyword splitting.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} source - raw JavaScript source
|
|
96
|
+
* @param {object} [options]
|
|
97
|
+
* @param {number} [options.minConfidence=0.3]
|
|
98
|
+
* @param {boolean} [options.witness=true]
|
|
99
|
+
* @param {boolean} [options.reconstruct=false] - apply readable reconstruction
|
|
100
|
+
* @param {boolean} [options.validate=false] - validate reconstruction preserves semantics
|
|
101
|
+
* @param {string} [options.patternPath] - path to training patterns JSON
|
|
102
|
+
* @param {boolean} [options.addComments=true] - add JSDoc comments during reconstruction
|
|
103
|
+
* @param {boolean} [options.improveStyle=true] - apply style improvements during reconstruction
|
|
104
|
+
* @param {boolean} [options.useRust=true] - try Rust Louvain partitioner first
|
|
105
|
+
* @param {string} [options.filePath] - original file path (needed for Rust pipeline)
|
|
106
|
+
* @returns {{modules: object[], metrics: object, witness: object|null, beautifiedSource: string, reconstruction?: object}}
|
|
107
|
+
*/
|
|
108
|
+
function decompileSource(source, options = {}) {
|
|
109
|
+
const {
|
|
110
|
+
minConfidence = 0.3,
|
|
111
|
+
witness: generateWitness = true,
|
|
112
|
+
reconstruct = false,
|
|
113
|
+
validate = false,
|
|
114
|
+
patternPath,
|
|
115
|
+
addComments = true,
|
|
116
|
+
improveStyle = true,
|
|
117
|
+
useRust = true,
|
|
118
|
+
filePath,
|
|
119
|
+
} = options;
|
|
120
|
+
|
|
121
|
+
// Try Rust Louvain pipeline first (878+ modules, 100% parse rate)
|
|
122
|
+
if (useRust && filePath && source.length > 100000) {
|
|
123
|
+
const tmpDir = path.join(require('os').tmpdir(), 'ruvector-decompile-' + Date.now());
|
|
124
|
+
const rustResult = tryRustDecompiler(filePath, tmpDir);
|
|
125
|
+
if (rustResult && rustResult.success) {
|
|
126
|
+
// Load modules from Rust output
|
|
127
|
+
const sourceDir = path.join(tmpDir, 'source');
|
|
128
|
+
const rustModules = [];
|
|
129
|
+
try {
|
|
130
|
+
for (const f of fs.readdirSync(sourceDir).filter(f => f.endsWith('.js'))) {
|
|
131
|
+
const content = fs.readFileSync(path.join(sourceDir, f), 'utf8');
|
|
132
|
+
rustModules.push({
|
|
133
|
+
name: f.replace('.js', ''),
|
|
134
|
+
content,
|
|
135
|
+
fragments: 0,
|
|
136
|
+
confidence: 0.8,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
if (rustModules.length > 0) {
|
|
141
|
+
const sourceMetrics = computeMetrics(source);
|
|
142
|
+
const witnessPath = path.join(tmpDir, 'witness.json');
|
|
143
|
+
let witnessChain = null;
|
|
144
|
+
try { witnessChain = JSON.parse(fs.readFileSync(witnessPath, 'utf8')); } catch {}
|
|
145
|
+
return {
|
|
146
|
+
modules: rustModules,
|
|
147
|
+
metrics: { source: sourceMetrics, modules: rustModules.length, engine: 'rust-louvain' },
|
|
148
|
+
witness: witnessChain,
|
|
149
|
+
beautifiedSource: source,
|
|
150
|
+
source,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Fallback: Node.js keyword-based splitting
|
|
157
|
+
const beautified = beautify(source);
|
|
158
|
+
const { modules, unclassified } = splitModules(beautified, { minConfidence });
|
|
159
|
+
const sourceMetrics = computeMetrics(beautified);
|
|
160
|
+
const moduleMetrics = computeModuleMetrics(modules);
|
|
161
|
+
const witnessChain = generateWitness ? buildWitnessChain(source, modules) : null;
|
|
162
|
+
|
|
163
|
+
// Optional: apply readable reconstruction to each module
|
|
164
|
+
let reconstructionSummary = null;
|
|
165
|
+
if (reconstruct) {
|
|
166
|
+
let totalRenames = 0;
|
|
167
|
+
let totalComments = 0;
|
|
168
|
+
let totalConfidence = 0;
|
|
169
|
+
let validationResults = [];
|
|
170
|
+
|
|
171
|
+
for (const mod of modules) {
|
|
172
|
+
const result = reconstructCode(mod.content, {
|
|
173
|
+
patternPath,
|
|
174
|
+
propagateNames: true,
|
|
175
|
+
addComments,
|
|
176
|
+
improveStyle,
|
|
177
|
+
minConfidence,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const originalContent = mod.content;
|
|
181
|
+
mod.content = result.code;
|
|
182
|
+
mod.renames = result.renames;
|
|
183
|
+
mod.confidence = Math.max(mod.confidence, result.confidence);
|
|
184
|
+
|
|
185
|
+
totalRenames += result.renames.length;
|
|
186
|
+
totalComments += result.comments;
|
|
187
|
+
totalConfidence += result.confidence;
|
|
188
|
+
|
|
189
|
+
// Optional: validate the reconstruction
|
|
190
|
+
if (validate) {
|
|
191
|
+
const validation = validateReconstruction(originalContent, result.code);
|
|
192
|
+
validationResults.push({
|
|
193
|
+
module: mod.name,
|
|
194
|
+
...validation,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
reconstructionSummary = {
|
|
200
|
+
totalRenames,
|
|
201
|
+
totalComments,
|
|
202
|
+
averageConfidence: modules.length > 0
|
|
203
|
+
? parseFloat((totalConfidence / modules.length).toFixed(3))
|
|
204
|
+
: 0,
|
|
205
|
+
modulesProcessed: modules.length,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
if (validate) {
|
|
209
|
+
reconstructionSummary.validation = validationResults;
|
|
210
|
+
reconstructionSummary.allValid = validationResults.every((v) => v.syntaxValid);
|
|
211
|
+
reconstructionSummary.allEquivalent = validationResults.every((v) => v.functionallyEquivalent);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
modules,
|
|
217
|
+
metrics: {
|
|
218
|
+
source: sourceMetrics,
|
|
219
|
+
modules: moduleMetrics,
|
|
220
|
+
unclassifiedStatements: unclassified.length,
|
|
221
|
+
},
|
|
222
|
+
witness: witnessChain,
|
|
223
|
+
beautifiedSource: beautified,
|
|
224
|
+
...(reconstructionSummary ? { reconstruction: reconstructionSummary } : {}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Decompile an npm package.
|
|
230
|
+
*
|
|
231
|
+
* @param {string} packageName - e.g. 'express', '@anthropic-ai/claude-code'
|
|
232
|
+
* @param {string} [version] - defaults to 'latest'
|
|
233
|
+
* @param {object} [options]
|
|
234
|
+
* @param {number} [options.minConfidence=0.3]
|
|
235
|
+
* @param {boolean} [options.witness=true]
|
|
236
|
+
* @returns {Promise<{modules: object[], metrics: object, witness: object|null, packageInfo: object, bundlePath: string, source: string}>}
|
|
237
|
+
*/
|
|
238
|
+
async function decompilePackage(packageName, version, options = {}) {
|
|
239
|
+
const info = await fetchPackageInfo(packageName);
|
|
240
|
+
const resolvedVersion = version || info.latest;
|
|
241
|
+
|
|
242
|
+
if (!info.versions.includes(resolvedVersion)) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Version "${resolvedVersion}" not found for ${packageName}. ` +
|
|
245
|
+
`Available: ${info.versions.slice(0, 10).join(', ')}...`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const files = await fetchPackageFileList(packageName, resolvedVersion);
|
|
250
|
+
const pkgJson = info.packageJson || {};
|
|
251
|
+
const bundlePath = findMainBundle(files, pkgJson);
|
|
252
|
+
|
|
253
|
+
if (!bundlePath) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`Could not find main bundle for ${packageName}@${resolvedVersion}. ` +
|
|
256
|
+
`Files: ${files.slice(0, 10).map((f) => f.name).join(', ')}`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const source = await fetchFileContent(packageName, resolvedVersion, bundlePath);
|
|
261
|
+
const result = decompileSource(source, options);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
...result,
|
|
265
|
+
packageInfo: {
|
|
266
|
+
name: info.name,
|
|
267
|
+
version: resolvedVersion,
|
|
268
|
+
description: info.description,
|
|
269
|
+
bundlePath,
|
|
270
|
+
bundleSize: source.length,
|
|
271
|
+
},
|
|
272
|
+
source,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Decompile a local JavaScript file.
|
|
278
|
+
*
|
|
279
|
+
* @param {string} filePath - path to a .js file
|
|
280
|
+
* @param {object} [options]
|
|
281
|
+
* @returns {{modules: object[], metrics: object, witness: object|null, filePath: string, source: string}}
|
|
282
|
+
*/
|
|
283
|
+
function decompileFile(filePath, options = {}) {
|
|
284
|
+
const resolved = path.resolve(filePath);
|
|
285
|
+
|
|
286
|
+
if (!fs.existsSync(resolved)) {
|
|
287
|
+
throw new Error(`File not found: ${resolved}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const source = fs.readFileSync(resolved, 'utf-8');
|
|
291
|
+
const result = decompileSource(source, { ...options, filePath: resolved });
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
...result,
|
|
295
|
+
filePath: resolved,
|
|
296
|
+
source,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Decompile JavaScript from a URL.
|
|
302
|
+
*
|
|
303
|
+
* @param {string} url
|
|
304
|
+
* @param {object} [options]
|
|
305
|
+
* @returns {Promise<{modules: object[], metrics: object, witness: object|null, url: string, source: string}>}
|
|
306
|
+
*/
|
|
307
|
+
async function decompileUrl(url, options = {}) {
|
|
308
|
+
const resp = await fetch(url, { redirect: 'follow' });
|
|
309
|
+
if (!resp.ok) {
|
|
310
|
+
throw new Error(`Failed to fetch ${url} (HTTP ${resp.status})`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const source = await resp.text();
|
|
314
|
+
const result = decompileSource(source, options);
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
...result,
|
|
318
|
+
url,
|
|
319
|
+
source,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Write decompilation results to an output directory.
|
|
325
|
+
*
|
|
326
|
+
* @param {object} result - decompilation result from any of the decompile* functions
|
|
327
|
+
* @param {string} outputDir
|
|
328
|
+
* @param {string} [format='modules'] - 'modules', 'single', 'json'
|
|
329
|
+
*/
|
|
330
|
+
function writeOutput(result, outputDir, format = 'modules') {
|
|
331
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
332
|
+
|
|
333
|
+
if (format === 'json') {
|
|
334
|
+
const jsonResult = {
|
|
335
|
+
modules: result.modules.map((m) => ({
|
|
336
|
+
name: m.name,
|
|
337
|
+
fragments: m.fragments,
|
|
338
|
+
confidence: m.confidence,
|
|
339
|
+
content: m.content,
|
|
340
|
+
})),
|
|
341
|
+
metrics: result.metrics,
|
|
342
|
+
witness: result.witness,
|
|
343
|
+
packageInfo: result.packageInfo || null,
|
|
344
|
+
};
|
|
345
|
+
fs.writeFileSync(
|
|
346
|
+
path.join(outputDir, 'decompiled.json'),
|
|
347
|
+
JSON.stringify(jsonResult, null, 2),
|
|
348
|
+
);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (format === 'single') {
|
|
353
|
+
let output = '';
|
|
354
|
+
for (const mod of result.modules) {
|
|
355
|
+
output += `// ─── Module: ${mod.name} (confidence: ${mod.confidence}) ───\n\n`;
|
|
356
|
+
output += mod.content + '\n\n';
|
|
357
|
+
}
|
|
358
|
+
fs.writeFileSync(path.join(outputDir, 'decompiled.js'), output);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Default: 'modules' format — one file per module
|
|
363
|
+
// Supports hierarchical module names like 'tools/bash' -> tools/bash.js
|
|
364
|
+
for (let i = 0; i < result.modules.length; i++) {
|
|
365
|
+
const mod = result.modules[i];
|
|
366
|
+
const header = `// Module: ${mod.name}\n// Confidence: ${mod.confidence}\n// Fragments: ${mod.fragments}\n\n`;
|
|
367
|
+
|
|
368
|
+
if (mod.name.includes('/')) {
|
|
369
|
+
// Hierarchical: create subdirectories
|
|
370
|
+
const filePath = path.join(outputDir, mod.name + '.js');
|
|
371
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
372
|
+
fs.writeFileSync(filePath, header + mod.content);
|
|
373
|
+
} else {
|
|
374
|
+
const idx = String(i + 1).padStart(3, '0');
|
|
375
|
+
const fileName = `module-${idx}-${mod.name}.js`;
|
|
376
|
+
fs.writeFileSync(path.join(outputDir, fileName), header + mod.content);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Metrics
|
|
381
|
+
fs.writeFileSync(
|
|
382
|
+
path.join(outputDir, 'metrics.json'),
|
|
383
|
+
JSON.stringify(result.metrics, null, 2),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// Witness chain
|
|
387
|
+
if (result.witness) {
|
|
388
|
+
fs.writeFileSync(
|
|
389
|
+
path.join(outputDir, 'witness.json'),
|
|
390
|
+
JSON.stringify(result.witness, null, 2),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = {
|
|
396
|
+
decompilePackage,
|
|
397
|
+
decompileFile,
|
|
398
|
+
decompileUrl,
|
|
399
|
+
decompileSource,
|
|
400
|
+
writeOutput,
|
|
401
|
+
beautify,
|
|
402
|
+
parseTarget,
|
|
403
|
+
verifyWitnessChain,
|
|
404
|
+
reconstructCode,
|
|
405
|
+
reconstructRunnable,
|
|
406
|
+
validateReconstruction,
|
|
407
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* metrics.js - Code metrics extraction from JavaScript source.
|
|
3
|
+
*
|
|
4
|
+
* Computes structural metrics: function count, class count,
|
|
5
|
+
* declaration count, line count, async patterns, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Compute code metrics for a JavaScript source string.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} source - JavaScript source code
|
|
14
|
+
* @returns {{
|
|
15
|
+
* lines: number,
|
|
16
|
+
* sizeBytes: number,
|
|
17
|
+
* functions: number,
|
|
18
|
+
* asyncFunctions: number,
|
|
19
|
+
* arrowFunctions: number,
|
|
20
|
+
* classes: number,
|
|
21
|
+
* classExtensions: number,
|
|
22
|
+
* constDeclarations: number,
|
|
23
|
+
* letDeclarations: number,
|
|
24
|
+
* varDeclarations: number,
|
|
25
|
+
* imports: number,
|
|
26
|
+
* exports: number,
|
|
27
|
+
* requires: number,
|
|
28
|
+
* awaitExpressions: number,
|
|
29
|
+
* promiseUsages: number,
|
|
30
|
+
* tryBlocks: number,
|
|
31
|
+
* throwStatements: number,
|
|
32
|
+
* regexLiterals: number
|
|
33
|
+
* }}
|
|
34
|
+
*/
|
|
35
|
+
function computeMetrics(source) {
|
|
36
|
+
const count = (pattern) => (source.match(pattern) || []).length;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
lines: source.split('\n').length,
|
|
40
|
+
sizeBytes: Buffer.byteLength(source, 'utf-8'),
|
|
41
|
+
functions: count(/function\s*\w*\s*\(/g),
|
|
42
|
+
asyncFunctions: count(/async\s+function/g),
|
|
43
|
+
arrowFunctions: count(/=>/g),
|
|
44
|
+
classes: count(/class\s+\w+/g),
|
|
45
|
+
classExtensions: count(/extends\s+\w+/g),
|
|
46
|
+
constDeclarations: count(/\bconst\s+/g),
|
|
47
|
+
letDeclarations: count(/\blet\s+/g),
|
|
48
|
+
varDeclarations: count(/\bvar\s+/g),
|
|
49
|
+
imports: count(/\bimport\s+/g),
|
|
50
|
+
exports: count(/\bexport\s+/g),
|
|
51
|
+
requires: count(/\brequire\s*\(/g),
|
|
52
|
+
awaitExpressions: count(/\bawait\s+/g),
|
|
53
|
+
promiseUsages: count(/\bPromise\b/g),
|
|
54
|
+
tryBlocks: count(/\btry\s*\{/g),
|
|
55
|
+
throwStatements: count(/\bthrow\s+/g),
|
|
56
|
+
regexLiterals: count(/\/[^/\n]+\/[gimsuy]*/g),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Compute a summary of metrics across multiple modules.
|
|
62
|
+
*
|
|
63
|
+
* @param {Array<{name: string, content: string}>} modules
|
|
64
|
+
* @returns {{moduleCount: number, totalLines: number, totalBytes: number, perModule: object[]}}
|
|
65
|
+
*/
|
|
66
|
+
function computeModuleMetrics(modules) {
|
|
67
|
+
const perModule = modules.map((mod) => ({
|
|
68
|
+
name: mod.name,
|
|
69
|
+
...computeMetrics(mod.content),
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const totalLines = perModule.reduce((sum, m) => sum + m.lines, 0);
|
|
73
|
+
const totalBytes = perModule.reduce((sum, m) => sum + m.sizeBytes, 0);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
moduleCount: modules.length,
|
|
77
|
+
totalLines,
|
|
78
|
+
totalBytes,
|
|
79
|
+
perModule,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
computeMetrics,
|
|
85
|
+
computeModuleMetrics,
|
|
86
|
+
};
|