@unrdf/project-engine 5.0.1
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 +53 -0
- package/package.json +58 -0
- package/src/api-contract-validator.mjs +711 -0
- package/src/auto-test-generator.mjs +444 -0
- package/src/autonomic-mapek.mjs +511 -0
- package/src/capabilities-manifest.mjs +125 -0
- package/src/code-complexity-js.mjs +368 -0
- package/src/dependency-graph.mjs +276 -0
- package/src/doc-drift-checker.mjs +172 -0
- package/src/doc-generator.mjs +229 -0
- package/src/domain-infer.mjs +966 -0
- package/src/drift-snapshot.mjs +775 -0
- package/src/file-roles.mjs +94 -0
- package/src/fs-scan.mjs +305 -0
- package/src/gap-finder.mjs +376 -0
- package/src/golden-structure.mjs +149 -0
- package/src/hotspot-analyzer.mjs +412 -0
- package/src/index.mjs +151 -0
- package/src/initialize.mjs +957 -0
- package/src/lens/project-structure.mjs +74 -0
- package/src/mapek-orchestration.mjs +665 -0
- package/src/materialize-apply.mjs +505 -0
- package/src/materialize-plan.mjs +422 -0
- package/src/materialize.mjs +137 -0
- package/src/policy-derivation.mjs +869 -0
- package/src/project-config.mjs +142 -0
- package/src/project-diff.mjs +28 -0
- package/src/project-engine/build-utils.mjs +237 -0
- package/src/project-engine/code-analyzer.mjs +248 -0
- package/src/project-engine/doc-generator.mjs +407 -0
- package/src/project-engine/infrastructure.mjs +213 -0
- package/src/project-engine/metrics.mjs +146 -0
- package/src/project-model.mjs +111 -0
- package/src/project-report.mjs +348 -0
- package/src/refactoring-guide.mjs +242 -0
- package/src/stack-detect.mjs +102 -0
- package/src/stack-linter.mjs +213 -0
- package/src/template-infer.mjs +674 -0
- package/src/type-auditor.mjs +609 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Project engine configuration - central config for all capabilities
|
|
3
|
+
* @module project-engine/project-config
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Full project engine configuration schema
|
|
10
|
+
*/
|
|
11
|
+
export const ProjectEngineConfigSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
// Filesystem scanning
|
|
14
|
+
fs: z
|
|
15
|
+
.object({
|
|
16
|
+
ignorePatterns: z.array(z.string()).optional(),
|
|
17
|
+
baseIri: z.string().default('http://example.org/unrdf/fs#'),
|
|
18
|
+
scanHiddenFiles: z.boolean().default(false),
|
|
19
|
+
})
|
|
20
|
+
.optional(),
|
|
21
|
+
|
|
22
|
+
// Project modeling
|
|
23
|
+
project: z
|
|
24
|
+
.object({
|
|
25
|
+
conventions: z
|
|
26
|
+
.object({
|
|
27
|
+
sourcePaths: z.array(z.string()).default(['src']),
|
|
28
|
+
featurePaths: z.array(z.string()).default(['features', 'modules']),
|
|
29
|
+
testPaths: z.array(z.string()).default(['__tests__', 'test', 'tests']),
|
|
30
|
+
})
|
|
31
|
+
.optional(),
|
|
32
|
+
baseIri: z.string().default('http://example.org/unrdf/project#'),
|
|
33
|
+
})
|
|
34
|
+
.optional(),
|
|
35
|
+
|
|
36
|
+
// Golden structure
|
|
37
|
+
golden: z
|
|
38
|
+
.object({
|
|
39
|
+
profile: z
|
|
40
|
+
.enum([
|
|
41
|
+
'react-feature-v1',
|
|
42
|
+
'next-app-router-v1',
|
|
43
|
+
'next-pages-v1',
|
|
44
|
+
'nest-api-v1',
|
|
45
|
+
'express-api-v1',
|
|
46
|
+
])
|
|
47
|
+
.optional(),
|
|
48
|
+
loadFromPath: z.string().optional(),
|
|
49
|
+
})
|
|
50
|
+
.optional(),
|
|
51
|
+
|
|
52
|
+
// Diff/comparison
|
|
53
|
+
diff: z
|
|
54
|
+
.object({
|
|
55
|
+
structureLens: z.literal('project-structure').default('project-structure'),
|
|
56
|
+
transactionLens: z.string().optional(),
|
|
57
|
+
})
|
|
58
|
+
.optional(),
|
|
59
|
+
|
|
60
|
+
// Materialization
|
|
61
|
+
materialize: z
|
|
62
|
+
.object({
|
|
63
|
+
templateConfig: z.record(z.string(), z.any()).optional(),
|
|
64
|
+
outputRoot: z.string().default('.'),
|
|
65
|
+
dryRun: z.boolean().default(false),
|
|
66
|
+
})
|
|
67
|
+
.optional(),
|
|
68
|
+
|
|
69
|
+
// Observability
|
|
70
|
+
observability: z
|
|
71
|
+
.object({
|
|
72
|
+
enableTracing: z.boolean().default(true),
|
|
73
|
+
enableMetrics: z.boolean().default(true),
|
|
74
|
+
})
|
|
75
|
+
.optional(),
|
|
76
|
+
})
|
|
77
|
+
.strict();
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get project engine configuration from environment + defaults
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} [overrides] - Config overrides
|
|
83
|
+
* @returns {Object} Validated configuration
|
|
84
|
+
*/
|
|
85
|
+
export function getProjectEngineConfig(overrides = {}) {
|
|
86
|
+
const defaults = {
|
|
87
|
+
fs: {
|
|
88
|
+
ignorePatterns: ['node_modules', '.git', 'dist', 'build', '.next', '.turbo', 'coverage'],
|
|
89
|
+
baseIri: 'http://example.org/unrdf/fs#',
|
|
90
|
+
},
|
|
91
|
+
project: {
|
|
92
|
+
conventions: {
|
|
93
|
+
sourcePaths: ['src'],
|
|
94
|
+
featurePaths: ['features', 'modules'],
|
|
95
|
+
testPaths: ['__tests__', 'test', 'tests', 'spec'],
|
|
96
|
+
},
|
|
97
|
+
baseIri: 'http://example.org/unrdf/project#',
|
|
98
|
+
},
|
|
99
|
+
golden: {
|
|
100
|
+
profile: 'react-feature-v1',
|
|
101
|
+
},
|
|
102
|
+
diff: {
|
|
103
|
+
structureLens: 'project-structure',
|
|
104
|
+
},
|
|
105
|
+
materialize: {
|
|
106
|
+
outputRoot: '.',
|
|
107
|
+
dryRun: false,
|
|
108
|
+
},
|
|
109
|
+
observability: {
|
|
110
|
+
enableTracing: true,
|
|
111
|
+
enableMetrics: true,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Merge overrides
|
|
116
|
+
const merged = deepMerge(defaults, overrides);
|
|
117
|
+
|
|
118
|
+
// Validate
|
|
119
|
+
return ProjectEngineConfigSchema.parse(merged);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Deep merge objects for configuration
|
|
124
|
+
*
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
function deepMerge(target, source) {
|
|
128
|
+
const result = { ...target };
|
|
129
|
+
|
|
130
|
+
for (const key in source) {
|
|
131
|
+
if (Array.isArray(source[key]) && Array.isArray(result[key])) {
|
|
132
|
+
// Merge arrays by combining them
|
|
133
|
+
result[key] = [...result[key], ...source[key]];
|
|
134
|
+
} else if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
135
|
+
result[key] = deepMerge(result[key] || {}, source[key]);
|
|
136
|
+
} else {
|
|
137
|
+
result[key] = source[key];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Project structure diff - convenience wrapper over diff.mjs
|
|
3
|
+
* @module project-engine/project-diff
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { diffOntologyFromStores } from '../diff.mjs';
|
|
7
|
+
import { ProjectStructureLens } from './lens/project-structure.mjs';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
const ProjectDiffOptionsSchema = z.object({
|
|
11
|
+
actualStore: z.object({}).passthrough(),
|
|
12
|
+
goldenStore: z.object({}).passthrough(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compute project structure diff using low-level diff.mjs
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} options
|
|
19
|
+
* @param {Store} options.actualStore - Current project graph
|
|
20
|
+
* @param {Store} options.goldenStore - Expected golden structure
|
|
21
|
+
* @returns {OntologyDiff}
|
|
22
|
+
*/
|
|
23
|
+
export function diffProjectStructure(options) {
|
|
24
|
+
const validated = ProjectDiffOptionsSchema.parse(options);
|
|
25
|
+
const { actualStore, goldenStore } = validated;
|
|
26
|
+
|
|
27
|
+
return diffOntologyFromStores(goldenStore, actualStore, ProjectStructureLens);
|
|
28
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Build Utilities - Package building and verification
|
|
3
|
+
* @module @unrdf/project-engine/build-utils
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile, readdir, access, stat } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Package verification schema
|
|
12
|
+
*/
|
|
13
|
+
const VerificationResultSchema = z.object({
|
|
14
|
+
valid: z.boolean(),
|
|
15
|
+
errors: z.array(z.string()),
|
|
16
|
+
warnings: z.array(z.string()),
|
|
17
|
+
checkedFiles: z.array(z.string()),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build single package
|
|
22
|
+
* @param {string} packagePath - Path to package directory
|
|
23
|
+
* @returns {Promise<Object>} Build result
|
|
24
|
+
*
|
|
25
|
+
* @throws {TypeError} If packagePath is not a string
|
|
26
|
+
* @throws {Error} If build fails
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const result = await buildPackage('./packages/core');
|
|
30
|
+
* console.log('Build status:', result.success);
|
|
31
|
+
*/
|
|
32
|
+
export async function buildPackage(packagePath) {
|
|
33
|
+
if (typeof packagePath !== 'string') {
|
|
34
|
+
throw new TypeError('buildPackage: packagePath must be a string');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const { execFile } = await import('node:child_process');
|
|
39
|
+
const { promisify } = await import('node:util');
|
|
40
|
+
const execFileAsync = promisify(execFile);
|
|
41
|
+
|
|
42
|
+
// Check if package has build script
|
|
43
|
+
const packageJsonPath = join(packagePath, 'package.json');
|
|
44
|
+
const packageJsonContent = await readFile(packageJsonPath, 'utf-8');
|
|
45
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
46
|
+
|
|
47
|
+
if (!packageJson.scripts || !packageJson.scripts.build) {
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
message: 'No build script defined',
|
|
51
|
+
output: '',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Run build script
|
|
56
|
+
const { stdout, stderr } = await execFileAsync('pnpm', ['run', 'build'], {
|
|
57
|
+
cwd: packagePath,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
success: true,
|
|
62
|
+
message: 'Build completed successfully',
|
|
63
|
+
output: stdout,
|
|
64
|
+
errors: stderr,
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(`buildPackage failed: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Verify package integrity
|
|
73
|
+
* @param {string} packagePath - Path to package directory
|
|
74
|
+
* @returns {Promise<Object>} Verification result with errors/warnings
|
|
75
|
+
*
|
|
76
|
+
* @throws {TypeError} If packagePath is not a string
|
|
77
|
+
* @throws {Error} If verification fails
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* const result = await verifyPackage('./packages/core');
|
|
81
|
+
* console.log('Valid:', result.valid);
|
|
82
|
+
* console.log('Errors:', result.errors);
|
|
83
|
+
*/
|
|
84
|
+
export async function verifyPackage(packagePath) {
|
|
85
|
+
if (typeof packagePath !== 'string') {
|
|
86
|
+
throw new TypeError('verifyPackage: packagePath must be a string');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const errors = [];
|
|
90
|
+
const warnings = [];
|
|
91
|
+
const checkedFiles = [];
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Check package.json exists
|
|
95
|
+
const packageJsonPath = join(packagePath, 'package.json');
|
|
96
|
+
try {
|
|
97
|
+
await access(packageJsonPath);
|
|
98
|
+
checkedFiles.push('package.json');
|
|
99
|
+
|
|
100
|
+
const content = await readFile(packageJsonPath, 'utf-8');
|
|
101
|
+
const packageJson = JSON.parse(content);
|
|
102
|
+
|
|
103
|
+
// Validate required fields
|
|
104
|
+
if (!packageJson.name) {
|
|
105
|
+
errors.push('package.json missing "name" field');
|
|
106
|
+
}
|
|
107
|
+
if (!packageJson.version) {
|
|
108
|
+
errors.push('package.json missing "version" field');
|
|
109
|
+
}
|
|
110
|
+
if (!packageJson.type || packageJson.type !== 'module') {
|
|
111
|
+
errors.push('package.json must have "type": "module"');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check exports field
|
|
115
|
+
if (!packageJson.exports) {
|
|
116
|
+
warnings.push('package.json missing "exports" field');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Verify main entry point exists
|
|
120
|
+
if (packageJson.main) {
|
|
121
|
+
const mainPath = join(packagePath, packageJson.main);
|
|
122
|
+
try {
|
|
123
|
+
await access(mainPath);
|
|
124
|
+
checkedFiles.push(packageJson.main);
|
|
125
|
+
} catch {
|
|
126
|
+
errors.push(`Main entry point not found: ${packageJson.main}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check for README
|
|
131
|
+
const readmePath = join(packagePath, 'README.md');
|
|
132
|
+
try {
|
|
133
|
+
await access(readmePath);
|
|
134
|
+
checkedFiles.push('README.md');
|
|
135
|
+
} catch {
|
|
136
|
+
warnings.push('README.md not found');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check for LICENSE
|
|
140
|
+
const licensePath = join(packagePath, 'LICENSE');
|
|
141
|
+
try {
|
|
142
|
+
await access(licensePath);
|
|
143
|
+
checkedFiles.push('LICENSE');
|
|
144
|
+
} catch {
|
|
145
|
+
warnings.push('LICENSE file not found');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check src directory exists
|
|
149
|
+
const srcPath = join(packagePath, 'src');
|
|
150
|
+
try {
|
|
151
|
+
const srcStat = await stat(srcPath);
|
|
152
|
+
if (!srcStat.isDirectory()) {
|
|
153
|
+
errors.push('src must be a directory');
|
|
154
|
+
}
|
|
155
|
+
checkedFiles.push('src/');
|
|
156
|
+
} catch {
|
|
157
|
+
errors.push('src directory not found');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check test directory
|
|
161
|
+
const testPath = join(packagePath, 'test');
|
|
162
|
+
try {
|
|
163
|
+
const testStat = await stat(testPath);
|
|
164
|
+
if (!testStat.isDirectory()) {
|
|
165
|
+
warnings.push('test should be a directory');
|
|
166
|
+
}
|
|
167
|
+
checkedFiles.push('test/');
|
|
168
|
+
} catch {
|
|
169
|
+
warnings.push('test directory not found');
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
errors.push('package.json not found or invalid JSON');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const result = {
|
|
176
|
+
valid: errors.length === 0,
|
|
177
|
+
errors,
|
|
178
|
+
warnings,
|
|
179
|
+
checkedFiles,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return VerificationResultSchema.parse(result);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
throw new Error(`verifyPackage failed: ${error.message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* List all packages in monorepo
|
|
190
|
+
* @param {string} [monorepoPath='.'] - Path to monorepo root
|
|
191
|
+
* @returns {Promise<Array<Object>>} List of packages with metadata
|
|
192
|
+
*
|
|
193
|
+
* @throws {TypeError} If monorepoPath is not a string
|
|
194
|
+
* @throws {Error} If monorepo cannot be scanned
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* const packages = await listPackages();
|
|
198
|
+
* packages.forEach(pkg => console.log(pkg.name, pkg.path));
|
|
199
|
+
*/
|
|
200
|
+
export async function listPackages(monorepoPath = '.') {
|
|
201
|
+
if (typeof monorepoPath !== 'string') {
|
|
202
|
+
throw new TypeError('listPackages: monorepoPath must be a string');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const packagesPath = join(monorepoPath, 'packages');
|
|
207
|
+
const entries = await readdir(packagesPath, { withFileTypes: true });
|
|
208
|
+
|
|
209
|
+
const packages = [];
|
|
210
|
+
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (entry.isDirectory()) {
|
|
213
|
+
const packagePath = join(packagesPath, entry.name);
|
|
214
|
+
const packageJsonPath = join(packagePath, 'package.json');
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const content = await readFile(packageJsonPath, 'utf-8');
|
|
218
|
+
const packageJson = JSON.parse(content);
|
|
219
|
+
|
|
220
|
+
packages.push({
|
|
221
|
+
name: packageJson.name,
|
|
222
|
+
version: packageJson.version,
|
|
223
|
+
description: packageJson.description || '',
|
|
224
|
+
path: packagePath,
|
|
225
|
+
private: packageJson.private || false,
|
|
226
|
+
});
|
|
227
|
+
} catch {
|
|
228
|
+
// Skip invalid packages
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return packages;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw new Error(`listPackages failed: ${error.message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Code Analyzer - Package quality metrics and analysis
|
|
3
|
+
* @module @unrdf/project-engine/code-analyzer
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Package analysis schema
|
|
12
|
+
*/
|
|
13
|
+
const PackageAnalysisSchema = z.object({
|
|
14
|
+
name: z.string(),
|
|
15
|
+
linesOfCode: z.number(),
|
|
16
|
+
fileCount: z.number(),
|
|
17
|
+
exportCount: z.number(),
|
|
18
|
+
testCoverage: z.number().min(0).max(100),
|
|
19
|
+
dependencies: z.array(z.string()),
|
|
20
|
+
devDependencies: z.array(z.string()),
|
|
21
|
+
publicApis: z.array(z.string()),
|
|
22
|
+
complexity: z.enum(['low', 'medium', 'high']),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Analyze package quality metrics
|
|
27
|
+
* @param {string} packagePath - Path to package directory
|
|
28
|
+
* @returns {Promise<Object>} Package analysis results
|
|
29
|
+
*
|
|
30
|
+
* @throws {TypeError} If packagePath is not a string
|
|
31
|
+
* @throws {Error} If package cannot be analyzed
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const analysis = await analyzePackage('./packages/core');
|
|
35
|
+
* console.log('Lines of code:', analysis.linesOfCode);
|
|
36
|
+
* console.log('Test coverage:', analysis.testCoverage);
|
|
37
|
+
*/
|
|
38
|
+
export async function analyzePackage(packagePath) {
|
|
39
|
+
if (typeof packagePath !== 'string') {
|
|
40
|
+
throw new TypeError('analyzePackage: packagePath must be a string');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Read package.json
|
|
45
|
+
const packageJsonPath = join(packagePath, 'package.json');
|
|
46
|
+
const packageJsonContent = await readFile(packageJsonPath, 'utf-8');
|
|
47
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
48
|
+
|
|
49
|
+
// Count lines of code
|
|
50
|
+
const srcPath = join(packagePath, 'src');
|
|
51
|
+
const sourceFiles = await findFiles(srcPath, '.mjs');
|
|
52
|
+
let linesOfCode = 0;
|
|
53
|
+
|
|
54
|
+
for (const file of sourceFiles) {
|
|
55
|
+
const content = await readFile(file, 'utf-8');
|
|
56
|
+
linesOfCode += content.split('\n').length;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Find all exports
|
|
60
|
+
const publicApis = [];
|
|
61
|
+
for (const file of sourceFiles) {
|
|
62
|
+
const exports = await findExports(file);
|
|
63
|
+
publicApis.push(...exports);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Estimate test coverage
|
|
67
|
+
const testPath = join(packagePath, 'test');
|
|
68
|
+
const testFiles = await findFiles(testPath, '.test.mjs');
|
|
69
|
+
const testCoverage = await estimateTestCoverage(sourceFiles, testFiles);
|
|
70
|
+
|
|
71
|
+
// Determine complexity
|
|
72
|
+
const complexity = determineComplexity(linesOfCode, publicApis.length, sourceFiles.length);
|
|
73
|
+
|
|
74
|
+
const analysis = {
|
|
75
|
+
name: packageJson.name,
|
|
76
|
+
linesOfCode,
|
|
77
|
+
fileCount: sourceFiles.length,
|
|
78
|
+
exportCount: publicApis.length,
|
|
79
|
+
testCoverage,
|
|
80
|
+
dependencies: Object.keys(packageJson.dependencies || {}),
|
|
81
|
+
devDependencies: Object.keys(packageJson.devDependencies || {}),
|
|
82
|
+
publicApis,
|
|
83
|
+
complexity,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return PackageAnalysisSchema.parse(analysis);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new Error(`analyzePackage failed: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Find all exports from a module
|
|
94
|
+
* @param {string} filePath - Path to module file
|
|
95
|
+
* @returns {Promise<Array<string>>} List of exported identifiers
|
|
96
|
+
*
|
|
97
|
+
* @throws {TypeError} If filePath is not a string
|
|
98
|
+
* @throws {Error} If file cannot be read
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* const exports = await findExports('./src/index.mjs');
|
|
102
|
+
* console.log('Exports:', exports);
|
|
103
|
+
*/
|
|
104
|
+
export async function findExports(filePath) {
|
|
105
|
+
if (typeof filePath !== 'string') {
|
|
106
|
+
throw new TypeError('findExports: filePath must be a string');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const content = await readFile(filePath, 'utf-8');
|
|
111
|
+
const exports = [];
|
|
112
|
+
|
|
113
|
+
// Named exports: export function/const/let/class name
|
|
114
|
+
const namedMatches = content.matchAll(
|
|
115
|
+
/export\s+(async\s+)?(function|const|let|class)\s+(\w+)/g
|
|
116
|
+
);
|
|
117
|
+
for (const match of namedMatches) {
|
|
118
|
+
exports.push(match[3]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Export { name1, name2 }
|
|
122
|
+
const braceMatches = content.matchAll(/export\s+\{([^}]+)\}/g);
|
|
123
|
+
for (const match of braceMatches) {
|
|
124
|
+
const names = match[1].split(',').map(n => n.trim().split(/\s+as\s+/)[0]);
|
|
125
|
+
exports.push(...names);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return exports;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new Error(`findExports failed: ${error.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Estimate test coverage percentage
|
|
136
|
+
* @param {Array<string>} sourceFiles - Source file paths
|
|
137
|
+
* @param {Array<string>} testFiles - Test file paths
|
|
138
|
+
* @returns {Promise<number>} Estimated coverage percentage
|
|
139
|
+
*
|
|
140
|
+
* @throws {TypeError} If arguments are not arrays
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* const coverage = await countCoverage(['src/a.mjs'], ['test/a.test.mjs']);
|
|
144
|
+
* console.log('Coverage:', coverage + '%');
|
|
145
|
+
*/
|
|
146
|
+
export async function countCoverage(sourceFiles, testFiles) {
|
|
147
|
+
if (!Array.isArray(sourceFiles)) {
|
|
148
|
+
throw new TypeError('countCoverage: sourceFiles must be an array');
|
|
149
|
+
}
|
|
150
|
+
if (!Array.isArray(testFiles)) {
|
|
151
|
+
throw new TypeError('countCoverage: testFiles must be an array');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return estimateTestCoverage(sourceFiles, testFiles);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Find files with specific extension
|
|
159
|
+
* @param {string} dirPath - Directory path
|
|
160
|
+
* @param {string} extension - File extension
|
|
161
|
+
* @returns {Promise<Array<string>>} List of file paths
|
|
162
|
+
*/
|
|
163
|
+
async function findFiles(dirPath, extension) {
|
|
164
|
+
const files = [];
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
168
|
+
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
const fullPath = join(dirPath, entry.name);
|
|
171
|
+
|
|
172
|
+
if (entry.isDirectory()) {
|
|
173
|
+
const subFiles = await findFiles(fullPath, extension);
|
|
174
|
+
files.push(...subFiles);
|
|
175
|
+
} else if (entry.isFile() && entry.name.endsWith(extension)) {
|
|
176
|
+
files.push(fullPath);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
// Directory doesn't exist
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return files;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Estimate test coverage heuristically
|
|
188
|
+
* @param {Array<string>} sourceFiles - Source file paths
|
|
189
|
+
* @param {Array<string>} testFiles - Test file paths
|
|
190
|
+
* @returns {Promise<number>} Coverage percentage
|
|
191
|
+
*/
|
|
192
|
+
async function estimateTestCoverage(sourceFiles, testFiles) {
|
|
193
|
+
if (sourceFiles.length === 0) {
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (testFiles.length === 0) {
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Heuristic: ratio of test files to source files
|
|
202
|
+
const ratio = testFiles.length / sourceFiles.length;
|
|
203
|
+
|
|
204
|
+
// Count test assertions as proxy for coverage
|
|
205
|
+
let totalAssertions = 0;
|
|
206
|
+
for (const testFile of testFiles) {
|
|
207
|
+
const content = await readFile(testFile, 'utf-8');
|
|
208
|
+
const assertionCount = (content.match(/expect\(/g) || []).length;
|
|
209
|
+
totalAssertions += assertionCount;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Count source functions
|
|
213
|
+
let totalFunctions = 0;
|
|
214
|
+
for (const srcFile of sourceFiles) {
|
|
215
|
+
const content = await readFile(srcFile, 'utf-8');
|
|
216
|
+
const functionCount = (content.match(/export\s+(async\s+)?function/g) || []).length;
|
|
217
|
+
totalFunctions += functionCount;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (totalFunctions === 0) {
|
|
221
|
+
return ratio * 50; // Base estimate
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Coverage estimate: assertions per function
|
|
225
|
+
const assertionsPerFunction = totalAssertions / totalFunctions;
|
|
226
|
+
const coverageEstimate = Math.min(assertionsPerFunction * 30, 100);
|
|
227
|
+
|
|
228
|
+
return Math.round(coverageEstimate);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Determine package complexity
|
|
233
|
+
* @param {number} linesOfCode - Total lines of code
|
|
234
|
+
* @param {number} exportCount - Number of exports
|
|
235
|
+
* @param {number} fileCount - Number of files
|
|
236
|
+
* @returns {string} Complexity level
|
|
237
|
+
*/
|
|
238
|
+
function determineComplexity(linesOfCode, exportCount, fileCount) {
|
|
239
|
+
const score = linesOfCode / 100 + exportCount * 2 + fileCount * 5;
|
|
240
|
+
|
|
241
|
+
if (score < 50) {
|
|
242
|
+
return 'low';
|
|
243
|
+
} else if (score < 200) {
|
|
244
|
+
return 'medium';
|
|
245
|
+
} else {
|
|
246
|
+
return 'high';
|
|
247
|
+
}
|
|
248
|
+
}
|