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.
- package/README.md +7 -1
- package/bin/cli.js +117 -58
- package/bin/cli.mjs +175 -0
- package/index.cjs +18 -0
- package/index.mjs +51 -0
- package/package.json +7 -6
- package/src/EngineContext.js +0 -6
- package/src/EngineContext.mjs +428 -0
- package/src/Initializer.mjs +82 -0
- package/src/analyzers/CodeSmellAnalyzer.mjs +106 -0
- package/src/analyzers/OxcAnalyzer.mjs +11 -0
- package/src/api/HeadlessAPI.js +31 -16
- package/src/api/HeadlessAPI.mjs +369 -0
- package/src/api/PluginSDK.mjs +135 -0
- package/src/ast/ASTAnalyzer.js +23 -97
- package/src/ast/ASTAnalyzer.mjs +742 -0
- package/src/ast/AdvancedAnalysis.mjs +586 -0
- package/src/ast/BarrelParser.mjs +230 -0
- package/src/ast/DeadCodeDetector.js +7 -5
- package/src/ast/DeadCodeDetector.mjs +92 -0
- package/src/ast/MagicDetector.js +43 -23
- package/src/ast/MagicDetector.mjs +203 -0
- package/src/ast/OxcAnalyzer.js +27 -190
- package/src/ast/OxcAnalyzer.mjs +188 -0
- package/src/ast/SecretScanner.mjs +374 -0
- package/src/healing/GitSandbox.mjs +82 -0
- package/src/healing/SelfHealer.mjs +48 -0
- package/src/index.js +41 -88
- package/src/index.mjs +1176 -0
- package/src/performance/GraphCache.js +0 -27
- package/src/performance/GraphCache.mjs +108 -0
- package/src/performance/SupplyChainGuard.mjs +92 -0
- package/src/performance/WorkerPool.js +4 -22
- package/src/performance/WorkerPool.mjs +132 -0
- package/src/performance/WorkerTaskRunner.js +0 -26
- package/src/performance/WorkerTaskRunner.mjs +144 -0
- package/src/plugins/BasePlugin.js +27 -16
- package/src/plugins/BasePlugin.mjs +240 -0
- package/src/plugins/PluginRegistry.js +0 -44
- package/src/plugins/PluginRegistry.mjs +203 -0
- package/src/plugins/ecosystems/BackendServices.mjs +197 -0
- package/src/plugins/ecosystems/GenericPlugins.mjs +142 -0
- package/src/plugins/ecosystems/ModernFrameworks.mjs +162 -0
- package/src/plugins/ecosystems/MorePlugins.mjs +562 -0
- package/src/plugins/ecosystems/NewPlugins.mjs +526 -0
- package/src/plugins/ecosystems/NextJsPlugin.mjs +45 -0
- package/src/plugins/ecosystems/PluginLoader.mjs +193 -0
- package/src/plugins/ecosystems/TypeScriptPlugin.mjs +56 -0
- package/src/plugins/ecosystems/UltimateBundle.js +0 -259
- package/src/plugins/ecosystems/UltimateBundle.mjs +1182 -0
- package/src/refractor/ImpactAnalyzer.mjs +92 -0
- package/src/refractor/SourceRewriter.mjs +86 -0
- package/src/refractor/TransactionManager.mjs +5 -0
- package/src/refractor/TypeIntegrity.mjs +4 -0
- package/src/resolution/BuildOrchestrator.mjs +46 -0
- package/src/resolution/CircularDetector.mjs +91 -0
- package/src/resolution/ConfigGenerator.mjs +83 -0
- package/src/resolution/ConfigLoader.js +26 -190
- package/src/resolution/ConfigLoader.mjs +94 -0
- package/src/resolution/DepencyResolver.mjs +66 -0
- package/src/resolution/DependencyFixer.mjs +88 -0
- package/src/resolution/DependencyProfiler.mjs +286 -0
- package/src/resolution/EntryPointDetector.mjs +134 -0
- package/src/resolution/FrameworkConfigParser.mjs +119 -0
- package/src/resolution/GraphAnalyzer.js +1 -65
- package/src/resolution/GraphAnalyzer.mjs +80 -0
- package/src/resolution/MigrationAnalyzer.mjs +60 -0
- package/src/resolution/PathMapper.js +0 -81
- package/src/resolution/PathMapper.mjs +82 -0
- package/src/resolution/TSConfigLoader.js +1 -3
- package/src/resolution/TSConfigLoader.mjs +162 -0
- package/src/resolution/WorkSpaceGraph.js +89 -260
- package/src/resolution/WorkSpaceGraph.mjs +183 -0
- package/src/resolution/WorkspaceDiagnostic.mjs +139 -0
- package/test.js +76 -0
- package/wrangler.toml +6 -0
|
@@ -0,0 +1,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,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
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
26
|
+
const parsed = JSON.parse(stripped);
|
|
27
|
+
config = this._merge(config, parsed);
|
|
24
28
|
} catch (e) {}
|
|
25
29
|
|
|
26
|
-
// 2. entkapp.config.
|
|
27
|
-
for (const configFile of ['entkapp.config.
|
|
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'
|
|
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(
|
|
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.
|
|
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: {
|
|
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
|
}
|