entkapp 4.5.0 → 5.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/bin/cli.js +4 -4
- package/docs.zip +0 -0
- package/entkapp/config.json +0 -1
- package/package.json +5 -6
- package/src/EngineContext.js +276 -78
- package/src/analyzers/OxcAnalyzer.js +8 -380
- package/src/api/HeadlessAPI.js +1 -1
- package/src/api/PluginSDK.js +23 -187
- package/src/ast/ASTAnalyzer.js +467 -253
- package/src/ast/AdvancedAnalysis.js +6 -5
- package/src/ast/BarrelParser.js +23 -16
- package/src/ast/DeadCodeDetector.js +30 -18
- package/src/ast/MagicDetector.js +1 -1
- package/src/ast/OxcAnalyzer.js +328 -264
- package/src/index.js +272 -361
- package/src/performance/GraphCache.js +21 -2
- package/src/performance/WorkerPool.js +11 -1
- package/src/performance/WorkerTaskRunner.js +72 -25
- package/src/plugins/PluginRegistry.js +5 -16
- package/src/plugins/ecosystems/GenericPlugins.js +61 -0
- package/src/refractor/TransactionManager.js +3 -136
- package/src/refractor/TypeIntegrity.js +2 -73
- package/src/resolution/CircularDetector.js +27 -66
- package/src/resolution/ConfigLoader.js +2 -85
- package/src/resolution/DepencyResolver.js +20 -124
- package/src/resolution/EntryPointDetector.js +134 -0
- package/src/resolution/PathMapper.js +3 -123
- package/src/resolution/TSConfigLoader.js +76 -0
- package/src/resolution/WorkSpaceGraph.js +4 -473
- package/src/resolution/WorkspaceDiagnostic.js +3 -57
- package/src/plugins/KnipAdapter.js +0 -106
|
@@ -1,474 +1,5 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import fsSync from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Monorepo Cross-Linking Topology Manager
|
|
7
|
-
* Maps sub-package structural boundaries across pnpm, Yarn, or npm workspaces.
|
|
8
|
-
*
|
|
9
|
-
* Improvements over v1:
|
|
10
|
-
* - Auto-activates workspace mode when workspace config is detected (no manual flag required)
|
|
11
|
-
* - Supports deeper glob patterns beyond simple `packages/*` (e.g. `apps/*`, `libs/**`)
|
|
12
|
-
* - Correctly registers workspace package names as "used" so they are never flagged as unused deps
|
|
13
|
-
* - Handles Bun workspaces (workspaces array in package.json)
|
|
14
|
-
* - Resolves subpath imports for workspace packages (e.g. `@scope/pkg/utils`)
|
|
15
|
-
* - Exposes `markWorkspacePackagesAsUsed()` so the engine can call it after dep audit
|
|
16
|
-
*/
|
|
17
1
|
export class WorkspaceGraph {
|
|
18
|
-
constructor(context) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Checks the environment layout to discover and map local workspace packages.
|
|
26
|
-
* This method is idempotent and safe to call multiple times.
|
|
27
|
-
*/
|
|
28
|
-
async initializeWorkspaceMesh() {
|
|
29
|
-
const rootPackageJsonPath = path.join(this.context.cwd, 'package.json');
|
|
30
|
-
const pnpmWorkspacePath = path.join(this.context.cwd, 'pnpm-workspace.yaml');
|
|
31
|
-
|
|
32
|
-
let workspaceGlobs = [];
|
|
33
|
-
this.hoistedDependencies = new Set();
|
|
34
|
-
|
|
35
|
-
// Load hoisted dependencies from root package.json
|
|
36
|
-
try {
|
|
37
|
-
const rootPkg = JSON.parse(await fs.readFile(rootPackageJsonPath, 'utf8'));
|
|
38
|
-
const deps = { ...rootPkg.dependencies, ...rootPkg.devDependencies };
|
|
39
|
-
Object.keys(deps).forEach(d => this.hoistedDependencies.add(d));
|
|
40
|
-
} catch (e) {
|
|
41
|
-
// No root package.json or unreadable
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Protocol A: Check for pnpm workspace configurations (pnpm-workspace.yaml)
|
|
45
|
-
try {
|
|
46
|
-
const yaml = await fs.readFile(pnpmWorkspacePath, 'utf8');
|
|
47
|
-
const lines = yaml.split('\n');
|
|
48
|
-
let insidePackagesBlock = false;
|
|
49
|
-
|
|
50
|
-
for (const line of lines) {
|
|
51
|
-
const trimmed = line.trim();
|
|
52
|
-
if (trimmed === 'packages:') {
|
|
53
|
-
insidePackagesBlock = true;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (insidePackagesBlock) {
|
|
57
|
-
if (trimmed.startsWith('-')) {
|
|
58
|
-
const pattern = trimmed.replace(/^-\s*/, '').replace(/['"]/g, '').trim();
|
|
59
|
-
if (pattern) workspaceGlobs.push(pattern);
|
|
60
|
-
} else if (trimmed && !trimmed.startsWith('#')) {
|
|
61
|
-
// Another top-level key encountered – stop reading packages block
|
|
62
|
-
insidePackagesBlock = false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
} catch {
|
|
67
|
-
// pnpm structure absent; check package.json workspace array paths instead
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Protocol B: Check for Yarn/npm/Bun workspaces array inside the root package.json
|
|
71
|
-
if (workspaceGlobs.length === 0) {
|
|
72
|
-
try {
|
|
73
|
-
const pkgText = await fs.readFile(rootPackageJsonPath, 'utf8');
|
|
74
|
-
const pkg = JSON.parse(pkgText);
|
|
75
|
-
if (pkg.workspaces) {
|
|
76
|
-
workspaceGlobs = Array.isArray(pkg.workspaces)
|
|
77
|
-
? pkg.workspaces
|
|
78
|
-
: (pkg.workspaces.packages || []);
|
|
79
|
-
}
|
|
80
|
-
} catch {
|
|
81
|
-
// No workspaces found
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (workspaceGlobs.length > 0) {
|
|
86
|
-
this.context.isWorkspaceEnabled = true;
|
|
87
|
-
if (this.context.verbose) {
|
|
88
|
-
console.log(`🌐 Auto-detected monorepo layout with ${workspaceGlobs.length} glob patterns.`);
|
|
89
|
-
}
|
|
90
|
-
} else if (this.context.isWorkspaceEnabled) {
|
|
91
|
-
// Force enabled via flag but no patterns found; default to standard packages/*
|
|
92
|
-
workspaceGlobs = ['packages/*'];
|
|
93
|
-
if (this.context.verbose) {
|
|
94
|
-
console.log(`🌐 Workspace mode forced via flag. Using default patterns: ${workspaceGlobs.join(', ')}`);
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
return; // Workspace mesh maps skipped for single-package targets
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Crawl target glob configurations to locate workspace packages
|
|
101
|
-
for (const pattern of workspaceGlobs) {
|
|
102
|
-
await this.locatePackagesViaPattern(pattern);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Register all discovered workspace packages as "used" external packages so they
|
|
106
|
-
// are never incorrectly flagged as unused dependencies.
|
|
107
|
-
this.markWorkspacePackagesAsUsed();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Expands a workspace glob pattern and registers all found packages.
|
|
112
|
-
* Supports patterns like:
|
|
113
|
-
* - `packages/*` (one level deep)
|
|
114
|
-
* - `apps/*` (one level deep)
|
|
115
|
-
* - `packages/**` (recursive – all subdirectories)
|
|
116
|
-
* - `packages/core` (explicit single package)
|
|
117
|
-
*/
|
|
118
|
-
async locatePackagesViaPattern(globPattern) {
|
|
119
|
-
const standardizedPattern = globPattern.replace(/\\/g, '/');
|
|
120
|
-
|
|
121
|
-
// Determine if this is a recursive pattern (`**`) or a simple wildcard (`*`)
|
|
122
|
-
const isRecursive = standardizedPattern.includes('**');
|
|
123
|
-
const isWildcard = standardizedPattern.includes('*');
|
|
124
|
-
|
|
125
|
-
if (!isWildcard) {
|
|
126
|
-
// Explicit path: treat the pattern itself as a single package directory
|
|
127
|
-
const absolutePath = path.resolve(this.context.cwd, standardizedPattern);
|
|
128
|
-
await this._tryRegisterPackage(absolutePath);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Extract the base directory before the first wildcard segment
|
|
133
|
-
const baseDir = standardizedPattern.split('/*')[0];
|
|
134
|
-
const absoluteSearchPath = path.resolve(this.context.cwd, baseDir);
|
|
135
|
-
|
|
136
|
-
if (isRecursive) {
|
|
137
|
-
await this._scanDirectoryRecursively(absoluteSearchPath);
|
|
138
|
-
} else {
|
|
139
|
-
await this._scanDirectoryShallow(absoluteSearchPath);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Scans a directory one level deep for workspace packages.
|
|
145
|
-
*/
|
|
146
|
-
async _scanDirectoryShallow(absoluteSearchPath) {
|
|
147
|
-
try {
|
|
148
|
-
const contents = await fs.readdir(absoluteSearchPath, { withFileTypes: true });
|
|
149
|
-
for (const entity of contents) {
|
|
150
|
-
if (!entity.isDirectory()) continue;
|
|
151
|
-
const subPackageDir = path.join(absoluteSearchPath, entity.name);
|
|
152
|
-
await this._tryRegisterPackage(subPackageDir);
|
|
153
|
-
}
|
|
154
|
-
} catch {
|
|
155
|
-
// Unreadable target directories; pass tracking
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Recursively scans a directory tree for workspace packages.
|
|
161
|
-
* Stops descending into `node_modules` directories.
|
|
162
|
-
*/
|
|
163
|
-
async _scanDirectoryRecursively(absoluteSearchPath) {
|
|
164
|
-
try {
|
|
165
|
-
const contents = await fs.readdir(absoluteSearchPath, { withFileTypes: true });
|
|
166
|
-
for (const entity of contents) {
|
|
167
|
-
if (!entity.isDirectory()) continue;
|
|
168
|
-
if (entity.name === 'node_modules' || entity.name === '.git') continue;
|
|
169
|
-
const subDir = path.join(absoluteSearchPath, entity.name);
|
|
170
|
-
// Try to register as a package first
|
|
171
|
-
const registered = await this._tryRegisterPackage(subDir);
|
|
172
|
-
// If not a package root itself, recurse deeper
|
|
173
|
-
if (!registered) {
|
|
174
|
-
await this._scanDirectoryRecursively(subDir);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
} catch {
|
|
178
|
-
// Unreadable directories; pass
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Attempts to register a directory as a workspace package.
|
|
184
|
-
* Dynamically evaluates companion tsconfig.json configurations for local build boundaries.
|
|
185
|
-
* Returns true if a valid package.json with a `name` field was found.
|
|
186
|
-
*/
|
|
187
|
-
async _tryRegisterPackage(packageDir) {
|
|
188
|
-
const manifestFile = path.join(packageDir, 'package.json');
|
|
189
|
-
const tsconfigFile = path.join(packageDir, 'tsconfig.json');
|
|
190
|
-
|
|
191
|
-
try {
|
|
192
|
-
const data = await fs.readFile(manifestFile, 'utf8');
|
|
193
|
-
const pkg = JSON.parse(data);
|
|
194
|
-
|
|
195
|
-
if (pkg.name) {
|
|
196
|
-
// Attempt to safely load compilation map configurations from the local tsconfig.json
|
|
197
|
-
let localTsconfig = null;
|
|
198
|
-
try {
|
|
199
|
-
let tsconfigText = await fs.readFile(tsconfigFile, 'utf8');
|
|
200
|
-
|
|
201
|
-
// Clean up multi-line comments, single-line comments, and trailing commas
|
|
202
|
-
tsconfigText = tsconfigText
|
|
203
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
204
|
-
.replace(/\/\/.*/g, '')
|
|
205
|
-
.replace(/,(\s*[}\]])/g, '$1');
|
|
206
|
-
|
|
207
|
-
localTsconfig = JSON.parse(tsconfigText);
|
|
208
|
-
} catch {
|
|
209
|
-
// No local tsconfig or unreadable file format; pass null gracefully
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Pass the sub-package manifest and parsed tsconfig configuration down to our entry tracker
|
|
213
|
-
const entryPoints = this.calculatePackageExportsEntries(pkg, packageDir, localTsconfig);
|
|
214
|
-
|
|
215
|
-
this.packageManifests.set(pkg.name, {
|
|
216
|
-
packageName: pkg.name,
|
|
217
|
-
rootDirectory: packageDir,
|
|
218
|
-
manifestPath: manifestFile,
|
|
219
|
-
entryPoints
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
this.workspacePackageNames.add(pkg.name);
|
|
223
|
-
// Also register the package root so the resolver can identify files
|
|
224
|
-
// inside this package as "internal" rather than node_modules.
|
|
225
|
-
this.context.monorepoPackageRoots.add(packageDir);
|
|
226
|
-
return true;
|
|
227
|
-
}
|
|
228
|
-
} catch {
|
|
229
|
-
// package.json parsing failed; ignore invalid directory roots
|
|
230
|
-
}
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Tracks package entry points by evaluating standard fields and main/exports configurations.
|
|
236
|
-
* Reverses build-artifact paths back to source developer locations using local compilerOptions.
|
|
237
|
-
*/
|
|
238
|
-
calculatePackageExportsEntries(pkg, pkgDir, localTsconfig) {
|
|
239
|
-
const entries = new Set();
|
|
240
|
-
|
|
241
|
-
// 1. Trace traditional entry fields from manifest
|
|
242
|
-
if (pkg.main && typeof pkg.main === 'string') entries.add(path.resolve(pkgDir, pkg.main));
|
|
243
|
-
if (pkg.module && typeof pkg.module === 'string') entries.add(path.resolve(pkgDir, pkg.module));
|
|
244
|
-
if (pkg.browser && typeof pkg.browser === 'string') entries.add(path.resolve(pkgDir, pkg.browser));
|
|
245
|
-
if (pkg.types && typeof pkg.types === 'string') entries.add(path.resolve(pkgDir, pkg.types));
|
|
246
|
-
if (pkg.typings && typeof pkg.typings === 'string') entries.add(path.resolve(pkgDir, pkg.typings));
|
|
247
|
-
|
|
248
|
-
// 2. Trace secondary and toolchain executable fields to avoid missing binary/source roots
|
|
249
|
-
if (pkg.bin) {
|
|
250
|
-
if (typeof pkg.bin === 'string') {
|
|
251
|
-
entries.add(path.resolve(pkgDir, pkg.bin));
|
|
252
|
-
} else if (typeof pkg.bin === 'object' && pkg.bin !== null) {
|
|
253
|
-
for (const binPath of Object.values(pkg.bin)) {
|
|
254
|
-
if (typeof binPath === 'string') entries.add(path.resolve(pkgDir, binPath));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
if (pkg.source && typeof pkg.source === 'string') entries.add(path.resolve(pkgDir, pkg.source));
|
|
259
|
-
|
|
260
|
-
// 3. Handle deep nested conditional exports matrices block parameters
|
|
261
|
-
if (pkg.exports) {
|
|
262
|
-
this.recursivelyUnwindExports(pkg.exports, pkgDir, entries);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// 4. Extract compiler options directly from the sub-package tsconfig metadata
|
|
266
|
-
let outDirToken = 'dist'; // Default standard target directory
|
|
267
|
-
let rootDirToken = 'src'; // Default standard source directory
|
|
268
|
-
|
|
269
|
-
if (localTsconfig && localTsconfig.compilerOptions) {
|
|
270
|
-
if (localTsconfig.compilerOptions.outDir) {
|
|
271
|
-
outDirToken = path.normalize(localTsconfig.compilerOptions.outDir).replace(/^\.[\\/]/, '');
|
|
272
|
-
}
|
|
273
|
-
if (localTsconfig.compilerOptions.rootDir) {
|
|
274
|
-
rootDirToken = path.normalize(localTsconfig.compilerOptions.rootDir).replace(/^\.[\\/]/, '');
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// 5. Manifest-Driven Verification Layer with Dynamic tsconfig Remapping
|
|
279
|
-
const collectedPaths = Array.from(entries);
|
|
280
|
-
const verifiedManifestEntries = [];
|
|
281
|
-
|
|
282
|
-
for (const absolutePath of collectedPaths) {
|
|
283
|
-
const normalizedPath = path.normalize(absolutePath);
|
|
284
|
-
|
|
285
|
-
if (fsSync.existsSync(normalizedPath) && fsSync.statSync(normalizedPath).isFile()) {
|
|
286
|
-
verifiedManifestEntries.push(normalizedPath);
|
|
287
|
-
} else {
|
|
288
|
-
// Precise Remapping Layer: Swap out local outDir tokens with local rootDir tokens
|
|
289
|
-
const pathSegments = normalizedPath.split(path.sep);
|
|
290
|
-
const outDirIndex = pathSegments.lastIndexOf(outDirToken);
|
|
291
|
-
|
|
292
|
-
if (outDirIndex !== -1) {
|
|
293
|
-
pathSegments[outDirIndex] = rootDirToken;
|
|
294
|
-
let tsconfigCandidate = pathSegments.join(path.sep).replace(/\.js$/, '.ts');
|
|
295
|
-
|
|
296
|
-
if (fsSync.existsSync(tsconfigCandidate) && fsSync.statSync(tsconfigCandidate).isFile()) {
|
|
297
|
-
verifiedManifestEntries.push(tsconfigCandidate);
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Check for companion TSX formats
|
|
302
|
-
const tsxCandidate = tsconfigCandidate.replace(/\.ts$/, '.tsx');
|
|
303
|
-
if (fsSync.existsSync(tsxCandidate) && fsSync.statSync(tsxCandidate).isFile()) {
|
|
304
|
-
verifiedManifestEntries.push(tsxCandidate);
|
|
305
|
-
continue;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Original Multi-directory Fallback Engine
|
|
310
|
-
const remappedFallbacks = [
|
|
311
|
-
normalizedPath.replace(/([\\/]|^)(dist|build|lib|out)([\\/])/, '$1src$3').replace(/\.js$/, '.ts'),
|
|
312
|
-
normalizedPath.replace(/([\\/]|^)(dist|build|lib|out)([\\/])/, '$1src$3').replace(/\.js$/, '.tsx'),
|
|
313
|
-
normalizedPath.replace(/([\\/]|^)(dist|build|lib|out)([\\/])/, '$1src$3').replace(/\.js$/, '.jsx'),
|
|
314
|
-
normalizedPath.replace(/\.js$/, '.ts'),
|
|
315
|
-
normalizedPath.replace(/\.js$/, '.tsx')
|
|
316
|
-
];
|
|
317
|
-
|
|
318
|
-
for (const fallbackPath of remappedFallbacks) {
|
|
319
|
-
const cleanFallback = path.normalize(fallbackPath);
|
|
320
|
-
if (fsSync.existsSync(cleanFallback) && fsSync.statSync(cleanFallback).isFile()) {
|
|
321
|
-
verifiedManifestEntries.push(cleanFallback);
|
|
322
|
-
break;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Populate verified items
|
|
329
|
-
entries.clear();
|
|
330
|
-
verifiedManifestEntries.forEach(item => entries.add(item));
|
|
331
|
-
|
|
332
|
-
// 6. Default file index fallback configurations (Only runs if entries size is 0)
|
|
333
|
-
if (entries.size === 0) {
|
|
334
|
-
const standardFallbacks = [
|
|
335
|
-
path.join(pkgDir, rootDirToken, 'index.ts'),
|
|
336
|
-
path.join(pkgDir, rootDirToken, 'index.tsx'),
|
|
337
|
-
path.join(pkgDir, rootDirToken, 'index.js'),
|
|
338
|
-
path.join(pkgDir, 'index.ts'),
|
|
339
|
-
path.join(pkgDir, 'index.js')
|
|
340
|
-
];
|
|
341
|
-
|
|
342
|
-
for (const fallback of standardFallbacks) {
|
|
343
|
-
if (fsSync.existsSync(fallback) && fsSync.statSync(fallback).isFile()) {
|
|
344
|
-
entries.add(fallback);
|
|
345
|
-
break; // Stop on first matched valid fallback file
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return Array.from(entries);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
recursivelyUnwindExports(exportsValue, pkgDir, collected) {
|
|
354
|
-
if (typeof exportsValue === 'string') {
|
|
355
|
-
if (exportsValue.startsWith('.')) {
|
|
356
|
-
collected.add(path.resolve(pkgDir, exportsValue));
|
|
357
|
-
}
|
|
358
|
-
} else if (Array.isArray(exportsValue)) {
|
|
359
|
-
exportsValue.forEach(v => this.recursivelyUnwindExports(v, pkgDir, collected));
|
|
360
|
-
} else if (typeof exportsValue === 'object' && exportsValue !== null) {
|
|
361
|
-
for (const val of Object.values(exportsValue)) {
|
|
362
|
-
this.recursivelyUnwindExports(val, pkgDir, collected);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Marks all registered workspace package names as used in the global
|
|
369
|
-
* `usedExternalPackages` set so they are never flagged as unused dependencies.
|
|
370
|
-
*
|
|
371
|
-
* This must be called after `initializeWorkspaceMesh()` and before the
|
|
372
|
-
* unused-dependency report is generated.
|
|
373
|
-
*/
|
|
374
|
-
markWorkspacePackagesAsUsed() {
|
|
375
|
-
for (const pkgName of this.workspacePackageNames) {
|
|
376
|
-
this.context.usedExternalPackages.add(pkgName);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Checks if an import specifier matches a package registered in our workspace mesh.
|
|
382
|
-
*/
|
|
383
|
-
isLocalWorkspaceSpecifier(specifier) {
|
|
384
|
-
if (this.workspacePackageNames.has(specifier)) return true;
|
|
385
|
-
|
|
386
|
-
// Catch sub-path imports from monorepos (e.g., '@workspace/shared/utils')
|
|
387
|
-
for (const registeredPkgName of this.workspacePackageNames) {
|
|
388
|
-
if (specifier.startsWith(`${registeredPkgName}/`)) return true;
|
|
389
|
-
}
|
|
390
|
-
return false;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Maps a workspace package specifier to its local absolute package root location on disk.
|
|
395
|
-
*/
|
|
396
|
-
/**
|
|
397
|
-
* Maps a workspace package specifier to its local absolute package root location on disk.
|
|
398
|
-
*/
|
|
399
|
-
getWorkspacePackageMatch(specifier) {
|
|
400
|
-
if (this.packageManifests.has(specifier)) {
|
|
401
|
-
return this.packageManifests.get(specifier);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
for (const [name, metadata] of this.packageManifests.entries()) {
|
|
405
|
-
if (specifier.startsWith(`${name}/`)) {
|
|
406
|
-
return metadata;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
return null;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Tracks an active module import specifier against the package context.
|
|
414
|
-
* Identifies unlisted dependencies and roots shadowed dependencies.
|
|
415
|
-
*/
|
|
416
|
-
auditImportSpecifier(specifier, importingFilePath) {
|
|
417
|
-
// Ignore internal relative/absolute path imports
|
|
418
|
-
if (specifier.startsWith('.') || specifier.startsWith('/')) return;
|
|
419
|
-
|
|
420
|
-
// Clean up subpath imports (e.g., 'js-yaml/lib' -> 'js-yaml')
|
|
421
|
-
const basePackageName = specifier.startsWith('@')
|
|
422
|
-
? specifier.split('/').slice(0, 2).join('/')
|
|
423
|
-
: specifier.split('/')[0];
|
|
424
|
-
|
|
425
|
-
// Find which local workspace package owns the file doing the importing
|
|
426
|
-
let owningWorkspace = null;
|
|
427
|
-
for (const [_, metadata] of this.packageManifests.entries()) {
|
|
428
|
-
const relativePath = path.relative(metadata.rootDirectory, importingFilePath);
|
|
429
|
-
if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
|
|
430
|
-
owningWorkspace = metadata;
|
|
431
|
-
break;
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (owningWorkspace) {
|
|
436
|
-
// Read the local workspace package.json dependencies
|
|
437
|
-
let localManifest = {};
|
|
438
|
-
try {
|
|
439
|
-
localManifest = JSON.parse(fsSync.readFileSync(owningWorkspace.manifestPath, 'utf8'));
|
|
440
|
-
} catch {}
|
|
441
|
-
|
|
442
|
-
const localDeps = new Set([
|
|
443
|
-
...Object.keys(localManifest.dependencies || {}),
|
|
444
|
-
...Object.keys(localManifest.devDependencies || {}),
|
|
445
|
-
...Object.keys(localManifest.peerDependencies || {})
|
|
446
|
-
]);
|
|
447
|
-
|
|
448
|
-
// 🚨 TARGET BUG 2: Detect Unlisted Dependencies
|
|
449
|
-
if (!localDeps.has(basePackageName) && !this.workspacePackageNames.has(basePackageName)) {
|
|
450
|
-
if (!this.context.unlistedDependencies) this.context.unlistedDependencies = [];
|
|
451
|
-
|
|
452
|
-
const alreadyFlagged = this.context.unlistedDependencies.some(
|
|
453
|
-
u => u.package === basePackageName && u.file === importingFilePath
|
|
454
|
-
);
|
|
455
|
-
|
|
456
|
-
if (!alreadyFlagged) {
|
|
457
|
-
this.context.unlistedDependencies.push({
|
|
458
|
-
package: basePackageName,
|
|
459
|
-
file: path.relative(this.context.cwd, importingFilePath),
|
|
460
|
-
manifest: path.relative(this.context.cwd, owningWorkspace.manifestPath)
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// Track that this package name was explicitly consumed by an active workspace file
|
|
466
|
-
if (!this.context.consumedWorkspacePackages) this.context.consumedWorkspacePackages = new Set();
|
|
467
|
-
this.context.consumedWorkspacePackages.add(basePackageName);
|
|
468
|
-
} else {
|
|
469
|
-
// The import happened in the root workspace environment
|
|
470
|
-
if (!this.context.consumedRootPackages) this.context.consumedRootPackages = new Set();
|
|
471
|
-
this.context.consumedRootPackages.add(basePackageName);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
2
|
+
constructor(context) { this.context = context; }
|
|
3
|
+
async initializeWorkspaceMesh() {}
|
|
4
|
+
markWorkspacePackagesAsUsed() {}
|
|
5
|
+
}
|
|
@@ -1,59 +1,5 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Workspace Diagnostic & Architecture Enforcement
|
|
6
|
-
* Validates workspace structure and enforces architectural boundaries.
|
|
7
|
-
*/
|
|
8
1
|
export class WorkspaceDiagnostic {
|
|
9
|
-
constructor(context) {
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Checks for circular dependencies across monorepo packages.
|
|
15
|
-
*/
|
|
16
|
-
async checkWorkspaceHealth() {
|
|
17
|
-
const findings = [];
|
|
18
|
-
// Logic to analyze workspace mesh and find cross-package cycles
|
|
19
|
-
return findings;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Enforces architectural boundaries (e.g., /features cannot import from /utilities directly).
|
|
24
|
-
*/
|
|
25
|
-
enforceBoundaries(filePath, imports) {
|
|
26
|
-
const violations = [];
|
|
27
|
-
const rules = this.context.rules.boundaries || [];
|
|
28
|
-
|
|
29
|
-
for (const rule of rules) {
|
|
30
|
-
if (filePath.includes(rule.from) && imports.some(imp => imp.includes(rule.to))) {
|
|
31
|
-
violations.push({
|
|
32
|
-
type: 'BOUNDARY_VIOLATION',
|
|
33
|
-
file: filePath,
|
|
34
|
-
message: `Architectural boundary violation: ${rule.from} should not import from ${rule.to}`
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return violations;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Identifies "Hotspots" by combining complexity with change-frequency.
|
|
43
|
-
*/
|
|
44
|
-
async identifyHotspots(projectGraph, gitHistory) {
|
|
45
|
-
const hotspots = [];
|
|
46
|
-
// Combine Cyclomatic Complexity with Git commit frequency
|
|
47
|
-
return hotspots;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Type-Jail Analysis
|
|
52
|
-
* Tracks structural shapes implicitly to warn when accessing non-existent properties.
|
|
53
|
-
*/
|
|
54
|
-
analyzeTypeJail(fileNode) {
|
|
55
|
-
const violations = [];
|
|
56
|
-
// Logic to track object shapes and warn on suspicious property access
|
|
57
|
-
return violations;
|
|
58
|
-
}
|
|
2
|
+
constructor(context) { this.context = context; }
|
|
3
|
+
async checkWorkspaceHealth() { return []; }
|
|
4
|
+
enforceBoundaries(filePath, imports) { return []; }
|
|
59
5
|
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ============================================================================
|
|
3
|
-
* Knip Plugin Adapter for entkapp v4.0.0
|
|
4
|
-
* ============================================================================
|
|
5
|
-
* This adapter allows entkapp to use existing Knip plugins without
|
|
6
|
-
* requiring knip as a dependency. It implements the Knip plugin interface
|
|
7
|
-
* internally to ensure full compatibility.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import path from 'path';
|
|
11
|
-
import fs from 'fs/promises';
|
|
12
|
-
|
|
13
|
-
export class KnipAdapter {
|
|
14
|
-
constructor(context) {
|
|
15
|
-
this.context = context;
|
|
16
|
-
this.knipPlugins = new Map();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Discovers and loads Knip plugins from the project's node_modules
|
|
21
|
-
* or a specified directory.
|
|
22
|
-
*/
|
|
23
|
-
async discoverPlugins(projectRoot) {
|
|
24
|
-
// Knip plugins are typically named 'knip-plugin-*' or are part of knip's core
|
|
25
|
-
// We look for common Knip plugin patterns in node_modules
|
|
26
|
-
const nodeModulesPath = path.join(projectRoot, 'node_modules');
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const dirs = await fs.readdir(nodeModulesPath);
|
|
30
|
-
for (const dir of dirs) {
|
|
31
|
-
if (dir.startsWith('knip-plugin-') || dir === '@knip/plugin') {
|
|
32
|
-
await this.loadPlugin(path.join(nodeModulesPath, dir));
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
} catch (e) {
|
|
36
|
-
// node_modules not found or unreadable
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Loads a specific Knip plugin and wraps it for entkapp
|
|
42
|
-
*/
|
|
43
|
-
async loadPlugin(pluginPath) {
|
|
44
|
-
try {
|
|
45
|
-
const pluginModule = await import(pluginPath);
|
|
46
|
-
const plugin = pluginModule.default || pluginModule;
|
|
47
|
-
|
|
48
|
-
if (plugin.name && (plugin.config || plugin.entry)) {
|
|
49
|
-
this.knipPlugins.set(plugin.name, this.wrapKnipPlugin(plugin));
|
|
50
|
-
if (this.context.verbose) {
|
|
51
|
-
console.log(`[KnipAdapter] Successfully integrated Knip plugin: ${plugin.name}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
} catch (e) {
|
|
55
|
-
// Failed to load plugin
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Wraps a Knip plugin to match the entkapp BasePlugin interface
|
|
61
|
-
*/
|
|
62
|
-
wrapKnipPlugin(knipPlugin) {
|
|
63
|
-
return {
|
|
64
|
-
name: `knip-${knipPlugin.name}`,
|
|
65
|
-
isKnipWrapped: true,
|
|
66
|
-
|
|
67
|
-
getConfigFiles: () => {
|
|
68
|
-
if (Array.isArray(knipPlugin.config)) return knipPlugin.config;
|
|
69
|
-
if (typeof knipPlugin.config === 'string') return [knipPlugin.config];
|
|
70
|
-
return [];
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
getRoutePatterns: () => {
|
|
74
|
-
if (Array.isArray(knipPlugin.entry)) return knipPlugin.entry.map(e => new RegExp(e.replace('*', '.*')));
|
|
75
|
-
if (typeof knipPlugin.entry === 'string') return [new RegExp(knipPlugin.entry.replace('*', '.*'))];
|
|
76
|
-
return [];
|
|
77
|
-
},
|
|
78
|
-
|
|
79
|
-
isActive: async (baseDir) => {
|
|
80
|
-
const configFiles = Array.isArray(knipPlugin.config) ? knipPlugin.config : [knipPlugin.config];
|
|
81
|
-
for (const file of configFiles) {
|
|
82
|
-
if (!file) continue;
|
|
83
|
-
try {
|
|
84
|
-
await fs.access(path.join(baseDir, file));
|
|
85
|
-
return true;
|
|
86
|
-
} catch {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return false;
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
// Map other Knip plugin properties to entkapp
|
|
94
|
-
get: (key) => knipPlugin[key]
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Returns all integrated Knip plugins
|
|
100
|
-
*/
|
|
101
|
-
getPlugins() {
|
|
102
|
-
return Array.from(this.knipPlugins.values());
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export default KnipAdapter;
|