entkapp 4.1.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/.scaffold-ignore +22 -0
- package/LICENSE +211 -0
- package/NOTICE +13 -0
- package/README.md +33 -0
- package/bin/cli.js +174 -0
- package/entkapp/config.json +42 -0
- package/entkapp/plugins/README.md +19 -0
- package/index.js +2254 -0
- package/logo.png +0 -0
- package/package.json +96 -0
- package/src/EngineContext.js +185 -0
- package/src/api/HeadlessAPI.js +376 -0
- package/src/api/PluginSDK.js +299 -0
- package/src/ast/ASTAnalyzer.js +492 -0
- package/src/ast/BarrelParser.js +221 -0
- package/src/ast/DeadCodeDetector.js +73 -0
- package/src/ast/MagicDetector.js +203 -0
- package/src/ast/OxcAnalyzer.js +337 -0
- package/src/ast/SecretScanner.js +304 -0
- package/src/healing/GitSandbox.js +82 -0
- package/src/healing/SelfHealer.js +52 -0
- package/src/index.js +723 -0
- package/src/performance/GraphCache.js +87 -0
- package/src/performance/SupplyChainGuard.js +92 -0
- package/src/performance/WorkerPool.js +109 -0
- package/src/plugins/BasePlugin.js +71 -0
- package/src/plugins/KnipAdapter.js +106 -0
- package/src/plugins/PluginRegistry.js +197 -0
- package/src/plugins/ecosystems/BackendServices.js +61 -0
- package/src/plugins/ecosystems/GenericPlugins.js +64 -0
- package/src/plugins/ecosystems/ModernFrameworks.js +159 -0
- package/src/plugins/ecosystems/MorePlugins.js +184 -0
- package/src/plugins/ecosystems/NextJsPlugin.js +33 -0
- package/src/plugins/ecosystems/PluginLoader.js +20 -0
- package/src/plugins/ecosystems/TypeScriptPlugin.js +56 -0
- package/src/refractor/ImpactAnalyzer.js +92 -0
- package/src/refractor/SourceRewriter.js +86 -0
- package/src/refractor/TransactionManager.js +138 -0
- package/src/refractor/TypeIntegrity.js +75 -0
- package/src/resolution/CircularDetector.js +91 -0
- package/src/resolution/ConfigLoader.js +87 -0
- package/src/resolution/DepencyResolver.js +133 -0
- package/src/resolution/DependencyProfiler.js +268 -0
- package/src/resolution/PathMapper.js +125 -0
- package/src/resolution/WorkSpaceGraph.js +474 -0
- package/tsconfig.json +26 -0
|
@@ -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,138 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Transactional File System Operations Supervisor
|
|
7
|
+
* Guarantees atomicity across multi-file transformations by tracking rolling backups.
|
|
8
|
+
*/
|
|
9
|
+
export class TransactionManager {
|
|
10
|
+
constructor(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
// FIX: Ensure context.cwd is always defined, fallback to process.cwd()
|
|
13
|
+
const cwd = context?.cwd || process.cwd();
|
|
14
|
+
this.backupDirectory = path.join(cwd, 'backups');
|
|
15
|
+
this.journal = [];
|
|
16
|
+
this.isLocked = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Begins a new transaction lifecycle loop, initializing isolation vectors.
|
|
21
|
+
*/
|
|
22
|
+
async begin() {
|
|
23
|
+
if (this.isLocked) {
|
|
24
|
+
throw new Error('Transaction Manager concurrency fault: Another transaction is already staged.');
|
|
25
|
+
}
|
|
26
|
+
this.isLocked = true;
|
|
27
|
+
this.journal = [];
|
|
28
|
+
await fs.mkdir(this.backupDirectory, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Stages a destructive file modification or creation sequence.
|
|
33
|
+
* @param {string} filePath - Absolute system file target location
|
|
34
|
+
* @param {string} nextContent - The completely rewritten proposed source structure
|
|
35
|
+
*/
|
|
36
|
+
async stageWrite(filePath, nextContent) {
|
|
37
|
+
this.assertLock();
|
|
38
|
+
let originalContent = null;
|
|
39
|
+
let backupPath = null;
|
|
40
|
+
let operationType = 'UPDATE';
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await fs.access(filePath);
|
|
44
|
+
originalContent = await fs.readFile(filePath, 'utf8');
|
|
45
|
+
|
|
46
|
+
const fileId = crypto.createHash('sha256').update(filePath).digest('hex');
|
|
47
|
+
backupPath = path.join(this.backupDirectory, `${fileId}.bak`);
|
|
48
|
+
await fs.writeFile(backupPath, originalContent, 'utf8');
|
|
49
|
+
} catch {
|
|
50
|
+
operationType = 'CREATE';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Attempt target file mutation write directly to disk
|
|
54
|
+
await fs.writeFile(filePath, nextContent, 'utf8');
|
|
55
|
+
|
|
56
|
+
this.journal.push({
|
|
57
|
+
type: operationType,
|
|
58
|
+
targetFile: filePath,
|
|
59
|
+
backupLocation: backupPath
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Stages an element deletion sequence safely.
|
|
65
|
+
* @param {string} filePath - Target component being evaluated as dead
|
|
66
|
+
*/
|
|
67
|
+
async stageDeletion(filePath) {
|
|
68
|
+
this.assertLock();
|
|
69
|
+
|
|
70
|
+
// Read previous state data for archiving before dropping linkage
|
|
71
|
+
const originalContent = await fs.readFile(filePath, 'utf8');
|
|
72
|
+
const fileId = crypto.createHash('sha256').update(filePath).digest('hex');
|
|
73
|
+
const backupPath = path.join(this.backupDirectory, `${fileId}.bak`);
|
|
74
|
+
|
|
75
|
+
await fs.writeFile(backupPath, originalContent, 'utf8');
|
|
76
|
+
await fs.unlink(filePath);
|
|
77
|
+
|
|
78
|
+
this.journal.push({
|
|
79
|
+
type: 'DELETE',
|
|
80
|
+
targetFile: filePath,
|
|
81
|
+
backupLocation: backupPath
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Finalizes the transaction sequence, cleaning up local transaction cache points.
|
|
87
|
+
*/
|
|
88
|
+
async commit() {
|
|
89
|
+
this.assertLock();
|
|
90
|
+
try {
|
|
91
|
+
for (const record of this.journal) {
|
|
92
|
+
if (record.backupLocation) {
|
|
93
|
+
await fs.unlink(record.backupLocation).catch(() => {});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
this.releaseLock();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reverts changes to their original states if any error or verification failure is flagged.
|
|
103
|
+
*/
|
|
104
|
+
async rollback() {
|
|
105
|
+
this.assertLock();
|
|
106
|
+
try {
|
|
107
|
+
// Process journal logs in reverse execution order to preserve dependencies
|
|
108
|
+
for (let i = this.journal.length - 1; i >= 0; i--) {
|
|
109
|
+
const record = this.journal[i];
|
|
110
|
+
|
|
111
|
+
if (record.type === 'CREATE') {
|
|
112
|
+
await fs.unlink(record.targetFile).catch(() => {});
|
|
113
|
+
} else if (record.type === 'UPDATE' || record.type === 'DELETE') {
|
|
114
|
+
try {
|
|
115
|
+
const originalContent = await fs.readFile(record.backupLocation, 'utf8');
|
|
116
|
+
await fs.writeFile(record.targetFile, originalContent, 'utf8');
|
|
117
|
+
await fs.unlink(record.backupLocation).catch(() => {});
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.warn(`[Transaction] Rollback failed for ${record.targetFile}: ${e.message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} finally {
|
|
124
|
+
this.releaseLock();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
assertLock() {
|
|
129
|
+
if (!this.isLocked) {
|
|
130
|
+
throw new Error('Transaction Manager boundary violation: No active tracking block exists.');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
releaseLock() {
|
|
135
|
+
this.isLocked = false;
|
|
136
|
+
this.journal = [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ambient Type Declaration (`.d.ts`) Alignment Supervisor
|
|
7
|
+
* Updates declaration mapping entries to keep compiler checks stable.
|
|
8
|
+
*/
|
|
9
|
+
export class TypeIntegrity {
|
|
10
|
+
constructor(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Re-evaluates ambient files to verify type alignment after optimization changes.
|
|
16
|
+
* @param {string} sourceFilePath - Absolute filename target context location
|
|
17
|
+
* @param {string} prunedSymbolName - Target variable token removed from active source graph
|
|
18
|
+
*/
|
|
19
|
+
async synchronizeDeclarationFile(sourceFilePath, prunedSymbolName) {
|
|
20
|
+
const fileDirectory = path.dirname(sourceFilePath);
|
|
21
|
+
const baselineName = path.basename(sourceFilePath, path.extname(sourceFilePath));
|
|
22
|
+
|
|
23
|
+
// Map standard ambient types build output combinations
|
|
24
|
+
const declarationTargets = [
|
|
25
|
+
path.join(fileDirectory, `${baselineName}.d.ts`),
|
|
26
|
+
path.join(this.context.cwd, 'dist', `${baselineName}.d.ts`),
|
|
27
|
+
path.join(this.context.cwd, 'types', `${baselineName}.d.ts`)
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
for (const dtsPath of declarationTargets) {
|
|
31
|
+
try {
|
|
32
|
+
await fs.access(dtsPath);
|
|
33
|
+
const code = await fs.readFile(dtsPath, 'utf8');
|
|
34
|
+
|
|
35
|
+
const sourceFile = ts.createSourceFile(
|
|
36
|
+
dtsPath,
|
|
37
|
+
code,
|
|
38
|
+
ts.ScriptTarget.Latest,
|
|
39
|
+
true,
|
|
40
|
+
ts.ScriptKind.TS
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const replacementIntervals = [];
|
|
44
|
+
this.inspectDtsNodes(sourceFile, prunedSymbolName, replacementIntervals);
|
|
45
|
+
|
|
46
|
+
if (replacementIntervals.length > 0) {
|
|
47
|
+
let updatedDtsText = code;
|
|
48
|
+
// Apply substring text deletions from back to front to keep offset indexes accurate
|
|
49
|
+
replacementIntervals.sort((a, b) => b.start - a.start);
|
|
50
|
+
|
|
51
|
+
for (const interval of replacementIntervals) {
|
|
52
|
+
updatedDtsText = updatedDtsText.slice(0, interval.start) + updatedDtsText.slice(interval.end);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await fs.writeFile(dtsPath, updatedDtsText, 'utf8');
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Declaration file variation target absent; proceed to fallback check routes
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
inspectDtsNodes(node, matchSymbol, intervals) {
|
|
64
|
+
if (!node) return;
|
|
65
|
+
|
|
66
|
+
// Match type mutations inside ambient structural parameters
|
|
67
|
+
if (ts.isExportSpecifier(node) && node.name.text === matchSymbol) {
|
|
68
|
+
intervals.push({ start: node.getStart(), end: node.getEnd() });
|
|
69
|
+
} else if ((ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node)) && node.name && node.name.text === matchSymbol) {
|
|
70
|
+
intervals.push({ start: node.getStart(), end: node.getEnd() });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ts.forEachChild(node, child => this.inspectDtsNodes(child, matchSymbol, intervals));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export class CircularDetector {
|
|
2
|
+
constructor(context) {
|
|
3
|
+
this.context = context;
|
|
4
|
+
this.cycles = [];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
detectCycles(graph, context = null) {
|
|
8
|
+
if (context) this.context = context;
|
|
9
|
+
this.cwd = context?.cwd || this.context?.cwd || process.cwd();
|
|
10
|
+
this.cycles = [];
|
|
11
|
+
|
|
12
|
+
let index = 0;
|
|
13
|
+
const stack = [];
|
|
14
|
+
const indices = new Map();
|
|
15
|
+
const lowlink = new Map();
|
|
16
|
+
const onStack = new Set();
|
|
17
|
+
|
|
18
|
+
const strongconnect = (v) => {
|
|
19
|
+
indices.set(v, index);
|
|
20
|
+
lowlink.set(v, index);
|
|
21
|
+
index++;
|
|
22
|
+
stack.push(v);
|
|
23
|
+
onStack.add(v);
|
|
24
|
+
|
|
25
|
+
const node = graph.get(v);
|
|
26
|
+
if (node && node.outgoingEdges) {
|
|
27
|
+
for (const w of node.outgoingEdges) {
|
|
28
|
+
if (!indices.has(w)) {
|
|
29
|
+
strongconnect(w);
|
|
30
|
+
lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
|
|
31
|
+
} else if (onStack.has(w)) {
|
|
32
|
+
lowlink.set(v, Math.min(lowlink.get(v), indices.get(w)));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (lowlink.get(v) === indices.get(v)) {
|
|
38
|
+
const component = [];
|
|
39
|
+
let w;
|
|
40
|
+
do {
|
|
41
|
+
w = stack.pop();
|
|
42
|
+
onStack.delete(w);
|
|
43
|
+
component.push(w);
|
|
44
|
+
} while (w !== v);
|
|
45
|
+
|
|
46
|
+
if (component.length > 1) {
|
|
47
|
+
this.cycles.push(component.reverse());
|
|
48
|
+
} else {
|
|
49
|
+
const node = graph.get(v);
|
|
50
|
+
if (node && node.outgoingEdges && node.outgoingEdges.has(v)) {
|
|
51
|
+
this.cycles.push([v]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
for (const v of graph.keys()) {
|
|
58
|
+
if (!indices.has(v)) {
|
|
59
|
+
strongconnect(v);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return this.cycles;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
formatCycles() {
|
|
67
|
+
return this.cycles.map(cycle => {
|
|
68
|
+
const paths = cycle.map(p => {
|
|
69
|
+
let rel = p.replace(this.context.cwd, '').replace(/^\//, '');
|
|
70
|
+
if (rel.includes(':\\')) rel = rel.split(':\\')[1] || rel;
|
|
71
|
+
return rel;
|
|
72
|
+
});
|
|
73
|
+
if (cycle.length === 1) return `${paths[0]} -> (self-loop)`;
|
|
74
|
+
return paths.join(' -> ') + ' -> ' + paths[0];
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getCycleDetails() {
|
|
79
|
+
return this.cycles.map((cycle, idx) => ({
|
|
80
|
+
cycleId: idx + 1,
|
|
81
|
+
files: cycle.map(p => {
|
|
82
|
+
let rel = p.replace(this.context.cwd, '').replace(/^\//, '');
|
|
83
|
+
if (rel.includes(':\\')) rel = rel.split(':\\')[1] || rel;
|
|
84
|
+
return rel;
|
|
85
|
+
}),
|
|
86
|
+
length: cycle.length,
|
|
87
|
+
isSelfLoop: cycle.length === 1
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export default CircularDetector;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Loads and parses entkapp configuration files.
|
|
7
|
+
* Supports scaffold.config.js, .scaffoldrc.json, and .scaffoldrc.
|
|
8
|
+
*/
|
|
9
|
+
export class ConfigLoader {
|
|
10
|
+
constructor(context) {
|
|
11
|
+
this.context = context;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async loadConfig(projectRoot) {
|
|
15
|
+
const searchPaths = [
|
|
16
|
+
'entkapp.json',
|
|
17
|
+
'entkapp.jsonc',
|
|
18
|
+
'.entkapp.json',
|
|
19
|
+
'.entkapp.jsonc',
|
|
20
|
+
'entkapp.js',
|
|
21
|
+
'entkapp.ts',
|
|
22
|
+
'entkapp/config.json',
|
|
23
|
+
'scaffold.config.js',
|
|
24
|
+
'scaffold.config.mjs',
|
|
25
|
+
'.scaffoldrc.json',
|
|
26
|
+
'.scaffoldrc'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
let config = this.getDefaultConfig();
|
|
30
|
+
|
|
31
|
+
// Try package.json first
|
|
32
|
+
try {
|
|
33
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
34
|
+
const pkgContent = await fs.readFile(pkgPath, 'utf8');
|
|
35
|
+
const pkg = JSON.parse(pkgContent);
|
|
36
|
+
if (pkg['entkapp']) {
|
|
37
|
+
Object.assign(config, pkg['entkapp']);
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// ignore
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const fileName of searchPaths) {
|
|
44
|
+
const fullPath = path.join(projectRoot, fileName);
|
|
45
|
+
try {
|
|
46
|
+
await fs.access(fullPath);
|
|
47
|
+
|
|
48
|
+
if (fileName.endsWith('.js') || fileName.endsWith('.mjs') || fileName.endsWith('.ts')) {
|
|
49
|
+
const module = await import(pathToFileURL(fullPath).href);
|
|
50
|
+
Object.assign(config, module.default || module);
|
|
51
|
+
break;
|
|
52
|
+
} else {
|
|
53
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
54
|
+
// Very basic JSONC parsing (strip comments)
|
|
55
|
+
const stripped = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
|
|
56
|
+
Object.assign(config, JSON.parse(stripped));
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return config;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getDefaultConfig() {
|
|
68
|
+
return {
|
|
69
|
+
entryPoints: ['src/index.ts', 'index.js', 'src/index.js', 'src/main.ts', 'src/main.js'],
|
|
70
|
+
exclude: [
|
|
71
|
+
'node_modules/**',
|
|
72
|
+
'dist/**',
|
|
73
|
+
'build/**',
|
|
74
|
+
'**/*.test.ts',
|
|
75
|
+
'**/*.spec.ts',
|
|
76
|
+
'**/*.test.js',
|
|
77
|
+
'**/*.spec.js'
|
|
78
|
+
],
|
|
79
|
+
plugins: [],
|
|
80
|
+
rules: {
|
|
81
|
+
'no-unused-exports': 'error',
|
|
82
|
+
'no-unused-vars': 'warn',
|
|
83
|
+
'no-dead-code': 'error'
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import resolve from 'enhanced-resolve';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Industrial-Strength Module Resolution Supervisor
|
|
6
|
+
* Integrates path mapping and workspace topologies using enhanced-resolve.
|
|
7
|
+
*/
|
|
8
|
+
export class DependencyResolver {
|
|
9
|
+
constructor(context, pathMapper, workspaceGraph) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.pathMapper = pathMapper;
|
|
12
|
+
this.workspaceGraph = workspaceGraph;
|
|
13
|
+
|
|
14
|
+
// Instantiate production-grade enhanced-resolve workspace parameters
|
|
15
|
+
this.nativeResolver = resolve.create.sync({
|
|
16
|
+
conditionNames: ['import', 'module', 'require', 'node', 'types'],
|
|
17
|
+
// Extensions prioritized: TS then JS
|
|
18
|
+
extensions: ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json', '.vue'],
|
|
19
|
+
mainFields: ['module', 'main', 'types'],
|
|
20
|
+
exportsFields: ['exports'],
|
|
21
|
+
symlinks: true
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolves a raw import string from a source file into an absolute file path.
|
|
27
|
+
* Handles the "trap" where .ts files import .js files that should resolve to .ts.
|
|
28
|
+
*/
|
|
29
|
+
resolveModulePath(containingFile, importSpecifier) {
|
|
30
|
+
if (importSpecifier.startsWith('node:') || [
|
|
31
|
+
'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console', 'constants',
|
|
32
|
+
'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'fs/promises', 'http', 'http2',
|
|
33
|
+
'https', 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks', 'process',
|
|
34
|
+
'punycode', 'querystring', 'readline', 'repl', 'stream', 'string_decoder',
|
|
35
|
+
'timers', 'tls', 'trace_events', 'tty', 'url', 'util', 'v8', 'vm', 'worker_threads', 'zlib'
|
|
36
|
+
].includes(importSpecifier)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const containingDir = path.dirname(containingFile);
|
|
41
|
+
|
|
42
|
+
// --- NEW: Fix for JS-in-TS import trap ---
|
|
43
|
+
// If we are in a TS file and importing something ending in .js,
|
|
44
|
+
// we should try to resolve the .ts version first.
|
|
45
|
+
let effectiveSpecifier = importSpecifier;
|
|
46
|
+
const isTsFile = /\.(ts|tsx|mts|cts)$/.test(containingFile);
|
|
47
|
+
if (isTsFile && importSpecifier.endsWith('.js')) {
|
|
48
|
+
// Try replacing .js with .ts
|
|
49
|
+
const tsSpecifier = importSpecifier.replace(/\.js$/, '.ts');
|
|
50
|
+
try {
|
|
51
|
+
const resolvedTs = this.nativeResolver(containingDir, tsSpecifier);
|
|
52
|
+
if (this.isAbsoluteInternalPath(resolvedTs)) return resolvedTs;
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// Fall back to original specifier if .ts version doesn't exist
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// -----------------------------------------
|
|
58
|
+
|
|
59
|
+
// Rule A: Intercept and resolve local monorepo workspace cross-links
|
|
60
|
+
if (this.workspaceGraph.isLocalWorkspaceSpecifier(effectiveSpecifier)) {
|
|
61
|
+
const match = this.workspaceGraph.getWorkspacePackageMatch(effectiveSpecifier);
|
|
62
|
+
if (match) {
|
|
63
|
+
if (effectiveSpecifier === match.packageName) {
|
|
64
|
+
return match.entryPoints[0] || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const subPathOffset = effectiveSpecifier.slice(match.packageName.length + 1);
|
|
68
|
+
try {
|
|
69
|
+
return this.nativeResolver(match.rootDirectory, `./${subPathOffset}`);
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Rule B: Intercept and expand path mapping aliases (@/*)
|
|
76
|
+
const aliasedCandidates = this.pathMapper.resolveCandidatePaths(effectiveSpecifier);
|
|
77
|
+
if (aliasedCandidates.length > 0) {
|
|
78
|
+
for (const candidate of aliasedCandidates) {
|
|
79
|
+
try {
|
|
80
|
+
const resolvedPath = this.nativeResolver(containingDir, candidate);
|
|
81
|
+
if (this.isAbsoluteInternalPath(resolvedPath)) {
|
|
82
|
+
return resolvedPath;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Rule C: Standard file system lookups
|
|
90
|
+
try {
|
|
91
|
+
const resolvedPath = this.nativeResolver(containingDir, effectiveSpecifier);
|
|
92
|
+
if (this.isAbsoluteInternalPath(resolvedPath)) {
|
|
93
|
+
return resolvedPath;
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (this.context.verbose) {
|
|
97
|
+
console.debug(`[Resolution Trace Skip] Specifier unresolvable: ${effectiveSpecifier} inside ${containingFile}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
isAbsoluteInternalPath(resolvedPath) {
|
|
105
|
+
if (!resolvedPath) return false;
|
|
106
|
+
const normalized = resolvedPath.replace(/\\/g, '/');
|
|
107
|
+
|
|
108
|
+
if (normalized.includes('/node_modules/')) {
|
|
109
|
+
for (const [name, meta] of this.workspaceGraph.packageManifests.entries()) {
|
|
110
|
+
if (normalized.startsWith(meta.rootDirectory.replace(/\\/g, '/'))) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return path.isAbsolute(resolvedPath);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
determineIntentProfile(filePath, declaredExportsManifest) {
|
|
121
|
+
const fileName = path.basename(filePath);
|
|
122
|
+
const isPublicContractFile = /(^index|^public\-api|^entry)\.(ts|js|tsx|jsx)$/i.test(fileName);
|
|
123
|
+
|
|
124
|
+
if (isPublicContractFile) {
|
|
125
|
+
for (const [symbolKey, metadata] of declaredExportsManifest.entries()) {
|
|
126
|
+
metadata.isLibraryContract = true;
|
|
127
|
+
}
|
|
128
|
+
return 'LIBRARY_CONSUMPTION_TARGET';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return 'INTERNAL_CODEBASE_ELEMENT';
|
|
132
|
+
}
|
|
133
|
+
}
|