entkapp 5.4.0 → 5.6.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.
Files changed (76) hide show
  1. package/README.md +7 -1
  2. package/bin/cli.js +117 -58
  3. package/bin/cli.mjs +175 -0
  4. package/index.cjs +18 -0
  5. package/index.mjs +51 -0
  6. package/package.json +7 -6
  7. package/src/EngineContext.js +0 -6
  8. package/src/EngineContext.mjs +428 -0
  9. package/src/Initializer.mjs +82 -0
  10. package/src/analyzers/CodeSmellAnalyzer.mjs +106 -0
  11. package/src/analyzers/OxcAnalyzer.mjs +11 -0
  12. package/src/api/HeadlessAPI.js +31 -16
  13. package/src/api/HeadlessAPI.mjs +369 -0
  14. package/src/api/PluginSDK.mjs +135 -0
  15. package/src/ast/ASTAnalyzer.js +23 -97
  16. package/src/ast/ASTAnalyzer.mjs +742 -0
  17. package/src/ast/AdvancedAnalysis.mjs +586 -0
  18. package/src/ast/BarrelParser.mjs +230 -0
  19. package/src/ast/DeadCodeDetector.js +7 -5
  20. package/src/ast/DeadCodeDetector.mjs +92 -0
  21. package/src/ast/MagicDetector.js +43 -23
  22. package/src/ast/MagicDetector.mjs +203 -0
  23. package/src/ast/OxcAnalyzer.js +27 -190
  24. package/src/ast/OxcAnalyzer.mjs +188 -0
  25. package/src/ast/SecretScanner.mjs +374 -0
  26. package/src/healing/GitSandbox.mjs +82 -0
  27. package/src/healing/SelfHealer.mjs +48 -0
  28. package/src/index.js +41 -88
  29. package/src/index.mjs +1176 -0
  30. package/src/performance/GraphCache.js +0 -27
  31. package/src/performance/GraphCache.mjs +108 -0
  32. package/src/performance/SupplyChainGuard.mjs +92 -0
  33. package/src/performance/WorkerPool.js +4 -22
  34. package/src/performance/WorkerPool.mjs +132 -0
  35. package/src/performance/WorkerTaskRunner.js +0 -26
  36. package/src/performance/WorkerTaskRunner.mjs +144 -0
  37. package/src/plugins/BasePlugin.js +27 -16
  38. package/src/plugins/BasePlugin.mjs +240 -0
  39. package/src/plugins/PluginRegistry.js +0 -44
  40. package/src/plugins/PluginRegistry.mjs +203 -0
  41. package/src/plugins/ecosystems/BackendServices.mjs +197 -0
  42. package/src/plugins/ecosystems/GenericPlugins.mjs +142 -0
  43. package/src/plugins/ecosystems/ModernFrameworks.mjs +162 -0
  44. package/src/plugins/ecosystems/MorePlugins.mjs +562 -0
  45. package/src/plugins/ecosystems/NewPlugins.mjs +526 -0
  46. package/src/plugins/ecosystems/NextJsPlugin.mjs +45 -0
  47. package/src/plugins/ecosystems/PluginLoader.mjs +193 -0
  48. package/src/plugins/ecosystems/TypeScriptPlugin.mjs +56 -0
  49. package/src/plugins/ecosystems/UltimateBundle.js +0 -259
  50. package/src/plugins/ecosystems/UltimateBundle.mjs +1182 -0
  51. package/src/refractor/ImpactAnalyzer.mjs +92 -0
  52. package/src/refractor/SourceRewriter.mjs +86 -0
  53. package/src/refractor/TransactionManager.mjs +5 -0
  54. package/src/refractor/TypeIntegrity.mjs +4 -0
  55. package/src/resolution/BuildOrchestrator.mjs +46 -0
  56. package/src/resolution/CircularDetector.mjs +91 -0
  57. package/src/resolution/ConfigGenerator.mjs +83 -0
  58. package/src/resolution/ConfigLoader.js +26 -190
  59. package/src/resolution/ConfigLoader.mjs +94 -0
  60. package/src/resolution/DepencyResolver.mjs +66 -0
  61. package/src/resolution/DependencyFixer.mjs +88 -0
  62. package/src/resolution/DependencyProfiler.mjs +286 -0
  63. package/src/resolution/EntryPointDetector.mjs +134 -0
  64. package/src/resolution/FrameworkConfigParser.mjs +119 -0
  65. package/src/resolution/GraphAnalyzer.js +1 -65
  66. package/src/resolution/GraphAnalyzer.mjs +80 -0
  67. package/src/resolution/MigrationAnalyzer.mjs +60 -0
  68. package/src/resolution/PathMapper.js +0 -81
  69. package/src/resolution/PathMapper.mjs +82 -0
  70. package/src/resolution/TSConfigLoader.js +1 -3
  71. package/src/resolution/TSConfigLoader.mjs +162 -0
  72. package/src/resolution/WorkSpaceGraph.js +89 -260
  73. package/src/resolution/WorkSpaceGraph.mjs +183 -0
  74. package/src/resolution/WorkspaceDiagnostic.mjs +139 -0
  75. package/test.js +76 -0
  76. package/wrangler.toml +6 -0
@@ -0,0 +1,92 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+
4
+ /**
5
+ * Cross-Reference Dependency Matrix & Breakage Risk Auditor
6
+ * Traces dynamic runtime usage patterns to prevent code pruning from breaking downstream systems.
7
+ */
8
+ export class ImpactAnalyzer {
9
+ constructor(context) {
10
+ this.context = context;
11
+ this.safetyOverlays = [/\.json$/, /\.json5$/, /\.html$/, /\.yaml$/, /\.yml$/];
12
+ }
13
+
14
+ /**
15
+ * Scans non-code assets and dynamic lookups to check if an unused export is needed elsewhere.
16
+ * @param {string} originFile - Absolute path of component housing target symbol
17
+ * @param {string} symbolName - Literal identifier being evaluated for deletion
18
+ * @param {Map} projectGraph - Full project structural graph representation
19
+ */
20
+ async verifyRefactorSafety(originFile, symbolName, projectGraph) {
21
+ // Avoid dropping generic single letter tokens or framework primitives
22
+ if (symbolName === 'default' || symbolName.length <= 2) {
23
+ return { isSafeToPrune: false, blockReason: 'PROTECTED_SYSTEM_CONTRACT_KEYWORD' };
24
+ }
25
+
26
+ // Rule 1: Check across all code files for loose string-based token references
27
+ for (const [filePath, fileNode] of projectGraph.entries()) {
28
+ if (filePath === originFile) continue;
29
+
30
+ // If the symbol name is explicitly referenced in an element lookup or template slice, flag it as risky
31
+ if (fileNode.rawStringReferences && fileNode.rawStringReferences.has(symbolName)) {
32
+ return {
33
+ isSafeToPrune: false,
34
+ blockReason: `LOOSE_STRING_ACCESS_MATCH_FOUND_IN: ${path.relative(this.context.cwd, filePath)}`
35
+ };
36
+ }
37
+
38
+ // Check member property chain lookups: customer.profile.billingAddress
39
+ if (fileNode.propertyAccessChains) {
40
+ for (const chain of fileNode.propertyAccessChains) {
41
+ if (chain.endsWith(`.${symbolName}`) || chain.includes(`.${symbolName}.`)) {
42
+ return {
43
+ isSafeToPrune: false,
44
+ blockReason: `DYNAMIC_PROPERTY_ACCESS_CHAIN_HIT: ${chain}`
45
+ };
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ // Rule 2: Crawl through external static manifests (JSON metadata, HTML routing templates, workflow files)
52
+ const configurations = await this.gatherMetadataFiles(this.context.cwd);
53
+
54
+ for (const confPath of configurations) {
55
+ try {
56
+ const payload = await fs.readFile(confPath, 'utf8');
57
+
58
+ // Match string references inside configuration boundaries
59
+ if (payload.includes(`"${symbolName}"`) || payload.includes(`'${symbolName}'`) || payload.includes(`data-${symbolName}`)) {
60
+ return {
61
+ isSafeToPrune: false,
62
+ blockReason: `METADATA_MANIFEST_DEPENDENCY_FOUND_IN: ${path.relative(this.context.cwd, confPath)}`
63
+ };
64
+ }
65
+ } catch {
66
+ // Read step error; skip unreadable descriptors
67
+ }
68
+ }
69
+
70
+ return { isSafeToPrune: true, blockReason: null };
71
+ }
72
+
73
+ async gatherMetadataFiles(dir, collected = []) {
74
+ try {
75
+ const entities = await fs.readdir(dir, { withFileTypes: true });
76
+ for (const ent of entities) {
77
+ const resolutionPath = path.join(dir, ent.name);
78
+
79
+ if (ent.isDirectory()) {
80
+ if (ent.name === 'node_modules' || ent.name === '.git' || ent.name === '.entkapp-cache' || ent.name === 'dist') continue;
81
+ await this.gatherMetadataFiles(resolutionPath, collected);
82
+ } else if (ent.isFile()) {
83
+ const actsAsMetaAsset = this.safetyOverlays.some(regex => regex.test(ent.name));
84
+ if (actsAsMetaAsset) {
85
+ collected.push(resolutionPath);
86
+ }
87
+ }
88
+ }
89
+ } catch {}
90
+ return collected;
91
+ }
92
+ }
@@ -0,0 +1,86 @@
1
+ import ts from 'typescript';
2
+ import fs from 'fs/promises';
3
+
4
+ /**
5
+ * Format-Preserving Abstract Syntax Tree Source Mutation Engine
6
+ * Executes targeted updates using token position logic while preserving trivia (comments/JSDoc).
7
+ */
8
+ export class SourceRewriter {
9
+ constructor(context) {
10
+ this.context = context;
11
+ }
12
+
13
+ /**
14
+ * Removes a specific named export symbol declaration block from code text.
15
+ * @param {string} filePath - Absolute path to on-disk code component
16
+ * @param {string} symbolName - Target export key to prune
17
+ * @param {Object} exportMeta - Mapped token offset indices (start/end offsets)
18
+ */
19
+ async stripNamedExportSignature(filePath, symbolName, exportMeta) {
20
+ const rawSource = await fs.readFile(filePath, 'utf8');
21
+
22
+ // Case A: Token is grouped inside a multi-export destructured statement block: export { a, b, c };
23
+ if (exportMeta.type === 'named') {
24
+ const updatedText = this.pruneFromSharedExportClause(rawSource, symbolName, exportMeta);
25
+ if (updatedText) return updatedText;
26
+ }
27
+
28
+ // Case B: Isolated node removal. Calculate indices from start to end while preserving comments.
29
+ const startOffset = exportMeta.start;
30
+ const endOffset = exportMeta.end;
31
+
32
+ // Split source without dropping trailing line structural punctuation or text formatting configurations
33
+ const prefix = rawSource.slice(0, startOffset);
34
+ let suffix = rawSource.slice(endOffset);
35
+
36
+ // Clean up trailing commas or semicolons to prevent syntax errors
37
+ if (suffix.startsWith(';') || suffix.startsWith(',')) {
38
+ suffix = suffix.slice(1);
39
+ }
40
+
41
+ return prefix + suffix;
42
+ }
43
+
44
+ /**
45
+ * Handles selective pruning of export elements within inline groupings like `export { x, y as z }`.
46
+ */
47
+ pruneFromSharedExportClause(sourceText, symbolName, meta) {
48
+ // Find the enclosing export declaration node using structural boundaries
49
+ const sourceFile = ts.createSourceFile(
50
+ 'temp.ts',
51
+ sourceText,
52
+ ts.ScriptTarget.Latest,
53
+ true
54
+ );
55
+
56
+ let targetText = null;
57
+
58
+ const findAndPrune = (node) => {
59
+ if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
60
+ const start = node.getStart(sourceFile);
61
+ const end = node.getEnd();
62
+
63
+ // Check if the target node spans the exact offset coordinates of the dead symbol
64
+ if (meta.start >= start && meta.end <= end) {
65
+ const activeElements = node.exportClause.elements;
66
+ const keptSymbols = activeElements
67
+ .filter(el => el.name.text !== symbolName)
68
+ .map(el => el.getText(sourceFile));
69
+
70
+ if (keptSymbols.length === 0) {
71
+ // Drop the entire declaration block if no active exports remain inside it
72
+ targetText = sourceText.slice(0, start) + sourceText.slice(end);
73
+ } else {
74
+ // Rewrite the export group inline, preserving formatting and comments
75
+ const reconstructedClause = `export { ${keptSymbols.join(', ')} };`;
76
+ targetText = sourceText.slice(0, start) + reconstructedClause + sourceText.slice(end);
77
+ }
78
+ }
79
+ }
80
+ ts.forEachChild(node, findAndPrune);
81
+ };
82
+
83
+ findAndPrune(sourceFile);
84
+ return targetText;
85
+ }
86
+ }
@@ -0,0 +1,5 @@
1
+ export class TransactionManager {
2
+ constructor(context) { this.context = context; }
3
+ async startTransaction() {}
4
+ async commitTransaction() {}
5
+ }
@@ -0,0 +1,4 @@
1
+ export class TypeIntegrity {
2
+ constructor(context) { this.context = context; }
3
+ async check() {}
4
+ }
@@ -0,0 +1,46 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * ============================================================================
6
+ * Auto-Build Orchestrator v5.2.0
7
+ * ============================================================================
8
+ * Orchestrates the build process based on active frameworks and tools.
9
+ */
10
+ export class BuildOrchestrator {
11
+ constructor(context) {
12
+ this.context = context;
13
+ this.cwd = context.cwd;
14
+ }
15
+
16
+ async getBuildCommand(activePlugins) {
17
+ const pluginNames = activePlugins.map(p => p.name);
18
+
19
+ // Priority-based build command detection
20
+ if (pluginNames.includes('nextjs')) return 'next build';
21
+ if (pluginNames.includes('nuxt')) return 'nuxt build';
22
+ if (pluginNames.includes('remix')) return 'remix build';
23
+ if (pluginNames.includes('astro')) return 'astro build';
24
+ if (pluginNames.includes('vite')) return 'vite build';
25
+ if (pluginNames.includes('webpack')) return 'webpack --mode production';
26
+ if (pluginNames.includes('rollup')) return 'rollup -c';
27
+ if (pluginNames.includes('esbuild')) return 'esbuild src/index.ts --bundle --outdir=dist';
28
+
29
+ return null;
30
+ }
31
+
32
+ async runBuild(activePlugins) {
33
+ const command = await this.getBuildCommand(activePlugins);
34
+ if (!command) {
35
+ return { success: false, message: 'No suitable build command found for this project setup.' };
36
+ }
37
+
38
+ console.log(`[BuildOrchestrator] Detected Build Command: ${command}`);
39
+ // In a real scenario, we would execute this via execa.
40
+ return {
41
+ success: true,
42
+ command: command,
43
+ message: `Project would be built using: ${command}`
44
+ };
45
+ }
46
+ }
@@ -0,0 +1,91 @@
1
+ import path from 'path';
2
+
3
+ export class CircularDetector {
4
+ constructor(context) {
5
+ this.context = context;
6
+ this.cycles = [];
7
+ }
8
+
9
+ detectCycles(projectGraph) {
10
+ const stack = [];
11
+ const onStack = new Set();
12
+ const indices = new Map();
13
+ const lowlink = new Map();
14
+ const sccs = [];
15
+ let index = 0;
16
+
17
+ const strongConnect = (v) => {
18
+ indices.set(v, index);
19
+ lowlink.set(v, index);
20
+ index++;
21
+ stack.push(v);
22
+ onStack.add(v);
23
+
24
+ const node = projectGraph.get(v);
25
+ if (node && node.outgoingEdges) {
26
+ // Ensure outgoingEdges is treated as an array-like structure
27
+ const edges = Array.isArray(node.outgoingEdges) ? node.outgoingEdges : Array.from(node.outgoingEdges);
28
+
29
+ for (let i = 0; i < edges.length; i++) {
30
+ const w = edges[i];
31
+ if (!indices.has(w)) {
32
+ strongConnect(w);
33
+ lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
34
+ } else if (onStack.has(w)) {
35
+ lowlink.set(v, Math.min(lowlink.get(v), indices.get(w)));
36
+ }
37
+ }
38
+ }
39
+
40
+ if (lowlink.get(v) === indices.get(v)) {
41
+ const scc = [];
42
+ let w;
43
+ do {
44
+ w = stack.pop();
45
+ onStack.delete(w);
46
+ scc.push(w);
47
+ } while (w !== v);
48
+
49
+ if (scc.length > 1) {
50
+ const cycle = scc.slice().reverse();
51
+ cycle.push(cycle[0]);
52
+ sccs.push(cycle);
53
+ } else if (scc.length === 1) {
54
+ const singleNode = projectGraph.get(scc[0]);
55
+ if (singleNode && singleNode.outgoingEdges) {
56
+ const edges = Array.isArray(singleNode.outgoingEdges) ? singleNode.outgoingEdges : Array.from(singleNode.outgoingEdges);
57
+ let hasSelfLoop = false;
58
+ for (let i = 0; i < edges.length; i++) {
59
+ if (edges[i] === scc[0]) {
60
+ hasSelfLoop = true;
61
+ break;
62
+ }
63
+ }
64
+ if (hasSelfLoop) {
65
+ sccs.push([scc[0], scc[0]]);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ };
71
+
72
+ const keys = Array.from(projectGraph.keys());
73
+ for (let i = 0; i < keys.length; i++) {
74
+ if (!indices.has(keys[i])) {
75
+ strongConnect(keys[i]);
76
+ }
77
+ }
78
+
79
+ this.cycles = sccs;
80
+ return sccs;
81
+ }
82
+
83
+ formatCycles() {
84
+ return this.cycles.map(cycle => {
85
+ return cycle.map(p => {
86
+ const relativePath = path.relative(this.context.cwd, p);
87
+ return relativePath.replace(/\\/g, '/');
88
+ }).join(' -> ');
89
+ });
90
+ }
91
+ }
@@ -0,0 +1,83 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * ============================================================================
6
+ * Auto-Config Engine v5.2.0
7
+ * ============================================================================
8
+ * Generates and optimizes configuration files based on project structure.
9
+ */
10
+ export class ConfigGenerator {
11
+ constructor(context) {
12
+ this.context = context;
13
+ this.cwd = context.cwd;
14
+ }
15
+
16
+ /**
17
+ * Generates missing configurations for active plugins.
18
+ */
19
+ async generateMissingConfigs(activePlugins) {
20
+ const generated = [];
21
+ const files = await fs.readdir(this.cwd);
22
+
23
+ for (const plugin of activePlugins) {
24
+ const configFiles = plugin.getConfigFiles();
25
+ const exists = configFiles.some(f => files.includes(f));
26
+
27
+ if (!exists && this.templates[plugin.name]) {
28
+ const template = this.templates[plugin.name]();
29
+ const fileName = configFiles[0]; // Use the first recommended filename
30
+ const filePath = path.join(this.cwd, fileName);
31
+
32
+ // In a real scenario, we would write the file here.
33
+ // For the SDK, we return the plan.
34
+ generated.push({
35
+ plugin: plugin.name,
36
+ file: fileName,
37
+ content: template
38
+ });
39
+ }
40
+ }
41
+ return generated;
42
+ }
43
+
44
+ get templates() {
45
+ return {
46
+ 'tailwind': () => `/** @type {import('tailwindcss').Config} */
47
+ module.exports = {
48
+ content: [
49
+ "./index.html",
50
+ "./src/**/*.{js,ts,jsx,tsx}",
51
+ ],
52
+ theme: {
53
+ extend: {},
54
+ },
55
+ plugins: [],
56
+ }`,
57
+ 'prettier': () => `{
58
+ "semi": true,
59
+ "singleQuote": true,
60
+ "tabWidth": 2,
61
+ "trailingComma": "es5"
62
+ }`,
63
+ 'eslint': () => `{
64
+ "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
65
+ "parser": "@typescript-eslint/parser",
66
+ "plugins": ["@typescript-eslint"],
67
+ "root": true
68
+ }`,
69
+ 'typescript': () => `{
70
+ "compilerOptions": {
71
+ "target": "ESNext",
72
+ "module": "ESNext",
73
+ "moduleResolution": "node",
74
+ "strict": true,
75
+ "esModuleInterop": true,
76
+ "skipLibCheck": true,
77
+ "forceConsistentCasingInFileNames": true
78
+ },
79
+ "include": ["src/**/*"]
80
+ }`
81
+ };
82
+ }
83
+ }
@@ -3,239 +3,73 @@ import path from 'path';
3
3
 
4
4
  /**
5
5
  * ConfigLoader
6
- *
7
- * Loads and merges entkapp configuration from multiple sources.
8
- * Supports Monorepo detection for pnpm, npm/yarn/bun, Lerna, Turbo, and Nx.
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)
9
11
  */
10
12
  export class ConfigLoader {
11
- constructor(cwd) {
12
- this.cwd = cwd;
13
+ constructor(cwd) {
14
+ this.cwd = cwd;
13
15
  }
14
16
 
15
17
  async loadConfig(overrides = {}) {
16
18
  let config = this._defaultConfig();
17
19
 
18
- // 1. entkapp/config.json
20
+ // 1. Try entkapp/config.json
19
21
  const jsonConfigPath = path.join(this.cwd, 'entkapp', 'config.json');
20
22
  try {
21
23
  const raw = await fs.readFile(jsonConfigPath, 'utf8');
24
+ // Strip comments from JSON (JSONC support)
22
25
  const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
23
- config = this._merge(config, JSON.parse(stripped));
26
+ const parsed = JSON.parse(stripped);
27
+ config = this._merge(config, parsed);
24
28
  } catch (e) {}
25
29
 
26
- // 2. entkapp.config.ts / .mjs / .js / .cjs
27
- for (const configFile of ['entkapp.config.ts', 'entkapp.config.mjs', 'entkapp.config.js', 'entkapp.config.cjs']) {
30
+ // 2. Try entkapp.config.js / entkapp.config.mjs
31
+ for (const configFile of ['entkapp.config.mjs', 'entkapp.config.js', 'entkapp.config.cjs']) {
28
32
  const jsConfigPath = path.join(this.cwd, configFile);
29
33
  try {
30
34
  const mod = await import(jsConfigPath);
31
35
  const jsConfig = mod.default || mod;
32
- if (typeof jsConfig === 'object' && jsConfig !== null) {
36
+ if (typeof jsConfig === 'object') {
33
37
  config = this._merge(config, jsConfig);
34
38
  break;
35
39
  }
36
40
  } catch (e) {}
37
41
  }
38
42
 
39
- // 3. package.json "entkapp" key
43
+ // 3. Try package.json "entkapp" key
44
+ const pkgPath = path.join(this.cwd, 'package.json');
40
45
  try {
41
- const pkg = JSON.parse(await fs.readFile(path.join(this.cwd, 'package.json'), 'utf8'));
46
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
42
47
  if (pkg.entkapp && typeof pkg.entkapp === 'object') {
43
48
  config = this._merge(config, pkg.entkapp);
44
49
  }
45
50
  } catch (e) {}
46
51
 
47
- // 4. Workspace / monorepo packages
48
- const workspaceData = await this.loadWorkspaceConfigs();
49
- if (workspaceData.packages.length > 0) {
50
- config.workspace = true;
51
- config.workspacePackages = workspaceData.packages;
52
- config.workspaceTsConfigs = workspaceData.tsConfigs;
53
- config.workspaceConfigFiles = workspaceData.configFiles;
54
- config.monorepoConfigs = workspaceData.monorepoConfigs;
55
- }
56
-
57
- // 5. CLI overrides
52
+ // 4. Apply CLI overrides
58
53
  config = this._merge(config, overrides);
59
54
 
60
- // Final sanity check: ensure arrays exist
61
- if (!Array.isArray(config.entryPoints)) config.entryPoints = [];
62
- if (!Array.isArray(config.workspacePackages)) config.workspacePackages = [];
63
- if (!Array.isArray(config.workspaceTsConfigs)) config.workspaceTsConfigs = [];
64
- if (!Array.isArray(config.workspaceConfigFiles)) config.workspaceConfigFiles = [];
65
-
66
55
  return config;
67
56
  }
68
57
 
69
- async loadWorkspaceConfigs() {
70
- const result = {
71
- packages: [],
72
- tsConfigs: [],
73
- configFiles: [],
74
- monorepoConfigs: {}
75
- };
76
-
77
- // Load global configs first
78
- const globalConfigs = ['turbo.json', 'nx.json', 'lerna.json'];
79
- for (const file of globalConfigs) {
80
- try {
81
- const content = JSON.parse(await fs.readFile(path.join(this.cwd, file), 'utf8'));
82
- result.monorepoConfigs[file] = content;
83
- } catch (e) {}
84
- }
85
-
86
- const patterns = await this._resolveWorkspacePatterns();
87
- if (patterns.length === 0) return result;
88
-
89
- for (const pattern of patterns) {
90
- const dirs = await this._expandGlob(pattern);
91
- for (const dir of dirs) {
92
- await this._readPackageDir(dir, result);
93
- }
94
- }
95
-
96
- return result;
97
- }
98
-
99
- async _resolveWorkspacePatterns() {
100
- const pnpmPatterns = await this._parseWorkspaceYaml('pnpm-workspace.yaml');
101
- if (pnpmPatterns.length > 0) return pnpmPatterns;
102
-
103
- try {
104
- const pkg = JSON.parse(await fs.readFile(path.join(this.cwd, 'package.json'), 'utf8'));
105
- if (pkg.workspaces) {
106
- return Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages || [];
107
- }
108
- } catch (e) {}
109
-
110
- try {
111
- const lerna = JSON.parse(await fs.readFile(path.join(this.cwd, 'lerna.json'), 'utf8'));
112
- if (lerna.packages) return lerna.packages;
113
- } catch (e) {}
114
-
115
- return this._parseWorkspaceYaml('workspace.yaml');
116
- }
117
-
118
- async _parseWorkspaceYaml(fileName) {
119
- const filePath = path.join(this.cwd, fileName);
120
- try {
121
- const raw = await fs.readFile(filePath, 'utf8');
122
- const patterns = [];
123
- const lines = raw.split('\n');
124
- let inPackages = false;
125
- for (const line of lines) {
126
- const trimmed = line.trim();
127
- if (!trimmed || trimmed.startsWith('#')) continue;
128
- if (/^packages\s*:/.test(trimmed)) {
129
- inPackages = true;
130
- const inlineMatch = trimmed.match(/^packages\s*:\s*\[([^\]]*)\]/);
131
- if (inlineMatch) {
132
- inPackages = false;
133
- for (const item of inlineMatch[1].split(',')) {
134
- const val = item.trim().replace(/^['"]|['"]$/g, '');
135
- if (val) patterns.push(val);
136
- }
137
- }
138
- continue;
139
- }
140
- if (inPackages) {
141
- if (line.match(/^[a-zA-Z0-9_-]/) && trimmed.endsWith(':')) {
142
- inPackages = false;
143
- continue;
144
- }
145
- if (trimmed.startsWith('-')) {
146
- const val = trimmed.substring(1).trim().replace(/^['"]|['"]$/g, '');
147
- if (val) patterns.push(val);
148
- }
149
- }
150
- }
151
- return patterns;
152
- } catch (e) {
153
- return [];
154
- }
155
- }
156
-
157
- async _readPackageDir(dir, result) {
158
- const normalizedDir = dir.replace(/\\/g, '/');
159
- let packageName;
160
-
161
- try {
162
- const pkg = JSON.parse(await fs.readFile(path.join(dir, 'package.json'), 'utf8'));
163
- packageName = pkg.name;
164
- result.packages.push({
165
- dir: normalizedDir,
166
- name: packageName,
167
- entkappConfig: (pkg.entkapp && typeof pkg.entkapp === 'object') ? pkg.entkapp : {}
168
- });
169
- } catch (e) {
170
- // If no package.json, skip as requested in WorkSpaceGraph logic
171
- return;
172
- }
173
-
174
- // tsconfig.json
175
- try {
176
- const tsconfigRaw = await fs.readFile(path.join(dir, 'tsconfig.json'), 'utf8');
177
- const stripped = tsconfigRaw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
178
- result.tsConfigs.push({ dir: normalizedDir, name: packageName, tsconfig: JSON.parse(stripped) });
179
- } catch (e) {}
180
-
181
- // *.config.ts / .js
182
- try {
183
- const entries = await fs.readdir(dir, { withFileTypes: true });
184
- for (const entry of entries) {
185
- if (!entry.isFile() || !/\.config\.(ts|js|mjs|cjs)$/.test(entry.name)) continue;
186
- const filePath = path.join(dir, entry.name).replace(/\\/g, '/');
187
- try {
188
- const content = await fs.readFile(filePath, 'utf8');
189
- result.configFiles.push({ dir: normalizedDir, name: packageName, fileName: entry.name, filePath, content });
190
- } catch (e) {}
191
- }
192
- } catch (e) {}
193
- }
194
-
195
- async _expandGlob(pattern) {
196
- const results = [];
197
- const cleanPattern = pattern.replace(/\/$/, '');
198
- if (cleanPattern.includes('*')) {
199
- const parts = cleanPattern.split('/');
200
- const wildcardIndex = parts.findIndex(p => p.includes('*'));
201
- const baseDir = path.join(this.cwd, ...parts.slice(0, wildcardIndex));
202
- try {
203
- const entries = await fs.readdir(baseDir, { withFileTypes: true });
204
- for (const entry of entries) {
205
- if (entry.isDirectory() && !entry.name.startsWith('.')) {
206
- const fullPath = path.join(baseDir, entry.name);
207
- try {
208
- await fs.access(path.join(fullPath, 'package.json'));
209
- results.push(fullPath.replace(/\\/g, '/'));
210
- } catch (e) {}
211
- }
212
- }
213
- } catch (e) {}
214
- } else {
215
- const fullPath = path.resolve(this.cwd, cleanPattern);
216
- try {
217
- const stat = await fs.stat(fullPath);
218
- if (stat.isDirectory()) results.push(fullPath.replace(/\\/g, '/'));
219
- } catch (e) {}
220
- }
221
- return results;
222
- }
223
-
224
58
  _defaultConfig() {
225
59
  return {
226
60
  interface: 'CLI',
227
61
  useBuiltinPlugins: true,
228
62
  useCustomPlugins: true,
229
- options: { verbose: false, fastMode: true, selfHealing: true },
63
+ options: {
64
+ verbose: false,
65
+ fastMode: true,
66
+ selfHealing: true
67
+ },
230
68
  enabledPlugins: [],
231
69
  ignoreDependencies: ['entkapp', '@types/*'],
232
70
  exclude: ['node_modules', '.git', 'dist', 'build', 'coverage'],
233
71
  entryPoints: [],
234
- workspace: false,
235
- workspacePackages: [],
236
- workspaceTsConfigs: [],
237
- workspaceConfigFiles: [],
238
- monorepoConfigs: {}
72
+ workspace: false
239
73
  };
240
74
  }
241
75
 
@@ -245,6 +79,8 @@ export class ConfigLoader {
245
79
  for (const key of Object.keys(override)) {
246
80
  if (key === 'options' && typeof override[key] === 'object') {
247
81
  result.options = { ...(base.options || {}), ...override[key] };
82
+ } else if (Array.isArray(override[key])) {
83
+ result[key] = override[key];
248
84
  } else {
249
85
  result[key] = override[key];
250
86
  }