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 CHANGED
@@ -1,75 +1,68 @@
1
- # 🕸️ entkapp Ultimate v5.2.4
1
+ # 🕸️ entkapp Ultimate v5.3.0
2
2
 
3
- > **The Ultimate Enterprise Codebase Janitor.** Faster than Knip with OXC integration, type-aware analysis, and automated structural healing. Fully standalone - solving what Knip cannot.
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
  ![Version](https://img.shields.io/npm/v/entkapp) ![License](https://img.shields.io/badge/license-Apache--2.0-green.svg) ![Performance](https://img.shields.io/badge/performance-OXC--Inside-blueviolet.svg)
6
6
 
7
7
  ---
8
8
 
9
- `entkapp` ist eine next-generation Engine zur statischen Code-Analyse, die die Stabilität der bewährten Version 7 mit den Hochleistungs-Features der Version 9 kombiniert. Sie findet ungenutzte Dateien, tote Exports, zirkuläre Abhängigkeiten und Sicherheitsrisiken schneller als jedes andere Tool im Ökosystem.
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
- ## 🚀 Warum entkapp?
11
+ ## 🚀 Why entkapp?
12
12
 
13
- * **⚡ Blazing Fast:** Nutzt den Rust-basierten `oxc-parser` für extrem schnelle AST-Traversierung, kombiniert mit der TypeScript Compiler API für tiefe semantische Analysen.
14
- * **🔌 Massives Plugin-System:** Über 80+ integrierte Plugins (React, Vue, Svelte, Angular, Next.js, Nuxt, SvelteKit, Astro, Vite, Webpack, Turbo, Nx, Tailwind, ESLint, Prettier und viele mehr).
15
- * **💀 True Dead Code Detection:** Fortschrittliche graphbasierte Analyse zur Identifizierung von wirklich verwaistem Code.
16
- * **🔄 Circular Dependency Detection:** Hochperformante Erkennung von zirkulären Abhängigkeiten mittels Tarjan's Algorithmus.
17
- * **🔐 Secrets Scanning:** Automatische Erkennung von hartkodierten API-Keys, Tokens und Anmeldedaten.
18
- * **🛠️ Automated Structural Healing:** Repariert Abhängigkeitsprobleme und strukturelle Fehler automatisch mit Git-basiertem Rollback-Schutz.
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
- # Direkt ausführen ohne Installation (empfohlen)
23
+ # Run directly without installation (recommended)
24
24
  npx entkapp
25
25
 
26
- # Oder global installieren
26
+ # Or install it globally
27
27
  npm install -g entkapp
28
28
  ```
29
29
 
30
- ## 🛠️ Nutzung
30
+ ## 🛠️ Usage
31
31
 
32
- ### Interaktiver Modus (Standard)
33
- Startet den interaktiven Analyse- und Scaffolding-Workflow:
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 Analyse-Modus
39
- Ideal für CI/CD-Pipelines. Führt die Analyse ohne Prompts durch und gibt JSON aus:
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 Modus
45
- Behebt automatisch Abhängigkeitskonflikte und strukturelle Probleme:
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
- ### Zusätzliche Optionen
50
+ ### Additional Options
51
51
  ```bash
52
- npx entkapp --cwd ./mein-projekt # Analysiert ein spezifisches Verzeichnis
53
- npx entkapp --verbose # Aktiviert detailliertes Logging
54
- npx entkapp --version # Zeigt die Versionsnummer
55
- npx entkapp --help # Zeigt das Hilfe-Panel
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
- ## 🧠 Fortgeschrittene Architektur
58
+ ## 🧠 Advanced Architecture
59
59
 
60
60
  ### Incremental Caching
61
- `entkapp` verwendet eine persistente Graph-Status-Schicht (`.entkapp-cache`). Es berechnet SHA-256 Hashes von Dateipuffern, um das AST-Parsing für unveränderte Dateien zu überspringen.
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
- Bei größeren Codebasen verteilt `entkapp` die Arbeitslast automatisch auf mehrere CPU-Threads mittels Node.js Worker Threads.
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
- ## 📄 Lizenz
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(targetCwd);
112
+ localConfig = await loader.loadConfig();
113
113
  } catch (e) {}
114
114
 
115
115
  // Merge options with local config
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://unpkg.com/entkapp@5.2.4/schema.json"
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
- This directory is for your custom plugins. entkapp will automatically load any `.js` or `.mjs` files placed here if `useCustomPlugins` is set to `true` in your `config.json`.
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
- ## Supported Ecosystems (Built-in)
5
+ ## 📦 Integrierte Framework- & Tool-Plugins (98 Gesamt)
6
6
 
7
- - **Next.js**: Handles pages, API routes, and App Router conventions.
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
- ## Knip Compatibility
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
- entkapp supports Knip-style plugins. You can drop Knip plugins into this folder, and the engine will attempt to wrap them for use within the entkapp ecosystem.
14
+ ### 🌐 State Management & Routing
15
+ - **Redux**, **Zustand**, **Jotai**, **Recoil**, **MobX**, **Pinia**, **TanStack Query**
16
+ - **React Router**, **TanStack Router**, **Vue Router**
16
17
 
17
- ## Documentation
18
+ ### 🛠️ Build Tools & Monorepo
19
+ - **Vite**, **Esbuild**, **Rollup**, **Webpack**, **Parcel**, **TypeScript**, **Babel**, **SWC**
20
+ - **Turbo**, **Nx**, **Lerna**, **Rush**, **Moon**, **Bazel**
18
21
 
19
- For a detailed guide on how to build your own plugins, please refer to the [Plugin Development Guide](../../docs/guide.md#plugin-development).
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.2.4",
5
- "description": "The Ultimate Enterprise Codebase Janitor. Faster than Knip with OXC integration, type-aware analysis, and automated structural healing. Fully standalone - solving what Knip cannot.",
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": ["nextjs", "nuxt", "remix", "sveltekit", "astro", "typescript", "vite", "webpack", "react", "vue", "angular"]
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 v4.6.0: Unified Architectural Refactoring Orchestrator
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) { this.cwd = cwd; }
3
- async loadConfig() { return {}; }
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(projectRoot, 'package.json');
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 githubWorkflows = path.join(projectRoot, '.github/workflows');
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
- // 4. Identify Unused Binaries
199
- // Scan node_modules/.bin for all available binaries
220
+ async _identifyUnusedBinaries(dir, usedBinaries) {
200
221
  try {
201
- const binDir = path.join(projectRoot, 'node_modules', '.bin');
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
- // Tsconfig paths can be loaded here later
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 (p.endsWith('.js')) {
23
- const tsPath = p.slice(0, -3) + '.ts';
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 (p.endsWith('.jsx')) {
29
- const tsxPath = p.slice(0, -4) + '.tsx';
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(p);
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(p, `index${ext}`);
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 p;
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, 'tsconfig.json');
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 result = ts.parseConfigFileTextWithComments(configPath, content);
22
+ const readResult = ts.readConfigFile(configPath, ts.sys.readFile);
21
23
 
22
- if (result.error) {
24
+ if (readResult.error) {
23
25
  return null;
24
26
  }
25
27
 
26
28
  const parsed = ts.parseJsonConfigFileContent(
27
- result.config,
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 || !parsedConfig.options.paths) {
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(/\*/, match[1]);
135
+ const resolvedReplacement = replacement.replace(/\*/g, match[1] || '');
61
136
  const fullPath = path.resolve(base, resolvedReplacement);
62
137
 
63
- // Prüfe ob Datei existiert
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) { this.context = context; }
3
- async checkWorkspaceHealth() { return []; }
4
- enforceBoundaries(filePath, imports) { return []; }
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
  }