entkapp 5.2.4 → 5.3.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/README.md +30 -37
- package/bin/cli.js +1 -1
- package/entkapp/config.json +18 -2
- package/entkapp/plugins/README.md +52 -12
- package/package.json +2 -2
- package/schema.json +87 -3
- package/src/index.js +36 -1
- package/src/resolution/ConfigLoader.js +88 -2
- package/src/resolution/DepencyResolver.js +15 -0
- package/src/resolution/DependencyProfiler.js +41 -23
- package/src/resolution/PathMapper.js +42 -9
- package/src/resolution/TSConfigLoader.js +96 -10
- package/src/resolution/WorkSpaceGraph.js +178 -3
- package/src/resolution/WorkspaceDiagnostic.js +137 -3
package/README.md
CHANGED
|
@@ -1,75 +1,68 @@
|
|
|
1
|
-
# 🕸️ entkapp Ultimate v5.
|
|
1
|
+
# 🕸️ entkapp Ultimate v5.3.0
|
|
2
2
|
|
|
3
|
-
> **The Ultimate Enterprise Codebase Janitor.**
|
|
3
|
+
> **The Ultimate Enterprise Codebase Janitor.** High-speed OXC integration, type-aware analysis, and automated structural healing. Fully standalone architectural orchestrator.
|
|
4
4
|
|
|
5
5
|
  
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
`entkapp`
|
|
9
|
+
`entkapp` is a next-generation static code analysis engine. It identifies unused files, dead exports, circular dependencies, and security risks faster than any other tool in the ecosystem..
|
|
10
10
|
|
|
11
|
-
## 🚀
|
|
11
|
+
## 🚀 Why entkapp?
|
|
12
12
|
|
|
13
|
-
* **⚡ Blazing Fast:**
|
|
14
|
-
* **🔌
|
|
15
|
-
* **💀 True Dead Code Detection:**
|
|
16
|
-
* **🔄 Circular Dependency Detection:**
|
|
17
|
-
* **🔐 Secrets Scanning:**
|
|
18
|
-
* **🛠️ Automated Structural Healing:**
|
|
13
|
+
* **⚡ Blazing Fast:** Leverages the Rust-based `oxc-parser` for ultra-fast AST traversal, combined with the TypeScript Compiler API for deep semantic analysis.
|
|
14
|
+
* **🔌 Massive Plugin System:** Over 98 built-in plugins (React, Vue, Svelte, Angular, Next.js, Nuxt, SvelteKit, Astro, Vite, Webpack, Turbo, Nx, Tailwind, ESLint, Prettier, and many more).
|
|
15
|
+
* **💀 True Dead Code Detection:** Advanced graph-based analysis to identify genuinely orphaned code.
|
|
16
|
+
* **🔄 Circular Dependency Detection:** High-performance detection of circular dependencies using Tarjan's algorithm.
|
|
17
|
+
* **🔐 Secrets Scanning:** Automatic detection of hardcoded API keys, tokens, and credentials.
|
|
18
|
+
* **🛠️ Automated Structural Healing:** Automatically fixes dependency issues and structural errors with Git-based rollback protection.
|
|
19
19
|
|
|
20
20
|
## 📦 Installation
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
#
|
|
23
|
+
# Run directly without installation (recommended)
|
|
24
24
|
npx entkapp
|
|
25
25
|
|
|
26
|
-
#
|
|
26
|
+
# Or install it globally
|
|
27
27
|
npm install -g entkapp
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
## 🛠️
|
|
30
|
+
## 🛠️ Usage
|
|
31
31
|
|
|
32
|
-
###
|
|
33
|
-
|
|
32
|
+
### Interactive Mode (Default)
|
|
33
|
+
Starts the interactive analysis:
|
|
34
34
|
```bash
|
|
35
|
-
npx entkapp
|
|
35
|
+
npx entkapp -r
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
### Headless
|
|
39
|
-
Ideal
|
|
38
|
+
### Headless Analysis Mode
|
|
39
|
+
Ideal for CI/CD pipelines. Performs analysis without prompts and outputs JSON:
|
|
40
40
|
```bash
|
|
41
41
|
npx entkapp --analyze
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
### Auto-Fix
|
|
45
|
-
|
|
44
|
+
### Auto-Fix Mode
|
|
45
|
+
Automatically resolves dependency conflicts and structural issues:
|
|
46
46
|
```bash
|
|
47
47
|
npx entkapp --fix
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
###
|
|
50
|
+
### Additional Options
|
|
51
51
|
```bash
|
|
52
|
-
npx entkapp --cwd ./
|
|
53
|
-
npx entkapp --verbose #
|
|
54
|
-
npx entkapp --version
|
|
55
|
-
npx entkapp --help
|
|
52
|
+
npx entkapp --cwd ./my-project -r # Analyzes a specific directory
|
|
53
|
+
npx entkapp --verbose -r # Enables detailed logging
|
|
54
|
+
npx entkapp --version # Displays the version number
|
|
55
|
+
npx entkapp --help # Displays the help panel
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
## 🧠
|
|
58
|
+
## 🧠 Advanced Architecture
|
|
59
59
|
|
|
60
60
|
### Incremental Caching
|
|
61
|
-
`entkapp`
|
|
61
|
+
`entkapp` uses a persistent graph state layer (`.entkapp-cache`). It calculates SHA-256 hashes of file buffers to skip AST parsing for unchanged files.
|
|
62
62
|
|
|
63
63
|
### Parallel Analysis
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
### Pluggable Architecture
|
|
67
|
-
Die Engine ist modular aufgebaut:
|
|
68
|
-
- **`EntkappEngine`**: Der zentrale Orchestrator.
|
|
69
|
-
- **`PluginRegistry`**: Verwaltet den Lebenszyklus der Framework-Plugins.
|
|
70
|
-
- **`ASTAnalyzer` / `OxcAnalyzer`**: Multi-pass AST-Walker für tiefgehende Analysen.
|
|
71
|
-
- **`SelfHealer`**: Transaktionales Refactoring mit Git-State-Capture und Rollback.
|
|
64
|
+
For larger codebases, `entkapp` automatically distributes the workload across multiple CPU threads using Node.js Worker Threads.
|
|
72
65
|
|
|
73
|
-
## 📄
|
|
66
|
+
## 📄 License
|
|
74
67
|
|
|
75
|
-
Apache-2.0
|
|
68
|
+
Apache-2.0
|
package/bin/cli.js
CHANGED
|
@@ -109,7 +109,7 @@ async function bootstrap() {
|
|
|
109
109
|
try {
|
|
110
110
|
const { ConfigLoader } = await import('../src/resolution/ConfigLoader.js');
|
|
111
111
|
const loader = new ConfigLoader(targetCwd);
|
|
112
|
-
localConfig = await loader.loadConfig(
|
|
112
|
+
localConfig = await loader.loadConfig();
|
|
113
113
|
} catch (e) {}
|
|
114
114
|
|
|
115
115
|
// Merge options with local config
|
package/entkapp/config.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://unpkg.com/entkapp@
|
|
2
|
+
"$schema": "https://unpkg.com/entkapp@latest/schema.json",
|
|
3
3
|
"interface": "CLI",
|
|
4
4
|
"useBuiltinPlugins": true,
|
|
5
5
|
"useCustomPlugins": true,
|
|
@@ -26,5 +26,21 @@
|
|
|
26
26
|
"dist",
|
|
27
27
|
"build",
|
|
28
28
|
"coverage"
|
|
29
|
-
]
|
|
29
|
+
],
|
|
30
|
+
"entryPoints": [],
|
|
31
|
+
"workspace": {
|
|
32
|
+
"enabled": false,
|
|
33
|
+
"enforcePackageBoundaries": true,
|
|
34
|
+
"checkVersionMismatches": true
|
|
35
|
+
},
|
|
36
|
+
"tsconfig": {
|
|
37
|
+
"filename": "tsconfig.json",
|
|
38
|
+
"followProjectReferences": true,
|
|
39
|
+
"resolveAliases": true
|
|
40
|
+
},
|
|
41
|
+
"buildScripts": {
|
|
42
|
+
"scanWorkspaceScripts": true,
|
|
43
|
+
"scanCIWorkflows": true,
|
|
44
|
+
"scanConfigFiles": true
|
|
45
|
+
}
|
|
30
46
|
}
|
|
@@ -1,19 +1,59 @@
|
|
|
1
|
-
# entkapp Plugins
|
|
1
|
+
# 🔌 entkapp Plugins (v5.3.0)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Dieses Verzeichnis dient für deine benutzerdefinierten Plugins. entkapp lädt automatisch alle `.js` oder `.mjs` Dateien aus diesem Ordner, wenn `useCustomPlugins` in deiner `config.json` auf `true` gesetzt ist.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 📦 Integrierte Framework- & Tool-Plugins (98 Gesamt)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **Nuxt**: Supports auto-imports and server routes.
|
|
9
|
-
- **Remix**: Maps loaders, actions, and root exports.
|
|
10
|
-
- **SvelteKit**: Tracks `+page`, `+layout`, and server-side scripts.
|
|
11
|
-
- **Astro**: Analyzes `.astro` files and static paths.
|
|
7
|
+
entkapp verfügt über eine massive Bibliothek von 98 integrierten Plugins, die automatisch die Architektur deiner Codebase erkennen und analysieren.
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
### 🖼️ Frontend Frameworks & Meta-Frameworks
|
|
10
|
+
- **Next.js** (App Router, Pages Router)
|
|
11
|
+
- **React**, **Preact**, **Solid**, **Qwik**, **Lit**, **Angular**, **Vue**, **Svelte**
|
|
12
|
+
- **Nuxt**, **Remix**, **SvelteKit**, **Astro**, **Vitepress**, **Gatsby**, **RedwoodJS**
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
### 🌐 State Management & Routing
|
|
15
|
+
- **Redux**, **Zustand**, **Jotai**, **Recoil**, **MobX**, **Pinia**, **TanStack Query**
|
|
16
|
+
- **React Router**, **TanStack Router**, **Vue Router**
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
### 🛠️ Build Tools & Monorepo
|
|
19
|
+
- **Vite**, **Esbuild**, **Rollup**, **Webpack**, **Parcel**, **TypeScript**, **Babel**, **SWC**
|
|
20
|
+
- **Turbo**, **Nx**, **Lerna**, **Rush**, **Moon**, **Bazel**
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
### 🎨 Styling & UI Components
|
|
23
|
+
- **Tailwind CSS**, **PostCSS**, **UnoCSS**, **Stylelint**, **RTLCSS**
|
|
24
|
+
- **Ant Design (Antd)**, **Material UI (Mui)**, **Shadcn/UI**, **Radix UI**, **Chakra UI**
|
|
25
|
+
- **Framer Motion**, **GSAP**
|
|
26
|
+
|
|
27
|
+
### 🧪 Testing & Quality
|
|
28
|
+
- **Jest**, **Vitest**, **Playwright**, **Cypress**, **Storybook**, **MSW**
|
|
29
|
+
- **ESLint**, **Prettier**, **Biome**, **Oxlint**, **Husky**, **Lint-Staged**, **Commitlint**, **Changesets**
|
|
30
|
+
|
|
31
|
+
### ☁️ Backend, API & Database
|
|
32
|
+
- **Express**, **Fastify**, **NestJS**, **Hono**, **Koa**, **Elysia**, **Hapi**, **Grammy**
|
|
33
|
+
- **GraphQL**, **Apollo**, **TRPC**, **Socket.io**
|
|
34
|
+
- **Prisma**, **Drizzle**, **Mongoose**, **TypeORM**, **Supabase**, **Firebase**, **Clerk**
|
|
35
|
+
|
|
36
|
+
### 🔧 Infrastructure & Dev Tools
|
|
37
|
+
- **GitHub Actions**, **Docker**, **Terraform**, **EditorConfig**, **Dotenv**
|
|
38
|
+
- **Nvm**, **Volta**, **Pnpm**, **Yarn**, **Bun**
|
|
39
|
+
- **Swiper**, **Quill**, **Envelop**, **Nitro Modules**, **CKEditor Engine**
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 🛠️ Eigene Plugins erstellen
|
|
44
|
+
|
|
45
|
+
Du kannst die Funktionalität von entkapp erweitern, indem du eine Plugin-Klasse erstellst:
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
export default class MyCustomPlugin {
|
|
49
|
+
name = 'my-custom-plugin';
|
|
50
|
+
|
|
51
|
+
async onAnalyze(context) {
|
|
52
|
+
// Deine Logik hier
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## 📜 Dokumentation
|
|
58
|
+
|
|
59
|
+
Für eine detaillierte Anleitung zur Plugin-Entwicklung besuche bitte den [Plugin Development Guide](https://dreamlongyt.github.io/entkapp/).
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
3
|
"name": "entkapp",
|
|
4
|
-
"version": "5.
|
|
5
|
-
"description": "The Ultimate Enterprise Codebase Janitor.
|
|
4
|
+
"version": "5.3.0",
|
|
5
|
+
"description": "The Ultimate Enterprise Codebase Janitor. High-speed OXC integration, type-aware analysis, and automated structural healing. Fully standalone architectural orchestrator.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"bin": {
|
package/schema.json
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
{
|
|
3
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
3
|
"title": "entkapp Configuration Schema",
|
|
@@ -46,11 +45,15 @@
|
|
|
46
45
|
"type": "array",
|
|
47
46
|
"items": {
|
|
48
47
|
"type": "string",
|
|
49
|
-
"enum": [
|
|
48
|
+
"enum": [
|
|
49
|
+
"nextjs", "nuxt", "remix", "sveltekit", "astro",
|
|
50
|
+
"typescript", "vite", "webpack", "react", "vue", "angular",
|
|
51
|
+
"turbo", "nx", "lerna"
|
|
52
|
+
]
|
|
50
53
|
}
|
|
51
54
|
},
|
|
52
55
|
"ignoreDependencies": {
|
|
53
|
-
"description": "Dependencies that should never be reported as unused.",
|
|
56
|
+
"description": "Dependencies that should never be reported as unused. Supports glob patterns like '@types/*'.",
|
|
54
57
|
"type": "array",
|
|
55
58
|
"items": {
|
|
56
59
|
"type": "string"
|
|
@@ -64,6 +67,87 @@
|
|
|
64
67
|
"type": "string"
|
|
65
68
|
},
|
|
66
69
|
"default": ["node_modules", ".git", "dist", "build", "coverage"]
|
|
70
|
+
},
|
|
71
|
+
"entryPoints": {
|
|
72
|
+
"description": "Explicit entry point files to protect from orphan detection. Supports glob patterns.",
|
|
73
|
+
"type": "array",
|
|
74
|
+
"items": {
|
|
75
|
+
"type": "string"
|
|
76
|
+
},
|
|
77
|
+
"default": []
|
|
78
|
+
},
|
|
79
|
+
"workspace": {
|
|
80
|
+
"description": "Monorepo workspace configuration.",
|
|
81
|
+
"type": "object",
|
|
82
|
+
"properties": {
|
|
83
|
+
"enabled": {
|
|
84
|
+
"description": "Force-enable monorepo workspace mode (auto-detected from package.json workspaces or pnpm-workspace.yaml).",
|
|
85
|
+
"type": "boolean",
|
|
86
|
+
"default": false
|
|
87
|
+
},
|
|
88
|
+
"packages": {
|
|
89
|
+
"description": "Override workspace package glob patterns (e.g. ['packages/*', 'apps/*']).",
|
|
90
|
+
"type": "array",
|
|
91
|
+
"items": { "type": "string" }
|
|
92
|
+
},
|
|
93
|
+
"ignoredPackages": {
|
|
94
|
+
"description": "Workspace packages to exclude from analysis.",
|
|
95
|
+
"type": "array",
|
|
96
|
+
"items": { "type": "string" }
|
|
97
|
+
},
|
|
98
|
+
"enforcePackageBoundaries": {
|
|
99
|
+
"description": "Report errors when a workspace package imports another without declaring it as a dependency.",
|
|
100
|
+
"type": "boolean",
|
|
101
|
+
"default": true
|
|
102
|
+
},
|
|
103
|
+
"checkVersionMismatches": {
|
|
104
|
+
"description": "Report warnings when the same dependency has different versions across workspace packages.",
|
|
105
|
+
"type": "boolean",
|
|
106
|
+
"default": true
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"tsconfig": {
|
|
111
|
+
"description": "TypeScript configuration options.",
|
|
112
|
+
"type": "object",
|
|
113
|
+
"properties": {
|
|
114
|
+
"filename": {
|
|
115
|
+
"description": "Path to the root tsconfig.json file.",
|
|
116
|
+
"type": "string",
|
|
117
|
+
"default": "tsconfig.json"
|
|
118
|
+
},
|
|
119
|
+
"followProjectReferences": {
|
|
120
|
+
"description": "Follow TypeScript project references to discover workspace packages.",
|
|
121
|
+
"type": "boolean",
|
|
122
|
+
"default": true
|
|
123
|
+
},
|
|
124
|
+
"resolveAliases": {
|
|
125
|
+
"description": "Use tsconfig paths/baseUrl to resolve module aliases.",
|
|
126
|
+
"type": "boolean",
|
|
127
|
+
"default": true
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"buildScripts": {
|
|
132
|
+
"description": "Build script analysis configuration.",
|
|
133
|
+
"type": "object",
|
|
134
|
+
"properties": {
|
|
135
|
+
"scanWorkspaceScripts": {
|
|
136
|
+
"description": "Also scan package.json scripts in all workspace packages (not just root).",
|
|
137
|
+
"type": "boolean",
|
|
138
|
+
"default": true
|
|
139
|
+
},
|
|
140
|
+
"scanCIWorkflows": {
|
|
141
|
+
"description": "Scan .github/workflows YAML files for tool usage.",
|
|
142
|
+
"type": "boolean",
|
|
143
|
+
"default": true
|
|
144
|
+
},
|
|
145
|
+
"scanConfigFiles": {
|
|
146
|
+
"description": "Detect tool usage from config files (vite.config.ts, jest.config.js, etc.).",
|
|
147
|
+
"type": "boolean",
|
|
148
|
+
"default": true
|
|
149
|
+
}
|
|
150
|
+
}
|
|
67
151
|
}
|
|
68
152
|
}
|
|
69
153
|
}
|
package/src/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { CodeSmellAnalyzer } from './analyzers/CodeSmellAnalyzer.js';
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* ============================================================================
|
|
10
|
-
* 📦 entkapp
|
|
10
|
+
* 📦 entkapp v5.3.0: Unified Architectural Refactoring Orchestrator
|
|
11
11
|
* ============================================================================
|
|
12
12
|
* Main execution bridge managing multi-pass compilation cycles, semantic cross-linking,
|
|
13
13
|
* supply-chain validation audits, and automated structural healing rollbacks.
|
|
@@ -64,6 +64,11 @@ export class RefactoringEngine {
|
|
|
64
64
|
this.resolver = new DependencyResolver(this.context, this.pathMapper, this.workspaceGraph);
|
|
65
65
|
this.circularDetector = new CircularDetector(this.context);
|
|
66
66
|
|
|
67
|
+
// Lazy import DependencyProfiler
|
|
68
|
+
import('./resolution/DependencyProfiler.js').then(({ DependencyProfiler }) => {
|
|
69
|
+
this.dependencyProfiler = new DependencyProfiler(this.context);
|
|
70
|
+
}).catch(() => {});
|
|
71
|
+
|
|
67
72
|
// Stage 3: Wire official AST Syntax parsers and framework processors
|
|
68
73
|
this.analyzer = new ASTAnalyzer(this.context);
|
|
69
74
|
this.oxcAnalyzer = new OxcAnalyzer(this.context);
|
|
@@ -115,6 +120,16 @@ export class RefactoringEngine {
|
|
|
115
120
|
await this.workspaceGraph.initializeWorkspaceMesh();
|
|
116
121
|
if (this.context.isWorkspaceEnabled) {
|
|
117
122
|
console.log(ansis.dim('🌐 Monorepo workspace detected – mapping package mesh layers...'));
|
|
123
|
+
// Expose workspaceGraph on context for WorkspaceDiagnostic and other components
|
|
124
|
+
this.context.workspaceGraph = this.workspaceGraph;
|
|
125
|
+
// Reload PathMapper aliases now that workspace roots are known
|
|
126
|
+
await this.pathMapper.loadMappings(this.context.tsconfigFilename);
|
|
127
|
+
if (this.context.verbose) {
|
|
128
|
+
console.log(`[Workspace] Found ${this.workspaceGraph.packageManifests.size} workspace packages:`);
|
|
129
|
+
for (const [dir, manifest] of this.workspaceGraph.packageManifests.entries()) {
|
|
130
|
+
console.log(ansis.dim(` • ${manifest.name || dir}`));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
118
133
|
}
|
|
119
134
|
|
|
120
135
|
// UPGRADE: Always clear cache for fresh analysis run
|
|
@@ -141,6 +156,12 @@ export class RefactoringEngine {
|
|
|
141
156
|
this.context.metrics.totalFilesScanned = fileList.length;
|
|
142
157
|
|
|
143
158
|
// Identify meta-framework setups (Next.js, Remix, Nuxt, etc.)
|
|
159
|
+
if (this.dependencyProfiler) {
|
|
160
|
+
const usedDeps = await this.dependencyProfiler.traceImplicitInvocations(this.context.cwd);
|
|
161
|
+
for (const dep of usedDeps) {
|
|
162
|
+
this.context.usedExternalPackages.add(dep);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
144
165
|
const activeFrameworkEcosystems = await this.magicDetector.identifyActiveProjectEcosystems(this.context.cwd);
|
|
145
166
|
|
|
146
167
|
// Separate explicit configuration packages out for targeted supply chain security checks
|
|
@@ -985,6 +1006,20 @@ export class RefactoringEngine {
|
|
|
985
1006
|
peerDependencies: Object.keys(data.peerDependencies || {}),
|
|
986
1007
|
optionalDependencies: Object.keys(data.optionalDependencies || {})
|
|
987
1008
|
});
|
|
1009
|
+
|
|
1010
|
+
// Also register workspace manifests if not already done
|
|
1011
|
+
if (this.context.isWorkspaceEnabled && this.workspaceGraph) {
|
|
1012
|
+
for (const [dir, manifest] of this.workspaceGraph.packageManifests.entries()) {
|
|
1013
|
+
if (!this.context.manifestDependencies.has(manifest.manifestPath)) {
|
|
1014
|
+
this.context.manifestDependencies.set(manifest.manifestPath, {
|
|
1015
|
+
dependencies: Object.keys(manifest.dependencies || {}),
|
|
1016
|
+
devDependencies: Object.keys(manifest.devDependencies || {}),
|
|
1017
|
+
peerDependencies: Object.keys(manifest.peerDependencies || {}),
|
|
1018
|
+
optionalDependencies: Object.keys(manifest.optionalDependencies || {})
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
988
1023
|
} catch (e) {}
|
|
989
1024
|
}
|
|
990
1025
|
|
|
@@ -1,4 +1,90 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ConfigLoader
|
|
6
|
+
* Loads and merges entkapp configuration from multiple sources:
|
|
7
|
+
* - entkapp/config.json (project config)
|
|
8
|
+
* - entkapp.config.js / entkapp.config.mjs (JS config)
|
|
9
|
+
* - package.json "entkapp" key
|
|
10
|
+
* - CLI flags (passed as overrides)
|
|
11
|
+
*/
|
|
1
12
|
export class ConfigLoader {
|
|
2
|
-
constructor(cwd) {
|
|
3
|
-
|
|
13
|
+
constructor(cwd) {
|
|
14
|
+
this.cwd = cwd;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async loadConfig(overrides = {}) {
|
|
18
|
+
let config = this._defaultConfig();
|
|
19
|
+
|
|
20
|
+
// 1. Try entkapp/config.json
|
|
21
|
+
const jsonConfigPath = path.join(this.cwd, 'entkapp', 'config.json');
|
|
22
|
+
try {
|
|
23
|
+
const raw = await fs.readFile(jsonConfigPath, 'utf8');
|
|
24
|
+
// Strip comments from JSON (JSONC support)
|
|
25
|
+
const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
26
|
+
const parsed = JSON.parse(stripped);
|
|
27
|
+
config = this._merge(config, parsed);
|
|
28
|
+
} catch (e) {}
|
|
29
|
+
|
|
30
|
+
// 2. Try entkapp.config.js / entkapp.config.mjs
|
|
31
|
+
for (const configFile of ['entkapp.config.mjs', 'entkapp.config.js', 'entkapp.config.cjs']) {
|
|
32
|
+
const jsConfigPath = path.join(this.cwd, configFile);
|
|
33
|
+
try {
|
|
34
|
+
const mod = await import(jsConfigPath);
|
|
35
|
+
const jsConfig = mod.default || mod;
|
|
36
|
+
if (typeof jsConfig === 'object') {
|
|
37
|
+
config = this._merge(config, jsConfig);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 3. Try package.json "entkapp" key
|
|
44
|
+
const pkgPath = path.join(this.cwd, 'package.json');
|
|
45
|
+
try {
|
|
46
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
47
|
+
if (pkg.entkapp && typeof pkg.entkapp === 'object') {
|
|
48
|
+
config = this._merge(config, pkg.entkapp);
|
|
49
|
+
}
|
|
50
|
+
} catch (e) {}
|
|
51
|
+
|
|
52
|
+
// 4. Apply CLI overrides
|
|
53
|
+
config = this._merge(config, overrides);
|
|
54
|
+
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
_defaultConfig() {
|
|
59
|
+
return {
|
|
60
|
+
interface: 'CLI',
|
|
61
|
+
useBuiltinPlugins: true,
|
|
62
|
+
useCustomPlugins: true,
|
|
63
|
+
options: {
|
|
64
|
+
verbose: false,
|
|
65
|
+
fastMode: true,
|
|
66
|
+
selfHealing: true
|
|
67
|
+
},
|
|
68
|
+
enabledPlugins: [],
|
|
69
|
+
ignoreDependencies: ['entkapp', '@types/*'],
|
|
70
|
+
exclude: ['node_modules', '.git', 'dist', 'build', 'coverage'],
|
|
71
|
+
entryPoints: [],
|
|
72
|
+
workspace: false
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_merge(base, override) {
|
|
77
|
+
if (!override || typeof override !== 'object') return base;
|
|
78
|
+
const result = { ...base };
|
|
79
|
+
for (const key of Object.keys(override)) {
|
|
80
|
+
if (key === 'options' && typeof override[key] === 'object') {
|
|
81
|
+
result.options = { ...(base.options || {}), ...override[key] };
|
|
82
|
+
} else if (Array.isArray(override[key])) {
|
|
83
|
+
result[key] = override[key];
|
|
84
|
+
} else {
|
|
85
|
+
result[key] = override[key];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
4
90
|
}
|
|
@@ -25,8 +25,23 @@ export class DependencyResolver {
|
|
|
25
25
|
resolveModulePath(sourceFile, specifier) {
|
|
26
26
|
const cleanSource = this.normalizePath(sourceFile);
|
|
27
27
|
|
|
28
|
+
// Check if it's a workspace package reference
|
|
29
|
+
if (this.context.isWorkspaceEnabled && this.workspaceGraph && this.workspaceGraph.isLocalWorkspaceSpecifier(specifier)) {
|
|
30
|
+
const match = this.workspaceGraph.getWorkspacePackageMatch(specifier);
|
|
31
|
+
if (match && match.entryPoints && match.entryPoints.length > 0) {
|
|
32
|
+
// Return the first entry point for the workspace package
|
|
33
|
+
return this.normalizePath(match.entryPoints[0]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
// UPGRADE: Use PathMapper for sophisticated resolution (TS-to-JS, aliases, etc.)
|
|
29
38
|
if (this.pathMapper) {
|
|
39
|
+
// Allow pathMapper to resolve aliases directly from specifier
|
|
40
|
+
const aliasResolved = this.pathMapper.resolvePath(specifier);
|
|
41
|
+
if (aliasResolved && aliasResolved !== specifier && existsSync(aliasResolved)) {
|
|
42
|
+
return this.normalizePath(aliasResolved);
|
|
43
|
+
}
|
|
44
|
+
|
|
30
45
|
const dir = path.dirname(cleanSource);
|
|
31
46
|
const target = path.resolve(dir, specifier);
|
|
32
47
|
const resolved = this.pathMapper.resolvePath(target);
|
|
@@ -146,9 +146,44 @@ export class DependencyProfiler {
|
|
|
146
146
|
const usedPackages = new Set();
|
|
147
147
|
const usedBinaries = new Set(); // NEW: Track which binaries are actually used
|
|
148
148
|
|
|
149
|
-
// 1. Scan package.json scripts
|
|
149
|
+
// 1. Scan package.json scripts (Root)
|
|
150
|
+
await this._scanDirectoryForConfigs(projectRoot, usedPackages, usedBinaries);
|
|
151
|
+
|
|
152
|
+
// 1.5 Scan Workspace package.json scripts and configs
|
|
153
|
+
if (this.context.isWorkspaceEnabled && this.context.monorepoPackageRoots) {
|
|
154
|
+
for (const workspaceRoot of this.context.monorepoPackageRoots) {
|
|
155
|
+
await this._scanDirectoryForConfigs(workspaceRoot, usedPackages, usedBinaries);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 2. Scan CI workflows
|
|
160
|
+
try {
|
|
161
|
+
const githubWorkflows = path.join(projectRoot, '.github/workflows');
|
|
162
|
+
const files = await fs.readdir(githubWorkflows).catch(() => []);
|
|
163
|
+
for (const file of files) {
|
|
164
|
+
if (file.endsWith('.yml') || file.endsWith('.yaml')) {
|
|
165
|
+
const content = await fs.readFile(path.join(githubWorkflows, file), 'utf8');
|
|
166
|
+
this.extractPackagesFromScript(content, usedPackages, usedBinaries);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {}
|
|
170
|
+
|
|
171
|
+
// 4. Identify Unused Binaries (Root)
|
|
172
|
+
await this._identifyUnusedBinaries(projectRoot, usedBinaries);
|
|
173
|
+
|
|
174
|
+
// 4.5 Identify Unused Binaries (Workspaces)
|
|
175
|
+
if (this.context.isWorkspaceEnabled && this.context.monorepoPackageRoots) {
|
|
176
|
+
for (const workspaceRoot of this.context.monorepoPackageRoots) {
|
|
177
|
+
await this._identifyUnusedBinaries(workspaceRoot, usedBinaries);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return usedPackages;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async _scanDirectoryForConfigs(dir, usedPackages, usedBinaries) {
|
|
150
185
|
try {
|
|
151
|
-
const pkgJsonPath = path.join(
|
|
186
|
+
const pkgJsonPath = path.join(dir, 'package.json');
|
|
152
187
|
const pkg = JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'));
|
|
153
188
|
|
|
154
189
|
if (pkg.scripts) {
|
|
@@ -157,7 +192,6 @@ export class DependencyProfiler {
|
|
|
157
192
|
}
|
|
158
193
|
}
|
|
159
194
|
|
|
160
|
-
// Detect @types/* usage
|
|
161
195
|
if (pkg.dependencies || pkg.devDependencies) {
|
|
162
196
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
163
197
|
for (const depName of Object.keys(allDeps)) {
|
|
@@ -168,21 +202,8 @@ export class DependencyProfiler {
|
|
|
168
202
|
}
|
|
169
203
|
} catch (e) {}
|
|
170
204
|
|
|
171
|
-
// 2. Scan CI workflows
|
|
172
205
|
try {
|
|
173
|
-
const
|
|
174
|
-
const files = await fs.readdir(githubWorkflows).catch(() => []);
|
|
175
|
-
for (const file of files) {
|
|
176
|
-
if (file.endsWith('.yml') || file.endsWith('.yaml')) {
|
|
177
|
-
const content = await fs.readFile(path.join(githubWorkflows, file), 'utf8');
|
|
178
|
-
this.extractPackagesFromScript(content, usedPackages, usedBinaries);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
} catch (e) {}
|
|
182
|
-
|
|
183
|
-
// 3. Scan config files
|
|
184
|
-
try {
|
|
185
|
-
const dirEntries = await fs.readdir(projectRoot, { withFileTypes: true });
|
|
206
|
+
const dirEntries = await fs.readdir(dir, { withFileTypes: true });
|
|
186
207
|
for (const entry of dirEntries) {
|
|
187
208
|
if (!entry.isFile()) continue;
|
|
188
209
|
const fileName = entry.name;
|
|
@@ -194,15 +215,14 @@ export class DependencyProfiler {
|
|
|
194
215
|
}
|
|
195
216
|
}
|
|
196
217
|
} catch (e) {}
|
|
218
|
+
}
|
|
197
219
|
|
|
198
|
-
|
|
199
|
-
// Scan node_modules/.bin for all available binaries
|
|
220
|
+
async _identifyUnusedBinaries(dir, usedBinaries) {
|
|
200
221
|
try {
|
|
201
|
-
const binDir = path.join(
|
|
222
|
+
const binDir = path.join(dir, 'node_modules', '.bin');
|
|
202
223
|
const availableBinaries = await fs.readdir(binDir).catch(() => []);
|
|
203
224
|
|
|
204
225
|
for (const bin of availableBinaries) {
|
|
205
|
-
// Skip hidden files or package manager internal bins
|
|
206
226
|
if (bin.startsWith('.') || ['npm', 'pnpm', 'yarn', 'bun'].includes(bin)) continue;
|
|
207
227
|
|
|
208
228
|
if (!usedBinaries.has(bin)) {
|
|
@@ -210,8 +230,6 @@ export class DependencyProfiler {
|
|
|
210
230
|
}
|
|
211
231
|
}
|
|
212
232
|
} catch (e) {}
|
|
213
|
-
|
|
214
|
-
return usedPackages;
|
|
215
233
|
}
|
|
216
234
|
|
|
217
235
|
extractPackagesFromScript(script, packageCollector, binaryCollector) {
|
|
@@ -1,13 +1,35 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { TSConfigLoader } from './TSConfigLoader.js';
|
|
3
4
|
|
|
4
5
|
export class PathMapper {
|
|
5
6
|
constructor(context) {
|
|
6
7
|
this.context = context;
|
|
8
|
+
this.aliasMappers = []; // list of alias mapping functions
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
async loadMappings() {
|
|
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
|
+
}
|
|
11
33
|
}
|
|
12
34
|
|
|
13
35
|
/**
|
|
@@ -18,25 +40,36 @@ export class PathMapper {
|
|
|
18
40
|
resolvePath(p) {
|
|
19
41
|
if (!p || typeof p !== 'string') return p;
|
|
20
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
|
+
|
|
21
54
|
// FIX 1: If the import ends with .js, translate it to .ts for the search
|
|
22
|
-
if (
|
|
23
|
-
const tsPath =
|
|
55
|
+
if (resolvedP.endsWith('.js')) {
|
|
56
|
+
const tsPath = resolvedP.slice(0, -3) + '.ts';
|
|
24
57
|
if (fs.existsSync(tsPath)) return tsPath;
|
|
25
58
|
}
|
|
26
59
|
|
|
27
60
|
// FIX 2: If the import ends with .jsx, translate it to .tsx for the search
|
|
28
|
-
if (
|
|
29
|
-
const tsxPath =
|
|
61
|
+
if (resolvedP.endsWith('.jsx')) {
|
|
62
|
+
const tsxPath = resolvedP.slice(0, -4) + '.tsx';
|
|
30
63
|
if (fs.existsSync(tsxPath)) return tsxPath;
|
|
31
64
|
}
|
|
32
65
|
|
|
33
66
|
// FIX 3: Support for directory imports (z.B. ./adapters -> ./adapters/index.ts)
|
|
34
67
|
try {
|
|
35
|
-
const stat = fs.statSync(
|
|
68
|
+
const stat = fs.statSync(resolvedP);
|
|
36
69
|
if (stat.isDirectory()) {
|
|
37
70
|
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
38
71
|
for (const ext of extensions) {
|
|
39
|
-
const indexPath = path.join(
|
|
72
|
+
const indexPath = path.join(resolvedP, `index${ext}`);
|
|
40
73
|
if (fs.existsSync(indexPath)) return indexPath;
|
|
41
74
|
}
|
|
42
75
|
}
|
|
@@ -44,6 +77,6 @@ export class PathMapper {
|
|
|
44
77
|
// File does not exist or is not a directory, continue with default
|
|
45
78
|
}
|
|
46
79
|
|
|
47
|
-
return
|
|
80
|
+
return resolvedP;
|
|
48
81
|
}
|
|
49
82
|
}
|
|
@@ -9,39 +9,102 @@ export class TSConfigLoader {
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
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
|
|
12
14
|
* @returns {Object|null} Parsed config oder null.
|
|
13
15
|
*/
|
|
14
|
-
load() {
|
|
15
|
-
const configPath = path.join(this.targetDir,
|
|
16
|
+
load(filename = 'tsconfig.json') {
|
|
17
|
+
const configPath = path.join(this.targetDir, filename);
|
|
16
18
|
if (!fs.existsSync(configPath)) return null;
|
|
17
19
|
|
|
18
20
|
try {
|
|
19
21
|
const content = fs.readFileSync(configPath, 'utf8');
|
|
20
|
-
const
|
|
22
|
+
const readResult = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
21
23
|
|
|
22
|
-
if (
|
|
24
|
+
if (readResult.error) {
|
|
23
25
|
return null;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
const parsed = ts.parseJsonConfigFileContent(
|
|
27
|
-
|
|
29
|
+
readResult.config,
|
|
28
30
|
ts.sys,
|
|
29
31
|
this.targetDir
|
|
30
32
|
);
|
|
31
33
|
|
|
34
|
+
// Attach raw config for reference inspection
|
|
35
|
+
parsed._rawConfig = readResult.config;
|
|
36
|
+
|
|
32
37
|
return parsed;
|
|
33
38
|
} catch (e) {
|
|
34
39
|
return null;
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
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
|
+
|
|
38
100
|
/**
|
|
39
101
|
* Erstellt eine Mapping-Funktion für Aliases aus der tsconfig.
|
|
102
|
+
* Berücksichtigt baseUrl, paths und project references.
|
|
40
103
|
* @param {Object} parsedConfig
|
|
41
104
|
* @returns {Function} Mapper function.
|
|
42
105
|
*/
|
|
43
106
|
getAliasMapper(parsedConfig) {
|
|
44
|
-
if (!parsedConfig || !parsedConfig.options
|
|
107
|
+
if (!parsedConfig || !parsedConfig.options) {
|
|
45
108
|
return (source) => source;
|
|
46
109
|
}
|
|
47
110
|
|
|
@@ -49,19 +112,31 @@ export class TSConfigLoader {
|
|
|
49
112
|
const base = baseUrl ? path.resolve(this.targetDir, baseUrl) : this.targetDir;
|
|
50
113
|
|
|
51
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', '.js', '.jsx'];
|
|
120
|
+
for (const ext of extensions) {
|
|
121
|
+
if (fs.existsSync(candidate + ext)) return candidate + ext;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return source;
|
|
125
|
+
}
|
|
126
|
+
|
|
52
127
|
for (const pattern in paths) {
|
|
53
|
-
const regexPattern = pattern.replace(
|
|
128
|
+
const regexPattern = pattern.replace(/\*/g, '(.*)');
|
|
54
129
|
const regex = new RegExp(`^${regexPattern}$`);
|
|
55
130
|
const match = source.match(regex);
|
|
56
131
|
|
|
57
132
|
if (match) {
|
|
58
133
|
const replacements = paths[pattern];
|
|
59
134
|
for (const replacement of replacements) {
|
|
60
|
-
const resolvedReplacement = replacement.replace(
|
|
135
|
+
const resolvedReplacement = replacement.replace(/\*/g, match[1] || '');
|
|
61
136
|
const fullPath = path.resolve(base, resolvedReplacement);
|
|
62
137
|
|
|
63
|
-
//
|
|
64
|
-
const extensions = ['', '.ts', '.tsx', '.js', '.jsx'];
|
|
138
|
+
// Check with common extensions
|
|
139
|
+
const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx', '/index.js'];
|
|
65
140
|
for (const ext of extensions) {
|
|
66
141
|
if (fs.existsSync(fullPath + ext)) {
|
|
67
142
|
return fullPath + ext;
|
|
@@ -73,4 +148,15 @@ export class TSConfigLoader {
|
|
|
73
148
|
return source;
|
|
74
149
|
};
|
|
75
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
|
+
}
|
|
76
162
|
}
|
|
@@ -1,8 +1,183 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
1
4
|
export class WorkspaceGraph {
|
|
2
5
|
constructor(context) {
|
|
3
6
|
this.context = context;
|
|
4
|
-
this.packageManifests = new Map();
|
|
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;
|
|
5
182
|
}
|
|
6
|
-
async initializeWorkspaceMesh() {}
|
|
7
|
-
markWorkspacePackagesAsUsed() {}
|
|
8
183
|
}
|
|
@@ -1,5 +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
|
+
*/
|
|
1
9
|
export class WorkspaceDiagnostic {
|
|
2
|
-
constructor(context) {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
}
|
|
5
139
|
}
|