@chappibunny/repolens 1.16.2 → 1.16.3
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/CHANGELOG.md +31 -0
- package/README.md +8 -8
- package/package.json +1 -1
- package/src/ai/generate-sections.js +23 -4
- package/src/analyzers/complexity-analyzer.js +17 -4
- package/src/analyzers/context-builder.js +2 -1
- package/src/analyzers/dependency-graph.js +144 -21
- package/src/analyzers/flow-inference.js +10 -1
- package/src/analyzers/graphql-analyzer.js +80 -32
- package/src/analyzers/language-coverage.js +222 -0
- package/src/analyzers/workspace-packages.js +105 -0
- package/src/badge.js +59 -18
- package/src/core/scan.js +10 -4
- package/src/docs/generate-doc-set.js +11 -3
- package/src/help.js +8 -3
- package/src/renderers/render.js +14 -2
- package/src/renderers/renderAnalysis.js +12 -0
- package/src/utils/js-lexer.js +146 -0
|
@@ -130,7 +130,7 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
130
130
|
|
|
131
131
|
for (const docPlan of activeDocuments) {
|
|
132
132
|
docIndex++;
|
|
133
|
-
const emptyReason = emptyDocumentReason(docPlan, { config, scanResult, graphqlResult });
|
|
133
|
+
const emptyReason = emptyDocumentReason(docPlan, { config, scanResult, graphqlResult, jsdocResult });
|
|
134
134
|
if (emptyReason) {
|
|
135
135
|
info(`[${docIndex}/${totalDocs}] Skipping ${docPlan.key}: ${emptyReason}`);
|
|
136
136
|
continue;
|
|
@@ -244,10 +244,11 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
244
244
|
|
|
245
245
|
/**
|
|
246
246
|
* Documents that would only say "nothing found" are not written. A library has no GraphQL
|
|
247
|
-
* schema and no routes, so those two pages would be noise
|
|
247
|
+
* schema and no routes, so those two pages would be noise, and a CLI or library with no
|
|
248
|
+
* routes, no external API clients, and no documented exports has no API Surface to show. A document the config lists
|
|
248
249
|
* explicitly in `documentation.sections` is always written.
|
|
249
250
|
*/
|
|
250
|
-
export function emptyDocumentReason(docPlan, { config, scanResult, graphqlResult }) {
|
|
251
|
+
export function emptyDocumentReason(docPlan, { config, scanResult, graphqlResult, jsdocResult = null }) {
|
|
251
252
|
if (config?.documentation?.sections?.includes(docPlan.key)) return null;
|
|
252
253
|
if (docPlan.key === "graphql_schema" && !graphqlResult?.detected) {
|
|
253
254
|
return "no GraphQL schema detected";
|
|
@@ -255,9 +256,16 @@ export function emptyDocumentReason(docPlan, { config, scanResult, graphqlResult
|
|
|
255
256
|
if (docPlan.key === "route_map" && !(scanResult?.pages?.length) && !(scanResult?.api?.length)) {
|
|
256
257
|
return "no pages or API routes detected";
|
|
257
258
|
}
|
|
259
|
+
if (docPlan.key === "api_surface" && !(scanResult?.api?.length) && !(scanResult?.externalApis?.length) && !hasDocumentedExports(jsdocResult)) {
|
|
260
|
+
return "no API routes, external API integrations, or documented exports detected";
|
|
261
|
+
}
|
|
258
262
|
return null;
|
|
259
263
|
}
|
|
260
264
|
|
|
265
|
+
function hasDocumentedExports(jsdocResult) {
|
|
266
|
+
return Boolean(jsdocResult?.detected && Array.isArray(jsdocResult.exports) && jsdocResult.exports.length > 0);
|
|
267
|
+
}
|
|
268
|
+
|
|
261
269
|
async function generateDocument(docPlan, context) {
|
|
262
270
|
const { key } = docPlan;
|
|
263
271
|
const { scanResult, config, aiContext, moduleContext, flows, diffData, graphqlResult, tsResult, jsdocResult, depGraph, driftResult, securityResult, codeHealthResult, ownershipMap, pluginManager } = context;
|
package/src/help.js
CHANGED
|
@@ -32,6 +32,8 @@ It detects Python, Go, Rust, Java/Kotlin, JavaScript/TypeScript, Ruby, PHP,
|
|
|
32
32
|
C#, Swift, and C/C++ (and combinations) and sets scan.include plus
|
|
33
33
|
module_roots from the files it finds. A repository with no recognized
|
|
34
34
|
language gets multi-language globs, not JavaScript-only defaults.
|
|
35
|
+
Detection is not full analysis: the dependency graph and code health read
|
|
36
|
+
JavaScript/TypeScript, Python, and Go only.
|
|
35
37
|
|
|
36
38
|
The target directory must already exist.
|
|
37
39
|
|
|
@@ -129,9 +131,12 @@ Options:
|
|
|
129
131
|
--help, -h Show this help
|
|
130
132
|
|
|
131
133
|
With no config, demo detects the repository language and scans matching
|
|
132
|
-
files (Python, Go, Rust, Java, JavaScript, and others)
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
files (Python, Go, Rust, Java, JavaScript, and others) for modules and file
|
|
135
|
+
structure. The dependency graph and code health cover JavaScript/TypeScript,
|
|
136
|
+
Python, and Go only; for other languages (for example Java, Kotlin, or C#)
|
|
137
|
+
those pages say the language is not supported yet instead of showing a
|
|
138
|
+
score. It does not publish to Notion or Confluence. A scan that matches
|
|
139
|
+
0 files exits 2 and does not write empty docs.
|
|
135
140
|
|
|
136
141
|
The target directory must already exist.
|
|
137
142
|
|
package/src/renderers/render.js
CHANGED
|
@@ -151,6 +151,18 @@ function describeModule(key, depRole = null, evidence = {}) {
|
|
|
151
151
|
return base;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// A module whose files are mostly tests and fixtures is not "Application module".
|
|
155
|
+
// The file count stays the same; the role says how much of it is test code.
|
|
156
|
+
export function describeModuleWithTests(module, desc) {
|
|
157
|
+
const total = Number(module?.fileCount) || 0;
|
|
158
|
+
const tests = Number(module?.testFileCount) || 0;
|
|
159
|
+
if (!total || tests * 2 < total) return desc;
|
|
160
|
+
if (/^Test suites and fixtures/.test(desc)) return desc;
|
|
161
|
+
const role = desc.replace(/^Application module( · )?/, "");
|
|
162
|
+
const base = `Mostly tests and fixtures (${tests} of ${total} files)`;
|
|
163
|
+
return role ? `${base} · ${role}` : base;
|
|
164
|
+
}
|
|
165
|
+
|
|
154
166
|
export function renderModuleCatalog(cfg, scan, ownershipMap = {}, depGraph = null) {
|
|
155
167
|
const hasOwnership = Object.keys(ownershipMap).length > 0;
|
|
156
168
|
const moduleMetrics = computeModuleLevelMetrics(depGraph, scan.modules);
|
|
@@ -192,7 +204,7 @@ export function renderModuleCatalog(cfg, scan, ownershipMap = {}, depGraph = nul
|
|
|
192
204
|
|
|
193
205
|
for (const module of scan.modules.slice(0, 100)) {
|
|
194
206
|
const depRole = describeModuleDepRole(module.key, moduleMetrics);
|
|
195
|
-
const desc = describeModule(module.key, depRole, moduleEvidence(module.key, depGraph));
|
|
207
|
+
const desc = describeModuleWithTests(module, describeModule(module.key, depRole, moduleEvidence(module.key, depGraph)));
|
|
196
208
|
const owners = ownershipMap[module.key];
|
|
197
209
|
if (hasOwnership) {
|
|
198
210
|
lines.push(`| \`${module.key}\` | ${module.fileCount} | ${desc} | ${owners ? owners.join(", ") : "—"} |`);
|
|
@@ -236,7 +248,7 @@ export function renderApiSurface(cfg, scan, jsdocResult = null) {
|
|
|
236
248
|
|
|
237
249
|
if (!scan.api.length) {
|
|
238
250
|
lines.push(
|
|
239
|
-
`No API routes were detected
|
|
251
|
+
`No HTTP API routes were detected. RepoLens reads Next.js API routes and Express-style \`app.get(...)\` / \`router.post(...)\` handlers in JavaScript and TypeScript; libraries and CLIs usually have none.`,
|
|
240
252
|
``
|
|
241
253
|
);
|
|
242
254
|
} else {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// - Dependency Graph
|
|
5
5
|
// - Architecture Drift
|
|
6
6
|
|
|
7
|
+
import { coverageNotice } from "../analyzers/language-coverage.js";
|
|
8
|
+
|
|
7
9
|
export function renderGraphQLSchema(graphqlResult) {
|
|
8
10
|
if (!graphqlResult?.detected) {
|
|
9
11
|
return [
|
|
@@ -316,6 +318,10 @@ export function renderTypeGraph(tsResult) {
|
|
|
316
318
|
}
|
|
317
319
|
|
|
318
320
|
export function renderDependencyGraph(depResult) {
|
|
321
|
+
const coverageLines = coverageNotice(depResult?.coverage, "import analysis");
|
|
322
|
+
if (!depResult?.nodes?.length && depResult?.coverage?.status === "unsupported") {
|
|
323
|
+
return ["# Dependency Graph", "", ...coverageLines].join("\n");
|
|
324
|
+
}
|
|
319
325
|
if (!depResult?.nodes?.length) {
|
|
320
326
|
return [
|
|
321
327
|
"# Dependency Graph",
|
|
@@ -332,6 +338,7 @@ export function renderDependencyGraph(depResult) {
|
|
|
332
338
|
lines.push("");
|
|
333
339
|
lines.push(`> ${depResult.summary}`);
|
|
334
340
|
lines.push("");
|
|
341
|
+
lines.push(...coverageLines);
|
|
335
342
|
lines.push("This document maps every import relationship in the codebase, identifies the most-connected modules, and flags circular dependencies that may complicate refactoring or increase build times.");
|
|
336
343
|
lines.push("");
|
|
337
344
|
|
|
@@ -526,6 +533,10 @@ export function renderArchitectureDrift(driftResult) {
|
|
|
526
533
|
}
|
|
527
534
|
|
|
528
535
|
export function renderCodeHealth(healthResult) {
|
|
536
|
+
const coverageLines = coverageNotice(healthResult?.coverage, "code health");
|
|
537
|
+
if (!healthResult?.modules?.length && healthResult?.coverage?.status === "unsupported") {
|
|
538
|
+
return ["# Code Health Report", "", ...coverageLines].join("\n");
|
|
539
|
+
}
|
|
529
540
|
if (!healthResult?.modules?.length) {
|
|
530
541
|
return [
|
|
531
542
|
"# Code Health Report",
|
|
@@ -544,6 +555,7 @@ export function renderCodeHealth(healthResult) {
|
|
|
544
555
|
lines.push("");
|
|
545
556
|
lines.push(`> ${healthResult.summary}`);
|
|
546
557
|
lines.push("");
|
|
558
|
+
lines.push(...coverageLines);
|
|
547
559
|
lines.push("This report synthesizes complexity, coupling, documentation, and security signals into a single health score per module. Lower scores indicate higher risk and maintenance burden.");
|
|
548
560
|
lines.push("");
|
|
549
561
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Remove JavaScript/TypeScript comments while keeping every string, template literal
|
|
2
|
+
// (including `${...}` expressions, which may hold their own comments and strings),
|
|
3
|
+
// and regular-expression literal intact. Comment text becomes spaces; newlines stay,
|
|
4
|
+
// so offsets and line numbers still match the source.
|
|
5
|
+
|
|
6
|
+
const REGEX_PREFIX_WORDS = new Set([
|
|
7
|
+
"return", "typeof", "instanceof", "in", "of", "new", "delete", "void", "throw",
|
|
8
|
+
"case", "do", "else", "yield", "await",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function blank(text) {
|
|
12
|
+
return text.replace(/[^\n]/g, " ");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isIdentPart(ch) {
|
|
16
|
+
return /[A-Za-z0-9_$]/.test(ch || "");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function stripJsComments(source) {
|
|
20
|
+
const src = String(source ?? "");
|
|
21
|
+
const n = src.length;
|
|
22
|
+
let out = "";
|
|
23
|
+
let i = 0;
|
|
24
|
+
// Stack of contexts: { type: "code", depth, inTemplate } or { type: "template" }.
|
|
25
|
+
const stack = [{ type: "code", depth: 0, inTemplate: false }];
|
|
26
|
+
// Last significant code token, to tell a regex literal from division.
|
|
27
|
+
let lastChar = "";
|
|
28
|
+
let lastWord = "";
|
|
29
|
+
|
|
30
|
+
while (i < n) {
|
|
31
|
+
const ctx = stack[stack.length - 1];
|
|
32
|
+
const ch = src[i];
|
|
33
|
+
|
|
34
|
+
if (ctx.type === "template") {
|
|
35
|
+
if (ch === "\\") {
|
|
36
|
+
out += src.slice(i, i + 2);
|
|
37
|
+
i += 2;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (ch === "`") {
|
|
41
|
+
out += ch;
|
|
42
|
+
i += 1;
|
|
43
|
+
stack.pop();
|
|
44
|
+
lastChar = "`";
|
|
45
|
+
lastWord = "";
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (ch === "$" && src[i + 1] === "{") {
|
|
49
|
+
out += "${";
|
|
50
|
+
i += 2;
|
|
51
|
+
stack.push({ type: "code", depth: 0, inTemplate: true });
|
|
52
|
+
lastChar = "{";
|
|
53
|
+
lastWord = "";
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
out += ch;
|
|
57
|
+
i += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// code context
|
|
62
|
+
const next = src[i + 1];
|
|
63
|
+
if (ch === "/" && next === "/") {
|
|
64
|
+
const nl = src.indexOf("\n", i);
|
|
65
|
+
const end = nl === -1 ? n : nl;
|
|
66
|
+
out += blank(src.slice(i, end));
|
|
67
|
+
i = end;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === "/" && next === "*") {
|
|
71
|
+
const close = src.indexOf("*/", i + 2);
|
|
72
|
+
const end = close === -1 ? n : close + 2;
|
|
73
|
+
out += blank(src.slice(i, end));
|
|
74
|
+
i = end;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (ch === "'" || ch === '"') {
|
|
78
|
+
let j = i + 1;
|
|
79
|
+
while (j < n) {
|
|
80
|
+
if (src[j] === "\\") { j += 2; continue; }
|
|
81
|
+
if (src[j] === ch || src[j] === "\n") { j += 1; break; }
|
|
82
|
+
j += 1;
|
|
83
|
+
}
|
|
84
|
+
out += src.slice(i, Math.min(j, n));
|
|
85
|
+
i = Math.min(j, n);
|
|
86
|
+
lastChar = ch;
|
|
87
|
+
lastWord = "";
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ch === "`") {
|
|
91
|
+
out += ch;
|
|
92
|
+
i += 1;
|
|
93
|
+
stack.push({ type: "template" });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (ch === "/") {
|
|
97
|
+
const regexAllowed = lastChar === "" || "([{,;:!?&|~^=<>+-*%".includes(lastChar) || REGEX_PREFIX_WORDS.has(lastWord);
|
|
98
|
+
if (regexAllowed) {
|
|
99
|
+
let j = i + 1;
|
|
100
|
+
let inClass = false;
|
|
101
|
+
while (j < n) {
|
|
102
|
+
const c = src[j];
|
|
103
|
+
if (c === "\\") { j += 2; continue; }
|
|
104
|
+
if (c === "\n") break;
|
|
105
|
+
if (c === "[") inClass = true;
|
|
106
|
+
else if (c === "]") inClass = false;
|
|
107
|
+
else if (c === "/" && !inClass) { j += 1; break; }
|
|
108
|
+
j += 1;
|
|
109
|
+
}
|
|
110
|
+
while (j < n && /[a-z]/i.test(src[j])) j += 1;
|
|
111
|
+
out += src.slice(i, j);
|
|
112
|
+
i = j;
|
|
113
|
+
lastChar = ")";
|
|
114
|
+
lastWord = "";
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (ch === "{") ctx.depth += 1;
|
|
119
|
+
if (ch === "}") {
|
|
120
|
+
if (ctx.inTemplate && ctx.depth === 0) {
|
|
121
|
+
out += ch;
|
|
122
|
+
i += 1;
|
|
123
|
+
stack.pop();
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
ctx.depth -= 1;
|
|
127
|
+
}
|
|
128
|
+
if (isIdentPart(ch)) {
|
|
129
|
+
let j = i;
|
|
130
|
+
while (j < n && isIdentPart(src[j])) j += 1;
|
|
131
|
+
const word = src.slice(i, j);
|
|
132
|
+
out += word;
|
|
133
|
+
i = j;
|
|
134
|
+
lastWord = word;
|
|
135
|
+
lastChar = word[word.length - 1];
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
out += ch;
|
|
139
|
+
i += 1;
|
|
140
|
+
if (!/\s/.test(ch)) {
|
|
141
|
+
lastChar = ch;
|
|
142
|
+
lastWord = "";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|