entkapp 5.5.0 → 5.6.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/README.md +1 -1
- package/bin/cli.js +2 -2
- package/bin/cli.mjs +175 -0
- package/index.cjs +18 -0
- package/index.mjs +51 -0
- package/package.json +7 -6
- package/src/EngineContext.mjs +428 -0
- package/src/Initializer.mjs +82 -0
- package/src/analyzers/CodeSmellAnalyzer.mjs +106 -0
- package/src/analyzers/OxcAnalyzer.mjs +11 -0
- package/src/api/HeadlessAPI.js +31 -16
- package/src/api/HeadlessAPI.mjs +369 -0
- package/src/api/PluginSDK.mjs +135 -0
- package/src/ast/ASTAnalyzer.js +17 -3
- package/src/ast/ASTAnalyzer.mjs +742 -0
- package/src/ast/AdvancedAnalysis.mjs +586 -0
- package/src/ast/BarrelParser.mjs +230 -0
- package/src/ast/DeadCodeDetector.js +7 -5
- package/src/ast/DeadCodeDetector.mjs +92 -0
- package/src/ast/MagicDetector.mjs +203 -0
- package/src/ast/OxcAnalyzer.mjs +188 -0
- package/src/ast/SecretScanner.mjs +374 -0
- package/src/healing/GitSandbox.mjs +82 -0
- package/src/healing/SelfHealer.mjs +48 -0
- package/src/index.js +37 -1
- package/src/index.mjs +1180 -0
- package/src/performance/GraphCache.mjs +108 -0
- package/src/performance/SupplyChainGuard.mjs +92 -0
- package/src/performance/WorkerPool.mjs +132 -0
- package/src/performance/WorkerTaskRunner.mjs +144 -0
- package/src/plugins/BasePlugin.mjs +240 -0
- package/src/plugins/PluginRegistry.mjs +203 -0
- package/src/plugins/ecosystems/BackendServices.mjs +197 -0
- package/src/plugins/ecosystems/GenericPlugins.mjs +142 -0
- package/src/plugins/ecosystems/ModernFrameworks.mjs +162 -0
- package/src/plugins/ecosystems/MorePlugins.mjs +562 -0
- package/src/plugins/ecosystems/NewPlugins.mjs +526 -0
- package/src/plugins/ecosystems/NextJsPlugin.mjs +45 -0
- package/src/plugins/ecosystems/PluginLoader.mjs +193 -0
- package/src/plugins/ecosystems/TypeScriptPlugin.mjs +56 -0
- package/src/plugins/ecosystems/UltimateBundle.mjs +1182 -0
- package/src/refractor/ImpactAnalyzer.mjs +92 -0
- package/src/refractor/SourceRewriter.mjs +86 -0
- package/src/refractor/TransactionManager.mjs +5 -0
- package/src/refractor/TypeIntegrity.mjs +4 -0
- package/src/resolution/BuildOrchestrator.mjs +46 -0
- package/src/resolution/CircularDetector.mjs +91 -0
- package/src/resolution/ConfigGenerator.mjs +83 -0
- package/src/resolution/ConfigLoader.mjs +94 -0
- package/src/resolution/DepencyResolver.mjs +66 -0
- package/src/resolution/DependencyFixer.mjs +88 -0
- package/src/resolution/DependencyProfiler.mjs +286 -0
- package/src/resolution/EntryPointDetector.mjs +134 -0
- package/src/resolution/FrameworkConfigParser.mjs +119 -0
- package/src/resolution/GraphAnalyzer.mjs +80 -0
- package/src/resolution/MigrationAnalyzer.mjs +60 -0
- package/src/resolution/PathMapper.mjs +82 -0
- package/src/resolution/TSConfigLoader.mjs +162 -0
- package/src/resolution/WorkSpaceGraph.mjs +183 -0
- package/src/resolution/WorkspaceDiagnostic.mjs +139 -0
- package/test.js +76 -0
- package/wrangler.toml +6 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { TSConfigLoader } from './TSConfigLoader.mjs';
|
|
4
|
+
|
|
5
|
+
export class PathMapper {
|
|
6
|
+
constructor(context) {
|
|
7
|
+
this.context = context;
|
|
8
|
+
this.aliasMappers = []; // list of alias mapping functions
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async loadMappings(tsconfigFilename = 'tsconfig.json') {
|
|
12
|
+
// Load root tsconfig
|
|
13
|
+
const loader = new TSConfigLoader(this.context.cwd);
|
|
14
|
+
const config = loader.load();
|
|
15
|
+
if (config) {
|
|
16
|
+
const mapper = loader.getAliasMapper(config);
|
|
17
|
+
this.aliasMappers.push(mapper);
|
|
18
|
+
if (this.context.verbose) console.log(`[PathMapper] Loaded root tsconfig aliases`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Load workspace tsconfigs if available
|
|
22
|
+
if (this.context.isWorkspaceEnabled && this.context.monorepoPackageRoots) {
|
|
23
|
+
for (const root of this.context.monorepoPackageRoots) {
|
|
24
|
+
const wsLoader = new TSConfigLoader(root);
|
|
25
|
+
const wsConfig = wsLoader.load();
|
|
26
|
+
if (wsConfig) {
|
|
27
|
+
const wsMapper = wsLoader.getAliasMapper(wsConfig);
|
|
28
|
+
this.aliasMappers.push(wsMapper);
|
|
29
|
+
if (this.context.verbose) console.log(`[PathMapper] Loaded workspace tsconfig aliases from ${root}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolves physical module paths on disk, translating modern .js imports
|
|
37
|
+
* back to their actual TypeScript source files.
|
|
38
|
+
* @param {string} p - The target module specifier or absolute path
|
|
39
|
+
*/
|
|
40
|
+
resolvePath(p) {
|
|
41
|
+
if (!p || typeof p !== 'string') return p;
|
|
42
|
+
|
|
43
|
+
let resolvedP = p;
|
|
44
|
+
|
|
45
|
+
// Try alias mappers first
|
|
46
|
+
for (const mapper of this.aliasMappers) {
|
|
47
|
+
const mapped = mapper(resolvedP);
|
|
48
|
+
if (mapped !== resolvedP && fs.existsSync(mapped)) {
|
|
49
|
+
resolvedP = mapped;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// FIX 1: If the import ends with .js, translate it to .ts for the search
|
|
55
|
+
if (resolvedP.endsWith('.mjs')) {
|
|
56
|
+
const tsPath = resolvedP.slice(0, -3) + '.ts';
|
|
57
|
+
if (fs.existsSync(tsPath)) return tsPath;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// FIX 2: If the import ends with .jsx, translate it to .tsx for the search
|
|
61
|
+
if (resolvedP.endsWith('.jsx')) {
|
|
62
|
+
const tsxPath = resolvedP.slice(0, -4) + '.tsx';
|
|
63
|
+
if (fs.existsSync(tsxPath)) return tsxPath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// FIX 3: Support for directory imports (z.B. ./adapters -> ./adapters/index.ts)
|
|
67
|
+
try {
|
|
68
|
+
const stat = fs.statSync(resolvedP);
|
|
69
|
+
if (stat.isDirectory()) {
|
|
70
|
+
const extensions = ['.ts', '.tsx', '.mjs', '.jsx'];
|
|
71
|
+
for (const ext of extensions) {
|
|
72
|
+
const indexPath = path.join(resolvedP, `index${ext}`);
|
|
73
|
+
if (fs.existsSync(indexPath)) return indexPath;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// File does not exist or is not a directory, continue with default
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return resolvedP;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
|
|
5
|
+
export class TSConfigLoader {
|
|
6
|
+
constructor(targetDir) {
|
|
7
|
+
this.targetDir = targetDir;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lädt und parst die tsconfig.json.
|
|
12
|
+
* Unterstützt JSONC (Kommentare), extends, und project references.
|
|
13
|
+
* @param {string} [filename='tsconfig.json'] - Name der tsconfig-Datei
|
|
14
|
+
* @returns {Object|null} Parsed config oder null.
|
|
15
|
+
*/
|
|
16
|
+
load(filename = 'tsconfig.json') {
|
|
17
|
+
const configPath = path.join(this.targetDir, filename);
|
|
18
|
+
if (!fs.existsSync(configPath)) return null;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
22
|
+
const readResult = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
23
|
+
|
|
24
|
+
if (readResult.error) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
29
|
+
readResult.config,
|
|
30
|
+
ts.sys,
|
|
31
|
+
this.targetDir
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Attach raw config for reference inspection
|
|
35
|
+
parsed._rawConfig = readResult.config;
|
|
36
|
+
|
|
37
|
+
return parsed;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extracts project references from tsconfig (composite monorepo support).
|
|
45
|
+
* @returns {string[]} Array of referenced tsconfig paths
|
|
46
|
+
*/
|
|
47
|
+
getProjectReferences(filename = 'tsconfig.json') {
|
|
48
|
+
const configPath = path.join(this.targetDir, filename);
|
|
49
|
+
if (!fs.existsSync(configPath)) return [];
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
53
|
+
// Strip comments
|
|
54
|
+
const stripped = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
55
|
+
const raw = JSON.parse(stripped);
|
|
56
|
+
const refs = raw.references || [];
|
|
57
|
+
return refs.map(ref => {
|
|
58
|
+
const refPath = path.resolve(this.targetDir, ref.path);
|
|
59
|
+
// If it's a directory, look for tsconfig.json inside
|
|
60
|
+
if (fs.existsSync(refPath) && fs.statSync(refPath).isDirectory()) {
|
|
61
|
+
return path.join(refPath, 'tsconfig.json');
|
|
62
|
+
}
|
|
63
|
+
return refPath;
|
|
64
|
+
});
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extracts the "extends" chain from tsconfig.
|
|
72
|
+
* @returns {string[]} Array of extended tsconfig paths
|
|
73
|
+
*/
|
|
74
|
+
getExtendsChain(filename = 'tsconfig.json') {
|
|
75
|
+
const configPath = path.join(this.targetDir, filename);
|
|
76
|
+
if (!fs.existsSync(configPath)) return [];
|
|
77
|
+
|
|
78
|
+
const chain = [];
|
|
79
|
+
let currentPath = configPath;
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < 10; i++) { // limit depth to avoid infinite loops
|
|
82
|
+
try {
|
|
83
|
+
const content = fs.readFileSync(currentPath, 'utf8');
|
|
84
|
+
const stripped = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
85
|
+
const raw = JSON.parse(stripped);
|
|
86
|
+
if (!raw.extends) break;
|
|
87
|
+
const extendedPath = path.resolve(path.dirname(currentPath), raw.extends);
|
|
88
|
+
const resolvedPath = extendedPath.endsWith('.json') ? extendedPath : extendedPath + '.json';
|
|
89
|
+
if (!fs.existsSync(resolvedPath)) break;
|
|
90
|
+
chain.push(resolvedPath);
|
|
91
|
+
currentPath = resolvedPath;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return chain;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Erstellt eine Mapping-Funktion für Aliases aus der tsconfig.
|
|
102
|
+
* Berücksichtigt baseUrl, paths und project references.
|
|
103
|
+
* @param {Object} parsedConfig
|
|
104
|
+
* @returns {Function} Mapper function.
|
|
105
|
+
*/
|
|
106
|
+
getAliasMapper(parsedConfig) {
|
|
107
|
+
if (!parsedConfig || !parsedConfig.options) {
|
|
108
|
+
return (source) => source;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { paths, baseUrl } = parsedConfig.options;
|
|
112
|
+
const base = baseUrl ? path.resolve(this.targetDir, baseUrl) : this.targetDir;
|
|
113
|
+
|
|
114
|
+
return (source) => {
|
|
115
|
+
// If no paths configured, still try baseUrl resolution
|
|
116
|
+
if (!paths) {
|
|
117
|
+
if (baseUrl && !source.startsWith('.') && !source.startsWith('/') && !source.startsWith('@')) {
|
|
118
|
+
const candidate = path.resolve(base, source);
|
|
119
|
+
const extensions = ['', '.ts', '.tsx', '.mjs', '.jsx'];
|
|
120
|
+
for (const ext of extensions) {
|
|
121
|
+
if (fs.existsSync(candidate + ext)) return candidate + ext;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return source;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const pattern in paths) {
|
|
128
|
+
const regexPattern = pattern.replace(/\*/g, '(.*)');
|
|
129
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
130
|
+
const match = source.match(regex);
|
|
131
|
+
|
|
132
|
+
if (match) {
|
|
133
|
+
const replacements = paths[pattern];
|
|
134
|
+
for (const replacement of replacements) {
|
|
135
|
+
const resolvedReplacement = replacement.replace(/\*/g, match[1] || '');
|
|
136
|
+
const fullPath = path.resolve(base, resolvedReplacement);
|
|
137
|
+
|
|
138
|
+
// Check with common extensions
|
|
139
|
+
const extensions = ['', '.ts', '.tsx', '.mjs', '.jsx', '/index.ts', '/index.tsx', '/index.mjs'];
|
|
140
|
+
for (const ext of extensions) {
|
|
141
|
+
if (fs.existsSync(fullPath + ext)) {
|
|
142
|
+
return fullPath + ext;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return source;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Returns all include/exclude glob patterns from tsconfig.
|
|
154
|
+
*/
|
|
155
|
+
getIncludeExcludePatterns(parsedConfig) {
|
|
156
|
+
if (!parsedConfig || !parsedConfig._rawConfig) return { include: [], exclude: [] };
|
|
157
|
+
return {
|
|
158
|
+
include: parsedConfig._rawConfig.include || [],
|
|
159
|
+
exclude: parsedConfig._rawConfig.exclude || []
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export class WorkspaceGraph {
|
|
5
|
+
constructor(context) {
|
|
6
|
+
this.context = context;
|
|
7
|
+
this.packageManifests = new Map(); // dirPath -> manifestData
|
|
8
|
+
this.workspacePackages = new Map(); // packageName -> dirPath
|
|
9
|
+
this.tsconfigPaths = new Map(); // packageName -> tsconfigData
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async initializeWorkspaceMesh() {
|
|
13
|
+
const rootPkgPath = path.join(this.context.cwd, 'package.json');
|
|
14
|
+
try {
|
|
15
|
+
const rootPkg = JSON.parse(await fs.readFile(rootPkgPath, 'utf8'));
|
|
16
|
+
let workspaces = [];
|
|
17
|
+
|
|
18
|
+
// 1. Detect Workspaces (npm/yarn/pnpm/lerna)
|
|
19
|
+
if (rootPkg.workspaces) {
|
|
20
|
+
workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces.packages || [];
|
|
21
|
+
} else {
|
|
22
|
+
// Fallback for pnpm-workspace.yaml
|
|
23
|
+
const pnpmWorkspacePath = path.join(this.context.cwd, 'pnpm-workspace.yaml');
|
|
24
|
+
try {
|
|
25
|
+
const yaml = await fs.readFile(pnpmWorkspacePath, 'utf8');
|
|
26
|
+
const match = yaml.match(/packages:\n((?:\s+- .+\n?)+)/);
|
|
27
|
+
if (match) {
|
|
28
|
+
workspaces = match[1].split('\n')
|
|
29
|
+
.filter(line => line.trim().startsWith('-'))
|
|
30
|
+
.map(line => line.replace('-', '').trim().replace(/['"]/g, ''));
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (workspaces.length > 0) {
|
|
36
|
+
this.context.isWorkspaceEnabled = true;
|
|
37
|
+
if (this.context.verbose) console.log(`[Workspace] Detected workspaces:`, workspaces);
|
|
38
|
+
|
|
39
|
+
for (const pattern of workspaces) {
|
|
40
|
+
const matches = await this._expandGlob(pattern, this.context.cwd);
|
|
41
|
+
for (const matchDir of matches) {
|
|
42
|
+
const pkgPath = path.join(matchDir, 'package.json');
|
|
43
|
+
try {
|
|
44
|
+
const pkgData = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
45
|
+
const normalizedDir = matchDir.replace(/\\/g, '/');
|
|
46
|
+
|
|
47
|
+
this.packageManifests.set(normalizedDir, {
|
|
48
|
+
rootDirectory: normalizedDir,
|
|
49
|
+
manifestPath: pkgPath.replace(/\\/g, '/'),
|
|
50
|
+
name: pkgData.name,
|
|
51
|
+
dependencies: pkgData.dependencies || {},
|
|
52
|
+
devDependencies: pkgData.devDependencies || {},
|
|
53
|
+
peerDependencies: pkgData.peerDependencies || {},
|
|
54
|
+
scripts: pkgData.scripts || {},
|
|
55
|
+
entryPoints: this.calculatePackageExportsEntries(pkgData, normalizedDir)
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (pkgData.name) {
|
|
59
|
+
this.workspacePackages.set(pkgData.name, normalizedDir);
|
|
60
|
+
this.context.monorepoPackageRoots.add(normalizedDir);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Also try to load tsconfig.json for this workspace
|
|
64
|
+
const tsconfigPath = path.join(matchDir, 'tsconfig.json');
|
|
65
|
+
try {
|
|
66
|
+
const tsconfigData = JSON.parse(await fs.readFile(tsconfigPath, 'utf8'));
|
|
67
|
+
if (pkgData.name) this.tsconfigPaths.set(pkgData.name, tsconfigData);
|
|
68
|
+
} catch(e) {}
|
|
69
|
+
|
|
70
|
+
} catch (e) {}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (this.context.verbose) console.log('[Workspace] No root package.json found or invalid.');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
calculatePackageExportsEntries(pkgData, dirPath) {
|
|
80
|
+
const entries = [];
|
|
81
|
+
const addEntry = (p) => {
|
|
82
|
+
if (typeof p === 'string') entries.push(path.resolve(dirPath, p).replace(/\\/g, '/'));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (pkgData.main) addEntry(pkgData.main);
|
|
86
|
+
if (pkgData.module) addEntry(pkgData.module);
|
|
87
|
+
if (pkgData.source) addEntry(pkgData.source);
|
|
88
|
+
if (pkgData.types) addEntry(pkgData.types);
|
|
89
|
+
if (pkgData.typings) addEntry(pkgData.typings);
|
|
90
|
+
|
|
91
|
+
if (pkgData.bin) {
|
|
92
|
+
if (typeof pkgData.bin === 'string') addEntry(pkgData.bin);
|
|
93
|
+
else Object.values(pkgData.bin).forEach(addEntry);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (pkgData.exports) {
|
|
97
|
+
const traverseExports = (obj) => {
|
|
98
|
+
if (typeof obj === 'string') {
|
|
99
|
+
addEntry(obj);
|
|
100
|
+
} else if (typeof obj === 'object' && obj !== null) {
|
|
101
|
+
for (const key in obj) traverseExports(obj[key]);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
traverseExports(pkgData.exports);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return entries;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
isLocalWorkspaceSpecifier(specifier) {
|
|
111
|
+
if (!specifier) return false;
|
|
112
|
+
// Direct match
|
|
113
|
+
if (this.workspacePackages.has(specifier)) return true;
|
|
114
|
+
// Sub-path match (e.g. @my-org/ui/components)
|
|
115
|
+
for (const pkgName of this.workspacePackages.keys()) {
|
|
116
|
+
if (specifier.startsWith(pkgName + '/')) return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getWorkspacePackageMatch(specifier) {
|
|
122
|
+
if (this.workspacePackages.has(specifier)) {
|
|
123
|
+
const dir = this.workspacePackages.get(specifier);
|
|
124
|
+
return this.packageManifests.get(dir);
|
|
125
|
+
}
|
|
126
|
+
for (const pkgName of this.workspacePackages.keys()) {
|
|
127
|
+
if (specifier.startsWith(pkgName + '/')) {
|
|
128
|
+
const dir = this.workspacePackages.get(pkgName);
|
|
129
|
+
return this.packageManifests.get(dir);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
markWorkspacePackagesAsUsed() {
|
|
136
|
+
for (const [pkgName, dirPath] of this.workspacePackages.entries()) {
|
|
137
|
+
this.context.usedExternalPackages.add(pkgName);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Expands a workspace glob pattern (e.g. 'packages/*') to absolute directory paths.
|
|
143
|
+
* Supports single-level wildcards and direct paths.
|
|
144
|
+
*/
|
|
145
|
+
async _expandGlob(pattern, cwd) {
|
|
146
|
+
const results = [];
|
|
147
|
+
|
|
148
|
+
// Remove trailing slash
|
|
149
|
+
const cleanPattern = pattern.replace(/\/$/, '');
|
|
150
|
+
|
|
151
|
+
// Handle simple wildcard patterns like 'packages/*' or 'apps/*'
|
|
152
|
+
if (cleanPattern.includes('*')) {
|
|
153
|
+
const parts = cleanPattern.split('/');
|
|
154
|
+
const wildcardIndex = parts.findIndex(p => p.includes('*'));
|
|
155
|
+
const baseDir = path.join(cwd, ...parts.slice(0, wildcardIndex));
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
159
|
+
for (const entry of entries) {
|
|
160
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
161
|
+
const fullPath = path.join(baseDir, entry.name);
|
|
162
|
+
// Check if it has a package.json
|
|
163
|
+
try {
|
|
164
|
+
await fs.access(path.join(fullPath, 'package.json'));
|
|
165
|
+
results.push(fullPath.replace(/\\/g, '/'));
|
|
166
|
+
} catch (e) {}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {}
|
|
170
|
+
} else {
|
|
171
|
+
// Direct path
|
|
172
|
+
const fullPath = path.resolve(cwd, cleanPattern);
|
|
173
|
+
try {
|
|
174
|
+
const stat = await fs.stat(fullPath);
|
|
175
|
+
if (stat.isDirectory()) {
|
|
176
|
+
results.push(fullPath.replace(/\\/g, '/'));
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WorkspaceDiagnostic
|
|
6
|
+
* Performs health checks and architectural boundary enforcement for Monorepo workspaces.
|
|
7
|
+
* Detects cross-package import violations, version mismatches, and missing workspace declarations.
|
|
8
|
+
*/
|
|
9
|
+
export class WorkspaceDiagnostic {
|
|
10
|
+
constructor(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
this.findings = [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async checkWorkspaceHealth() {
|
|
16
|
+
this.findings = [];
|
|
17
|
+
|
|
18
|
+
if (!this.context.isWorkspaceEnabled) return this.findings;
|
|
19
|
+
|
|
20
|
+
const rootPkgPath = this.context.cwd ? path.join(this.context.cwd, 'package.json') : null;
|
|
21
|
+
let rootPkg = {};
|
|
22
|
+
try {
|
|
23
|
+
if (rootPkgPath) rootPkg = JSON.parse(await fs.readFile(rootPkgPath, 'utf8'));
|
|
24
|
+
} catch (e) {}
|
|
25
|
+
|
|
26
|
+
// 1. Check for version mismatches across workspace packages
|
|
27
|
+
const versionMap = new Map(); // dep name -> { version, source }
|
|
28
|
+
for (const [dir, manifest] of (this.context.monorepoPackageRoots || new Set()).entries ? [] : (this.context.monorepoPackageRoots || [])) {
|
|
29
|
+
// iterate over monorepoPackageRoots as a Set
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Use workspaceGraph if available
|
|
33
|
+
if (this.context.workspaceGraph && this.context.workspaceGraph.packageManifests) {
|
|
34
|
+
for (const [dir, manifest] of this.context.workspaceGraph.packageManifests.entries()) {
|
|
35
|
+
const allDeps = {
|
|
36
|
+
...manifest.dependencies,
|
|
37
|
+
...manifest.devDependencies
|
|
38
|
+
};
|
|
39
|
+
for (const [dep, version] of Object.entries(allDeps)) {
|
|
40
|
+
if (!versionMap.has(dep)) {
|
|
41
|
+
versionMap.set(dep, []);
|
|
42
|
+
}
|
|
43
|
+
versionMap.get(dep).push({ version, source: manifest.name || dir });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Report version mismatches
|
|
48
|
+
for (const [dep, usages] of versionMap.entries()) {
|
|
49
|
+
const uniqueVersions = new Set(usages.map(u => u.version));
|
|
50
|
+
if (uniqueVersions.size > 1) {
|
|
51
|
+
this.findings.push({
|
|
52
|
+
type: 'version-mismatch',
|
|
53
|
+
severity: 'warning',
|
|
54
|
+
message: `Dependency "${dep}" has conflicting versions across workspace packages: ${[...uniqueVersions].join(', ')}`,
|
|
55
|
+
packages: usages.map(u => u.source)
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. Check for missing workspace package declarations in root
|
|
61
|
+
for (const [dir, manifest] of this.context.workspaceGraph.packageManifests.entries()) {
|
|
62
|
+
if (!manifest.name) continue;
|
|
63
|
+
const rootDeps = {
|
|
64
|
+
...rootPkg.dependencies,
|
|
65
|
+
...rootPkg.devDependencies,
|
|
66
|
+
...rootPkg.peerDependencies
|
|
67
|
+
};
|
|
68
|
+
// If the workspace package is referenced in root deps but not as "workspace:*"
|
|
69
|
+
if (rootDeps[manifest.name] && !rootDeps[manifest.name].startsWith('workspace:')) {
|
|
70
|
+
this.findings.push({
|
|
71
|
+
type: 'workspace-declaration-missing',
|
|
72
|
+
severity: 'info',
|
|
73
|
+
message: `Workspace package "${manifest.name}" is referenced in root package.json without "workspace:" protocol`,
|
|
74
|
+
packages: ['root', manifest.name]
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 3. Check for packages that have no entry points defined
|
|
80
|
+
for (const [dir, manifest] of this.context.workspaceGraph.packageManifests.entries()) {
|
|
81
|
+
if (!manifest.entryPoints || manifest.entryPoints.length === 0) {
|
|
82
|
+
this.findings.push({
|
|
83
|
+
type: 'missing-entry-point',
|
|
84
|
+
severity: 'warning',
|
|
85
|
+
message: `Workspace package "${manifest.name || dir}" has no entry points (main/module/exports) defined in package.json`,
|
|
86
|
+
packages: [manifest.name || dir]
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this.findings;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
enforceBoundaries(filePath, imports) {
|
|
96
|
+
const violations = [];
|
|
97
|
+
if (!this.context.isWorkspaceEnabled) return violations;
|
|
98
|
+
if (!this.context.workspaceGraph || !this.context.workspaceGraph.packageManifests) return violations;
|
|
99
|
+
|
|
100
|
+
// Determine which workspace package this file belongs to
|
|
101
|
+
let sourcePackage = null;
|
|
102
|
+
for (const [dir, manifest] of this.context.workspaceGraph.packageManifests.entries()) {
|
|
103
|
+
if (filePath.startsWith(dir + '/') || filePath.startsWith(dir + '\\')) {
|
|
104
|
+
sourcePackage = manifest;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!sourcePackage) return violations;
|
|
110
|
+
|
|
111
|
+
// Check each import
|
|
112
|
+
for (const specifier of imports) {
|
|
113
|
+
if (!specifier || specifier.startsWith('.') || specifier.startsWith('/')) continue;
|
|
114
|
+
|
|
115
|
+
// Check if the import is a workspace package
|
|
116
|
+
if (this.context.workspaceGraph.isLocalWorkspaceSpecifier(specifier)) {
|
|
117
|
+
const targetManifest = this.context.workspaceGraph.getWorkspacePackageMatch(specifier);
|
|
118
|
+
if (targetManifest && targetManifest.name) {
|
|
119
|
+
// Check if the target package is declared as a dependency of the source package
|
|
120
|
+
const sourceDeps = {
|
|
121
|
+
...sourcePackage.dependencies,
|
|
122
|
+
...sourcePackage.devDependencies,
|
|
123
|
+
...sourcePackage.peerDependencies
|
|
124
|
+
};
|
|
125
|
+
if (!sourceDeps[targetManifest.name]) {
|
|
126
|
+
violations.push({
|
|
127
|
+
type: 'undeclared-workspace-dependency',
|
|
128
|
+
severity: 'error',
|
|
129
|
+
message: `Package "${sourcePackage.name}" imports "${specifier}" but "${targetManifest.name}" is not declared as a dependency`,
|
|
130
|
+
file: filePath
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return violations;
|
|
138
|
+
}
|
|
139
|
+
}
|
package/test.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
|
|
8
|
+
const projectRoot = path.resolve(__dirname);
|
|
9
|
+
const entkappCli = path.join(projectRoot, 'bin', 'cli.mjs');
|
|
10
|
+
|
|
11
|
+
const playgrounds = [
|
|
12
|
+
'playgrounds/basic',
|
|
13
|
+
'playgrounds/hard',
|
|
14
|
+
'playgrounds/intermediate',
|
|
15
|
+
'playgrounds/monorepo-basic/packages/app',
|
|
16
|
+
'playgrounds/monorepo-basic/packages/ui',
|
|
17
|
+
'playgrounds/monorepo-basic/packages/utils',
|
|
18
|
+
'playgrounds/monorepo-hard/packages/api',
|
|
19
|
+
'playgrounds/monorepo-hard/packages/app',
|
|
20
|
+
'playgrounds/monorepo-hard/packages/domain',
|
|
21
|
+
'playgrounds/monorepo-hard/packages/services',
|
|
22
|
+
'playgrounds/monorepo-intermediate/packages/app',
|
|
23
|
+
'playgrounds/monorepo-intermediate/packages/ui',
|
|
24
|
+
'playgrounds/monorepo-intermediate/packages/utils',
|
|
25
|
+
'playgrounds/monorepo-nightmare/packages/app',
|
|
26
|
+
'playgrounds/monorepo-nightmare/packages/bridge',
|
|
27
|
+
'playgrounds/monorepo-nightmare/packages/core',
|
|
28
|
+
'playgrounds/monorepo-normal/packages/app',
|
|
29
|
+
'playgrounds/monorepo-normal/packages/lib-a',
|
|
30
|
+
'playgrounds/monorepo-normal/packages/lib-b',
|
|
31
|
+
'playgrounds/nightmare',
|
|
32
|
+
'playgrounds/normal',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
async function runTest() {
|
|
36
|
+
let allTestsPassed = true;
|
|
37
|
+
|
|
38
|
+
for (const playground of playgrounds) {
|
|
39
|
+
const cwd = path.join(projectRoot, playground);
|
|
40
|
+
console.log(`\nRunning entkapp in: ${playground}`);
|
|
41
|
+
try {
|
|
42
|
+
const { stdout } = await execa('node', [entkappCli, '-r', '--cwd', cwd, '--yes'], { cwd: projectRoot });
|
|
43
|
+
console.log(stdout);
|
|
44
|
+
|
|
45
|
+
if (stdout.includes('Core optimization cycle completed smoothly')) {
|
|
46
|
+
console.log(`✅ Test passed for ${playground}`);
|
|
47
|
+
} else {
|
|
48
|
+
console.error(`❌ Test failed for ${playground}: Expected success message not found.`);
|
|
49
|
+
allTestsPassed = false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Add specific assertions for each playground if needed
|
|
53
|
+
if (playground === 'playgrounds/basic') {
|
|
54
|
+
if (!stdout.includes('Remove 1 unused dependencies:')) {
|
|
55
|
+
console.error(`❌ Basic playground test failed: Expected unused dependency detection.`);
|
|
56
|
+
allTestsPassed = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error(`❌ Test failed for ${playground}:`, error.message);
|
|
62
|
+
console.error(error.stdout);
|
|
63
|
+
allTestsPassed = false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (allTestsPassed) {
|
|
68
|
+
console.log('\n🎉 All playground tests completed successfully!');
|
|
69
|
+
process.exit(0);
|
|
70
|
+
} else {
|
|
71
|
+
console.error('\n🔥 Some playground tests failed.');
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
runTest();
|