project-graph-mcp 1.3.0 → 1.5.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/AGENT_ROLE.md +87 -30
- package/AGENT_ROLE_MINIMAL.md +23 -8
- package/README.md +100 -14
- package/package.json +1 -1
- package/src/.project-graph-cache.json +1 -0
- package/src/ai-context.js +113 -0
- package/src/analysis-cache.js +155 -0
- package/src/cli-handlers.js +131 -0
- package/src/cli.js +14 -2
- package/src/compact.js +207 -0
- package/src/complexity.js +21 -7
- package/src/compress.js +319 -0
- package/src/ctx-to-jsdoc.js +514 -0
- package/src/custom-rules.js +1 -0
- package/src/doc-dialect.js +716 -0
- package/src/full-analysis.js +307 -11
- package/src/instructions.js +3 -105
- package/src/jsdoc-checker.js +351 -0
- package/src/jsdoc-generator.js +0 -11
- package/src/large-files.js +1 -0
- package/src/mcp-server.js +208 -1
- package/src/mode-config.js +127 -0
- package/src/outdated-patterns.js +1 -0
- package/src/parser.js +223 -13
- package/src/similar-functions.js +1 -0
- package/src/test-annotations.js +203 -181
- package/src/tool-defs.js +270 -2
- package/src/tools.js +1 -1
- package/src/type-checker.js +188 -0
- package/src/undocumented.js +11 -12
- package/src/workspace.js +1 -1
- package/vendor/terser.mjs +49 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analysis Cache Manager
|
|
3
|
+
* Persistent file-based cache in .context/.cache/ with dual hashing
|
|
4
|
+
*
|
|
5
|
+
* - sig: interface hash (function names, params, exports) → invalidates docs
|
|
6
|
+
* - contentHash: full source hash → invalidates body-dependent metrics
|
|
7
|
+
*
|
|
8
|
+
* Cacheable (per-file): complexity, undocumented, jsdocConsistency
|
|
9
|
+
* NOT cacheable (cross-file): deadCode, similarity
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'fs';
|
|
13
|
+
import { join, dirname } from 'path';
|
|
14
|
+
import { createHash } from 'crypto';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} CacheEntry
|
|
18
|
+
* @property {string} sig - Interface hash
|
|
19
|
+
* @property {string} contentHash - Full content hash
|
|
20
|
+
* @property {Object} [complexity] - Cached complexity results
|
|
21
|
+
* @property {Array} [undocumented] - Cached undocumented items
|
|
22
|
+
* @property {Array} [jsdocIssues] - Cached JSDoc consistency issues
|
|
23
|
+
* @property {string} cachedAt - ISO timestamp
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Compute interface signature hash
|
|
28
|
+
* Matches @sig logic from doc-dialect.js
|
|
29
|
+
* @param {Object} fileData - Parsed file data with functions, classes, exports
|
|
30
|
+
* @returns {string} - 8-char hex hash
|
|
31
|
+
*/
|
|
32
|
+
export function computeSig(fileData) {
|
|
33
|
+
const parts = [];
|
|
34
|
+
|
|
35
|
+
// Function signatures
|
|
36
|
+
if (fileData.functions) {
|
|
37
|
+
for (const fn of fileData.functions) {
|
|
38
|
+
parts.push(`fn:${fn.name}:${fn.params?.length || 0}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Class signatures
|
|
43
|
+
if (fileData.classes) {
|
|
44
|
+
for (const cls of fileData.classes) {
|
|
45
|
+
parts.push(`cls:${cls.name}`);
|
|
46
|
+
if (cls.methods) {
|
|
47
|
+
for (const m of cls.methods) {
|
|
48
|
+
parts.push(`m:${cls.name}.${m}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Exports
|
|
55
|
+
if (fileData.exports) {
|
|
56
|
+
for (const exp of fileData.exports) {
|
|
57
|
+
parts.push(`exp:${exp}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const hash = createHash('md5').update(parts.sort().join('|')).digest('hex');
|
|
62
|
+
return hash.slice(0, 8);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compute full content hash (body-inclusive)
|
|
67
|
+
* Used for complexity and other body-dependent metrics
|
|
68
|
+
* @param {string} code - Source code
|
|
69
|
+
* @returns {string} - 8-char hex hash
|
|
70
|
+
*/
|
|
71
|
+
export function computeContentHash(code) {
|
|
72
|
+
return createHash('md5').update(code).digest('hex').slice(0, 8);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get cache file path for a source file
|
|
77
|
+
* @param {string} contextDir - .context directory path
|
|
78
|
+
* @param {string} relPath - Relative path to source file (e.g., "src/parser.js")
|
|
79
|
+
* @returns {string} - Cache file path
|
|
80
|
+
*/
|
|
81
|
+
export function getCachePath(contextDir, relPath) {
|
|
82
|
+
// src/parser.js → .context/.cache/src/parser.json
|
|
83
|
+
const cacheName = relPath.replace(/\.[^.]+$/, '.json');
|
|
84
|
+
return join(contextDir, '.cache', cacheName);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read cached analysis for a file
|
|
89
|
+
* @param {string} contextDir
|
|
90
|
+
* @param {string} relPath
|
|
91
|
+
* @returns {CacheEntry|null}
|
|
92
|
+
*/
|
|
93
|
+
export function readCache(contextDir, relPath) {
|
|
94
|
+
const cachePath = getCachePath(contextDir, relPath);
|
|
95
|
+
try {
|
|
96
|
+
if (!existsSync(cachePath)) return null;
|
|
97
|
+
return JSON.parse(readFileSync(cachePath, 'utf-8'));
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Write cache entry for a file
|
|
105
|
+
* @param {string} contextDir
|
|
106
|
+
* @param {string} relPath
|
|
107
|
+
* @param {CacheEntry} data
|
|
108
|
+
*/
|
|
109
|
+
export function writeCache(contextDir, relPath, data) {
|
|
110
|
+
const cachePath = getCachePath(contextDir, relPath);
|
|
111
|
+
try {
|
|
112
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
113
|
+
writeFileSync(cachePath, JSON.stringify({
|
|
114
|
+
...data,
|
|
115
|
+
cachedAt: new Date().toISOString(),
|
|
116
|
+
}, null, 2));
|
|
117
|
+
} catch (e) {
|
|
118
|
+
// Cache write failure is non-fatal
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Check if cache is still valid
|
|
124
|
+
* @param {CacheEntry|null} cached
|
|
125
|
+
* @param {string} currentSig - Current interface hash
|
|
126
|
+
* @param {string} currentContentHash - Current content hash
|
|
127
|
+
* @param {'sig'|'content'} level - Which hash to check
|
|
128
|
+
* @returns {boolean}
|
|
129
|
+
*/
|
|
130
|
+
export function isCacheValid(cached, currentSig, currentContentHash, level = 'content') {
|
|
131
|
+
if (!cached) return false;
|
|
132
|
+
if (!cached.sig || !cached.contentHash) return false;
|
|
133
|
+
|
|
134
|
+
if (level === 'sig') {
|
|
135
|
+
return cached.sig === currentSig;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// For body-dependent metrics, both hashes must match
|
|
139
|
+
return cached.sig === currentSig && cached.contentHash === currentContentHash;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Invalidate all caches (e.g., after structural changes)
|
|
144
|
+
* @param {string} contextDir
|
|
145
|
+
*/
|
|
146
|
+
export function invalidateAllCaches(contextDir) {
|
|
147
|
+
const cacheDir = join(contextDir, '.cache');
|
|
148
|
+
try {
|
|
149
|
+
if (existsSync(cacheDir)) {
|
|
150
|
+
rmSync(cacheDir, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
// Non-fatal
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/cli-handlers.js
CHANGED
|
@@ -15,7 +15,16 @@ import { getComplexity } from './complexity.js';
|
|
|
15
15
|
import { getLargeFiles } from './large-files.js';
|
|
16
16
|
import { getOutdatedPatterns } from './outdated-patterns.js';
|
|
17
17
|
import { getFullAnalysis } from './full-analysis.js';
|
|
18
|
+
import { compressFile } from './compress.js';
|
|
19
|
+
import { getProjectDocs, generateContextFiles } from './doc-dialect.js';
|
|
20
|
+
import { getGraph } from './tools.js';
|
|
21
|
+
import { parseProject } from './parser.js';
|
|
18
22
|
import { resolvePath } from './workspace.js';
|
|
23
|
+
import { checkJSDocConsistency } from './jsdoc-checker.js';
|
|
24
|
+
import { checkTypes } from './type-checker.js';
|
|
25
|
+
import { compactProject, expandProject } from './compact.js';
|
|
26
|
+
import { injectJSDoc, stripJSDoc, validateCtxContracts } from './ctx-to-jsdoc.js';
|
|
27
|
+
import { getConfig, setConfig, getModeDescription, getModeWorkflow } from './mode-config.js';
|
|
19
28
|
|
|
20
29
|
/**
|
|
21
30
|
* Parse named argument from args array
|
|
@@ -137,4 +146,126 @@ export const CLI_HANDLERS = {
|
|
|
137
146
|
return getFullAnalysis(getPath(args), { includeItems });
|
|
138
147
|
},
|
|
139
148
|
},
|
|
149
|
+
|
|
150
|
+
'jsdoc-check': {
|
|
151
|
+
handler: async (args) => checkJSDocConsistency(getPath(args)),
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
types: {
|
|
155
|
+
handler: async (args) => {
|
|
156
|
+
const maxDiagnostics = parseInt(getArg(args, 'max')) || 50;
|
|
157
|
+
return checkTypes(getPath(args), { maxDiagnostics });
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
compress: {
|
|
162
|
+
requiresArg: true,
|
|
163
|
+
argError: 'Usage: compress <file> [--no-beautify] [--no-legend]',
|
|
164
|
+
handler: async (args) => {
|
|
165
|
+
const beautify = !args.includes('--no-beautify');
|
|
166
|
+
const legend = !args.includes('--no-legend');
|
|
167
|
+
return compressFile(resolvePath(args[0]), { beautify, legend });
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
docs: {
|
|
172
|
+
requiresArg: true,
|
|
173
|
+
argError: 'Usage: docs <path> [--file=<filename>]',
|
|
174
|
+
handler: async (args) => {
|
|
175
|
+
const projectPath = resolvePath(args[0]);
|
|
176
|
+
const graph = await getGraph(projectPath);
|
|
177
|
+
const file = args.find(a => a.startsWith('--file='))?.split('=')[1];
|
|
178
|
+
return getProjectDocs(graph, projectPath, { file });
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
'generate-ctx': {
|
|
183
|
+
requiresArg: true,
|
|
184
|
+
argError: 'Usage: generate-ctx <path> [--overwrite] [--scope=focus|all]',
|
|
185
|
+
handler: async (args) => {
|
|
186
|
+
const projectPath = resolvePath(args[0]);
|
|
187
|
+
const graph = await getGraph(projectPath);
|
|
188
|
+
const parsed = await parseProject(projectPath);
|
|
189
|
+
const overwrite = args.includes('--overwrite');
|
|
190
|
+
const scope = args.find(a => a.startsWith('--scope='))?.split('=')[1] || 'all';
|
|
191
|
+
return generateContextFiles(graph, projectPath, parsed, { overwrite, scope });
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
compact: {
|
|
196
|
+
requiresArg: true,
|
|
197
|
+
argError: 'Usage: compact <path> [--dry-run]',
|
|
198
|
+
handler: async (args) => {
|
|
199
|
+
const projectPath = resolvePath(args[0]);
|
|
200
|
+
const dryRun = args.includes('--dry-run');
|
|
201
|
+
return compactProject(projectPath, { dryRun });
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
beautify: {
|
|
206
|
+
requiresArg: true,
|
|
207
|
+
argError: 'Usage: beautify <path> [--dry-run]',
|
|
208
|
+
handler: async (args) => {
|
|
209
|
+
const projectPath = resolvePath(args[0]);
|
|
210
|
+
const dryRun = args.includes('--dry-run');
|
|
211
|
+
return expandProject(projectPath, { dryRun });
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
'inject-jsdoc': {
|
|
216
|
+
requiresArg: true,
|
|
217
|
+
argError: 'Usage: inject-jsdoc <path> [--dry-run]',
|
|
218
|
+
handler: async (args) => {
|
|
219
|
+
const projectPath = resolvePath(args[0]);
|
|
220
|
+
const dryRun = args.includes('--dry-run');
|
|
221
|
+
return injectJSDoc(projectPath, { dryRun });
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
'strip-jsdoc': {
|
|
226
|
+
requiresArg: true,
|
|
227
|
+
argError: 'Usage: strip-jsdoc <path> [--dry-run]',
|
|
228
|
+
handler: async (args) => {
|
|
229
|
+
const projectPath = resolvePath(args[0]);
|
|
230
|
+
const dryRun = args.includes('--dry-run');
|
|
231
|
+
return stripJSDoc(projectPath, { dryRun });
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
'validate-ctx': {
|
|
236
|
+
requiresArg: true,
|
|
237
|
+
argError: 'Usage: validate-ctx <path> [--strict]',
|
|
238
|
+
handler: async (args) => {
|
|
239
|
+
const projectPath = resolvePath(args[0]);
|
|
240
|
+
const strict = args.includes('--strict');
|
|
241
|
+
return validateCtxContracts(projectPath, { strict });
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
mode: {
|
|
246
|
+
requiresArg: true,
|
|
247
|
+
argError: 'Usage: mode <path>',
|
|
248
|
+
handler: async (args) => {
|
|
249
|
+
const dir = resolvePath(args[0]);
|
|
250
|
+
const config = getConfig(dir);
|
|
251
|
+
return {
|
|
252
|
+
...config,
|
|
253
|
+
description: getModeDescription(config.mode),
|
|
254
|
+
workflow: getModeWorkflow(config.mode),
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
'set-mode': {
|
|
260
|
+
requiresArg: true,
|
|
261
|
+
argError: 'Usage: set-mode <path> <1|2|3>',
|
|
262
|
+
handler: async (args) => {
|
|
263
|
+
const dir = resolvePath(args[0]);
|
|
264
|
+
const mode = parseInt(args[1], 10);
|
|
265
|
+
if (!mode || ![1, 2, 3].includes(mode)) {
|
|
266
|
+
throw new Error('Mode must be 1, 2, or 3');
|
|
267
|
+
}
|
|
268
|
+
return setConfig(dir, { mode });
|
|
269
|
+
},
|
|
270
|
+
},
|
|
140
271
|
};
|
package/src/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ Commands:
|
|
|
20
20
|
expand <symbol> Expand minified symbol (e.g., SN, SN.togglePin)
|
|
21
21
|
deps <symbol> Get dependency tree
|
|
22
22
|
usages <symbol> Find all usages
|
|
23
|
-
pending <path> List pending
|
|
23
|
+
pending <path> List pending .ctx.md test checklists
|
|
24
24
|
summary <path> Get test progress summary
|
|
25
25
|
undocumented <path> Find missing JSDoc (--level=tests|params|all)
|
|
26
26
|
deadcode <path> Find unused functions/classes
|
|
@@ -30,6 +30,18 @@ Commands:
|
|
|
30
30
|
largefiles <path> Find files needing split (--problematic)
|
|
31
31
|
outdated <path> Find legacy patterns & redundant deps
|
|
32
32
|
analyze <path> Run ALL checks with Health Score
|
|
33
|
+
jsdoc-check <path> Validate JSDoc ↔ function signatures
|
|
34
|
+
types <path> Run tsc type checking (--max=50)
|
|
35
|
+
compress <file> Compress JS file for AI (--no-beautify, --no-legend)
|
|
36
|
+
compact <path> Compact all JS files — strips comments/whitespace (--dry-run)
|
|
37
|
+
beautify <path> Beautify/expand all JS files — inverse of compact (--dry-run)
|
|
38
|
+
inject-jsdoc <path> Generate JSDoc from .ctx files and inject into source
|
|
39
|
+
strip-jsdoc <path> Strip all JSDoc blocks from source files
|
|
40
|
+
docs <path> Get project docs in doc-dialect format (--file=<name>)
|
|
41
|
+
generate-ctx <path> Generate .context/ docs (--overwrite --scope=focus)
|
|
42
|
+
validate-ctx <path> Validate .ctx contracts against source AST (--strict)
|
|
43
|
+
mode <path> Show current compact code mode and workflow
|
|
44
|
+
set-mode <path> <1|2|3> Set compact code mode (1=compact, 2=full, 3=IDE)
|
|
33
45
|
filters Show current filter configuration
|
|
34
46
|
instructions Show agent guidelines (JSDoc, Arch)
|
|
35
47
|
help Show this help
|
|
@@ -37,7 +49,7 @@ Commands:
|
|
|
37
49
|
Examples:
|
|
38
50
|
npx project-graph-mcp skeleton src/components
|
|
39
51
|
npx project-graph-mcp expand SN
|
|
40
|
-
npx project-graph-mcp
|
|
52
|
+
npx project-graph-mcp compact src/ --dry-run
|
|
41
53
|
`);
|
|
42
54
|
}
|
|
43
55
|
|
package/src/compact.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact/Beautify — Project-wide code compression and expansion
|
|
3
|
+
*
|
|
4
|
+
* Converts JS files between compact (minified, no comments) and
|
|
5
|
+
* beautified (formatted, readable) forms. Both preserve all names
|
|
6
|
+
* (mangle: false) — only whitespace and comments are affected.
|
|
7
|
+
*
|
|
8
|
+
* Types and documentation live in .ctx files, not in source code.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
12
|
+
import { join, extname, relative } from 'path';
|
|
13
|
+
import { minify } from '../vendor/terser.mjs';
|
|
14
|
+
|
|
15
|
+
const SUPPORTED = new Set(['.js', '.mjs']);
|
|
16
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'vendor', '.context', 'dev-docs', '.agent', '.agents']);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Walk directory for JS files
|
|
20
|
+
* @param {string} dir
|
|
21
|
+
* @param {string} rootDir
|
|
22
|
+
* @returns {string[]} Absolute paths
|
|
23
|
+
*/
|
|
24
|
+
function walkJSFiles(dir, rootDir = dir) {
|
|
25
|
+
const results = [];
|
|
26
|
+
try {
|
|
27
|
+
for (const entry of readdirSync(dir)) {
|
|
28
|
+
if (entry.startsWith('.') && entry !== '.') continue;
|
|
29
|
+
const full = join(dir, entry);
|
|
30
|
+
const stat = statSync(full);
|
|
31
|
+
if (stat.isDirectory()) {
|
|
32
|
+
if (!SKIP_DIRS.has(entry)) {
|
|
33
|
+
results.push(...walkJSFiles(full, rootDir));
|
|
34
|
+
}
|
|
35
|
+
} else if (SUPPORTED.has(extname(entry).toLowerCase())) {
|
|
36
|
+
results.push(full);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch { /* skip unreadable */ }
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Compact a single file — minify with preserved names
|
|
45
|
+
* @param {string} filePath
|
|
46
|
+
* @returns {Promise<{original: number, compacted: number}>}
|
|
47
|
+
*/
|
|
48
|
+
async function compactFile(filePath) {
|
|
49
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
50
|
+
const original = source.length;
|
|
51
|
+
|
|
52
|
+
if (!source.trim()) return { original: 0, compacted: 0 };
|
|
53
|
+
|
|
54
|
+
const result = await minify(source, {
|
|
55
|
+
compress: {
|
|
56
|
+
dead_code: true,
|
|
57
|
+
drop_console: false,
|
|
58
|
+
passes: 2,
|
|
59
|
+
},
|
|
60
|
+
mangle: false,
|
|
61
|
+
module: true,
|
|
62
|
+
output: {
|
|
63
|
+
beautify: false,
|
|
64
|
+
comments: false,
|
|
65
|
+
semicolons: true,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (result.error) throw result.error;
|
|
70
|
+
|
|
71
|
+
writeFileSync(filePath, result.code, 'utf-8');
|
|
72
|
+
return { original, compacted: result.code.length };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Beautify a single file — format with readable output
|
|
77
|
+
* @param {string} filePath
|
|
78
|
+
* @returns {Promise<{original: number, beautified: number}>}
|
|
79
|
+
*/
|
|
80
|
+
async function beautifyFile(filePath) {
|
|
81
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
82
|
+
const original = source.length;
|
|
83
|
+
|
|
84
|
+
if (!source.trim()) return { original: 0, beautified: 0 };
|
|
85
|
+
|
|
86
|
+
const result = await minify(source, {
|
|
87
|
+
compress: false,
|
|
88
|
+
mangle: false,
|
|
89
|
+
module: true,
|
|
90
|
+
output: {
|
|
91
|
+
beautify: true,
|
|
92
|
+
comments: false,
|
|
93
|
+
indent_level: 2,
|
|
94
|
+
semicolons: true,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (result.error) throw result.error;
|
|
99
|
+
|
|
100
|
+
writeFileSync(filePath, result.code + '\n', 'utf-8');
|
|
101
|
+
return { original, beautified: result.code.length };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Compact all JS files in a directory
|
|
106
|
+
* @param {string} dir - Directory to compact
|
|
107
|
+
* @param {Object} [options]
|
|
108
|
+
* @param {boolean} [options.dryRun=false] - Preview without writing
|
|
109
|
+
* @returns {Promise<{files: number, originalBytes: number, compactedBytes: number, savings: string}>}
|
|
110
|
+
*/
|
|
111
|
+
export async function compactProject(dir, options = {}) {
|
|
112
|
+
const { dryRun = false } = options;
|
|
113
|
+
const files = walkJSFiles(dir);
|
|
114
|
+
let totalOriginal = 0;
|
|
115
|
+
let totalCompacted = 0;
|
|
116
|
+
const processed = [];
|
|
117
|
+
const errors = [];
|
|
118
|
+
|
|
119
|
+
for (const filePath of files) {
|
|
120
|
+
const rel = relative(dir, filePath);
|
|
121
|
+
try {
|
|
122
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
123
|
+
totalOriginal += source.length;
|
|
124
|
+
|
|
125
|
+
if (!dryRun) {
|
|
126
|
+
const { compacted } = await compactFile(filePath);
|
|
127
|
+
totalCompacted += compacted;
|
|
128
|
+
} else {
|
|
129
|
+
const result = await minify(source, {
|
|
130
|
+
compress: { dead_code: true, drop_console: false, passes: 2 },
|
|
131
|
+
mangle: false,
|
|
132
|
+
module: true,
|
|
133
|
+
output: { beautify: false, comments: false },
|
|
134
|
+
});
|
|
135
|
+
totalCompacted += result.code?.length || source.length;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
processed.push(rel);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
errors.push({ file: rel, error: e.message });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const savings = totalOriginal > 0
|
|
145
|
+
? Math.round((1 - totalCompacted / totalOriginal) * 100)
|
|
146
|
+
: 0;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
files: processed.length,
|
|
150
|
+
fileList: processed,
|
|
151
|
+
originalBytes: totalOriginal,
|
|
152
|
+
compactedBytes: totalCompacted,
|
|
153
|
+
savings: `${savings}%`,
|
|
154
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
155
|
+
dryRun,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Beautify all JS files in a directory
|
|
161
|
+
* @param {string} dir - Directory to beautify
|
|
162
|
+
* @param {Object} [options]
|
|
163
|
+
* @param {boolean} [options.dryRun=false] - Preview without writing
|
|
164
|
+
* @returns {Promise<{files: number, originalBytes: number, beautifiedBytes: number}>}
|
|
165
|
+
*/
|
|
166
|
+
export async function expandProject(dir, options = {}) {
|
|
167
|
+
const { dryRun = false } = options;
|
|
168
|
+
const files = walkJSFiles(dir);
|
|
169
|
+
let totalOriginal = 0;
|
|
170
|
+
let totalBeautified = 0;
|
|
171
|
+
const processed = [];
|
|
172
|
+
const errors = [];
|
|
173
|
+
|
|
174
|
+
for (const filePath of files) {
|
|
175
|
+
const rel = relative(dir, filePath);
|
|
176
|
+
try {
|
|
177
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
178
|
+
totalOriginal += source.length;
|
|
179
|
+
|
|
180
|
+
if (!dryRun) {
|
|
181
|
+
const { beautified } = await beautifyFile(filePath);
|
|
182
|
+
totalBeautified += beautified;
|
|
183
|
+
} else {
|
|
184
|
+
const result = await minify(source, {
|
|
185
|
+
compress: false,
|
|
186
|
+
mangle: false,
|
|
187
|
+
module: true,
|
|
188
|
+
output: { beautify: true, comments: false, indent_level: 2 },
|
|
189
|
+
});
|
|
190
|
+
totalBeautified += result.code?.length || source.length;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
processed.push(rel);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
errors.push({ file: rel, error: e.message });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
files: processed.length,
|
|
201
|
+
fileList: processed,
|
|
202
|
+
originalBytes: totalOriginal,
|
|
203
|
+
beautifiedBytes: totalBeautified,
|
|
204
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
205
|
+
dryRun,
|
|
206
|
+
};
|
|
207
|
+
}
|
package/src/complexity.js
CHANGED
|
@@ -109,13 +109,12 @@ function getRating(complexity) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* Analyze complexity of file
|
|
113
|
-
* @param {string}
|
|
112
|
+
* Analyze complexity of a single file (per-file export for cache integration)
|
|
113
|
+
* @param {string} code - File source code
|
|
114
|
+
* @param {string} relPath - Relative path for reporting
|
|
114
115
|
* @returns {ComplexityItem[]}
|
|
115
116
|
*/
|
|
116
|
-
function
|
|
117
|
-
const code = readFileSync(filePath, 'utf-8');
|
|
118
|
-
const relPath = relative(rootDir, filePath);
|
|
117
|
+
export function analyzeComplexityFile(code, relPath) {
|
|
119
118
|
const items = [];
|
|
120
119
|
|
|
121
120
|
let ast;
|
|
@@ -140,11 +139,9 @@ function analyzeFile(filePath, rootDir) {
|
|
|
140
139
|
},
|
|
141
140
|
|
|
142
141
|
ArrowFunctionExpression(node) {
|
|
143
|
-
// Skip small arrow functions
|
|
144
142
|
if (node.body.type !== 'BlockStatement') return;
|
|
145
143
|
const complexity = calculateComplexity(node.body);
|
|
146
144
|
if (complexity > 5) {
|
|
147
|
-
// Only report complex arrow functions
|
|
148
145
|
items.push({
|
|
149
146
|
name: '(arrow)',
|
|
150
147
|
type: 'function',
|
|
@@ -174,6 +171,23 @@ function analyzeFile(filePath, rootDir) {
|
|
|
174
171
|
return items;
|
|
175
172
|
}
|
|
176
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Analyze complexity of file (internal, reads from disk)
|
|
176
|
+
* @param {string} filePath
|
|
177
|
+
* @param {string} rootDir
|
|
178
|
+
* @returns {ComplexityItem[]}
|
|
179
|
+
*/
|
|
180
|
+
function analyzeFile(filePath, rootDir) {
|
|
181
|
+
let code;
|
|
182
|
+
try {
|
|
183
|
+
code = readFileSync(filePath, 'utf-8');
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return []; // File deleted between findJSFiles and read
|
|
186
|
+
}
|
|
187
|
+
const relPath = relative(rootDir, filePath);
|
|
188
|
+
return analyzeComplexityFile(code, relPath);
|
|
189
|
+
}
|
|
190
|
+
|
|
177
191
|
/**
|
|
178
192
|
* Get complexity analysis for directory
|
|
179
193
|
* @param {string} dir
|