pkg-scaffold 2.2.0 → 2.4.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/bin/cli.js +91 -0
- package/index.js +2254 -2544
- package/package.json +19 -7
- package/src/EngineContext.js +282 -0
- package/src/ast/ASTAnalyzer.js +313 -0
- package/src/ast/BarrelParser.js +177 -0
- package/src/ast/MagicDetector.js +154 -0
- package/src/healing/GitSandbox.js +160 -0
- package/src/healing/SelfHealer.js +150 -0
- package/src/index.js +343 -0
- package/src/performance/GraphCache.js +82 -0
- package/src/performance/SupplyChainGuard.js +106 -0
- package/src/performance/WorkerPool.js +89 -0
- package/src/performance/WorkerTaskRunner.js +64 -0
- package/src/refractor/ImpactAnalyzer.js +92 -0
- package/src/refractor/SourceRewriter.js +86 -0
- package/src/refractor/TransactionManager.js +131 -0
- package/src/refractor/TypeIntegrity.js +75 -0
- package/src/resolution/DepencyResolver.js +120 -0
- package/src/resolution/PathMapper.js +115 -0
- package/src/resolution/WorkSpaceGraph.js +171 -0
- package/tsconfig.json +26 -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 === '.scaffold-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,131 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Transactional File System Operations Supervisor
|
|
6
|
+
* Guarantees atomicity across multi-file transformations by tracking rolling backups.
|
|
7
|
+
*/
|
|
8
|
+
export class TransactionManager {
|
|
9
|
+
constructor(context) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.backupDirectory = path.join(context.cacheDir, 'backups');
|
|
12
|
+
this.journal = [];
|
|
13
|
+
this.isLocked = false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Begins a new transaction lifecycle loop, initializing isolation vectors.
|
|
18
|
+
*/
|
|
19
|
+
async begin() {
|
|
20
|
+
if (this.isLocked) {
|
|
21
|
+
throw new Error('Transaction Manager concurrency fault: Another transaction is already staged.');
|
|
22
|
+
}
|
|
23
|
+
this.isLocked = true;
|
|
24
|
+
this.journal = [];
|
|
25
|
+
await fs.mkdir(this.backupDirectory, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Stages a destructive file modification or creation sequence.
|
|
30
|
+
* @param {string} filePath - Absolute system file target location
|
|
31
|
+
* @param {string} nextContent - The completely rewritten proposed source structure
|
|
32
|
+
*/
|
|
33
|
+
async stageWrite(filePath, nextContent) {
|
|
34
|
+
this.assertLock();
|
|
35
|
+
let originalContent = null;
|
|
36
|
+
let backupPath = null;
|
|
37
|
+
let operationType = 'UPDATE';
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await fs.access(filePath);
|
|
41
|
+
originalContent = await fs.readFile(filePath, 'utf8');
|
|
42
|
+
|
|
43
|
+
const fileId = Buffer.from(filePath).toString('base64url');
|
|
44
|
+
backupPath = path.join(this.backupDirectory, `${fileId}.bak`);
|
|
45
|
+
await fs.writeFile(backupPath, originalContent, 'utf8');
|
|
46
|
+
} catch {
|
|
47
|
+
operationType = 'CREATE';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Attempt target file mutation write directly to disk
|
|
51
|
+
await fs.writeFile(filePath, nextContent, 'utf8');
|
|
52
|
+
|
|
53
|
+
this.journal.push({
|
|
54
|
+
type: operationType,
|
|
55
|
+
targetFile: filePath,
|
|
56
|
+
backupLocation: backupPath
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Stages an element deletion sequence safely.
|
|
62
|
+
* @param {string} filePath - Target component being evaluated as dead
|
|
63
|
+
*/
|
|
64
|
+
async stageDeletion(filePath) {
|
|
65
|
+
this.assertLock();
|
|
66
|
+
|
|
67
|
+
// Read previous state data for archiving before dropping linkage
|
|
68
|
+
const originalContent = await fs.readFile(filePath, 'utf8');
|
|
69
|
+
const fileId = Buffer.from(filePath).toString('base64url');
|
|
70
|
+
const backupPath = path.join(this.backupDirectory, `${fileId}.bak`);
|
|
71
|
+
|
|
72
|
+
await fs.writeFile(backupPath, originalContent, 'utf8');
|
|
73
|
+
await fs.unlink(filePath);
|
|
74
|
+
|
|
75
|
+
this.journal.push({
|
|
76
|
+
type: 'DELETE',
|
|
77
|
+
targetFile: filePath,
|
|
78
|
+
backupLocation: backupPath
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Finalizes the transaction sequence, cleaning up local transaction cache points.
|
|
84
|
+
*/
|
|
85
|
+
async commit() {
|
|
86
|
+
this.assertLock();
|
|
87
|
+
try {
|
|
88
|
+
for (const record of this.journal) {
|
|
89
|
+
if (record.backupLocation) {
|
|
90
|
+
await fs.unlink(record.backupLocation).catch(() => {});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
this.releaseLock();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Reverts changes to their original states if any error or verification failure is flagged.
|
|
100
|
+
*/
|
|
101
|
+
async rollback() {
|
|
102
|
+
this.assertLock();
|
|
103
|
+
try {
|
|
104
|
+
// Process journal logs in reverse execution order to preserve dependencies
|
|
105
|
+
for (let i = this.journal.length - 1; i >= 0; i--) {
|
|
106
|
+
const record = this.journal[i];
|
|
107
|
+
|
|
108
|
+
if (record.type === 'CREATE') {
|
|
109
|
+
await fs.unlink(record.targetFile).catch(() => {});
|
|
110
|
+
} else if (record.type === 'UPDATE' || record.type === 'DELETE') {
|
|
111
|
+
const originalContent = await fs.readFile(record.backupLocation, 'utf8');
|
|
112
|
+
await fs.writeFile(record.targetFile, originalContent, 'utf8');
|
|
113
|
+
await fs.unlink(record.backupLocation).catch(() => {});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} finally {
|
|
117
|
+
this.releaseLock();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
assertLock() {
|
|
122
|
+
if (!this.isLocked) {
|
|
123
|
+
throw new Error('Transaction Manager boundary violation: No active tracking block exists.');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
releaseLock() {
|
|
128
|
+
this.isLocked = false;
|
|
129
|
+
this.journal = [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -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,120 @@
|
|
|
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: ['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs', '.json', '.vue'],
|
|
18
|
+
mainFields: ['module', 'main', 'types'],
|
|
19
|
+
exportsFields: ['exports'],
|
|
20
|
+
symlinks: true
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolves a raw import string from a source file into an absolute file path.
|
|
26
|
+
* @param {string} containingFile - Absolute path of the file containing the import declaration
|
|
27
|
+
* @param {string} importSpecifier - Raw import target string (e.g., '../components/Button' or '@utils/math')
|
|
28
|
+
* @returns {string|null} Resolved absolute file path location on disk, or null if external/third-party node_module
|
|
29
|
+
*/
|
|
30
|
+
resolveModulePath(containingFile, importSpecifier) {
|
|
31
|
+
const containingDir = path.dirname(containingFile);
|
|
32
|
+
|
|
33
|
+
// Rule A: Intercept and resolve local monorepo workspace cross-links
|
|
34
|
+
if (this.workspaceGraph.isLocalWorkspaceSpecifier(importSpecifier)) {
|
|
35
|
+
const match = this.workspaceGraph.getWorkspacePackageMatch(importSpecifier);
|
|
36
|
+
if (match) {
|
|
37
|
+
if (importSpecifier === match.packageName) {
|
|
38
|
+
// Point directly to the target package's configured index entry file
|
|
39
|
+
return match.entryPoints[0] || null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Handle deep sub-path monorepo target imports
|
|
43
|
+
const subPathOffset = importSpecifier.slice(match.packageName.length + 1);
|
|
44
|
+
try {
|
|
45
|
+
return this.nativeResolver(match.rootDirectory, `./${subPathOffset}`);
|
|
46
|
+
} catch {
|
|
47
|
+
// Fall back to scanning the package root directly if the sub-path lookup fails
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Rule B: Intercept and expand path mapping aliases (@/*)
|
|
53
|
+
const aliasedCandidates = this.pathMapper.resolveCandidatePaths(importSpecifier);
|
|
54
|
+
if (aliasedCandidates.length > 0) {
|
|
55
|
+
for (const candidate of aliasedCandidates) {
|
|
56
|
+
try {
|
|
57
|
+
const resolvedPath = this.nativeResolver(containingDir, candidate);
|
|
58
|
+
if (this.isAbsoluteInternalPath(resolvedPath)) {
|
|
59
|
+
return resolvedPath;
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// Candidate target path absent; try the next fallback pattern entry
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Rule C: Standard file system lookups for standard files or package assets
|
|
68
|
+
try {
|
|
69
|
+
const resolvedPath = this.nativeResolver(containingDir, importSpecifier);
|
|
70
|
+
if (this.isAbsoluteInternalPath(resolvedPath)) {
|
|
71
|
+
return resolvedPath;
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (this.context.verbose) {
|
|
75
|
+
// Output trace logs for unresolvable dependencies during deep code investigations
|
|
76
|
+
console.debug(`[Resolution Trace Skip] Specifier unresolvable from context: ${importSpecifier} inside ${containingFile}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null; // Target is an external node_module dependency or an unresolvable asset
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Ensures our tracking focus stays locked onto internal codebase components, bypassing third-party node_modules.
|
|
85
|
+
*/
|
|
86
|
+
isAbsoluteInternalPath(resolvedPath) {
|
|
87
|
+
if (!resolvedPath) return false;
|
|
88
|
+
const normalized = resolvedPath.replace(/\\/g, '/');
|
|
89
|
+
|
|
90
|
+
// Ignore external node_modules blocks, but preserve local monorepo packages that live inside symlinked node_modules
|
|
91
|
+
if (normalized.includes('/node_modules/')) {
|
|
92
|
+
for (const [name, meta] of this.workspaceGraph.packageManifests.entries()) {
|
|
93
|
+
if (normalized.startsWith(meta.rootDirectory.replace(/\\/g, '/'))) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return path.isAbsolute(resolvedPath);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Challenge #17 Intent Detection. Evaluates if an export serves as a consumer contract distribution node.
|
|
105
|
+
*/
|
|
106
|
+
determineIntentProfile(filePath, declaredExportsManifest) {
|
|
107
|
+
const fileName = path.basename(filePath);
|
|
108
|
+
const isPublicContractFile = /(^index|^public\-api|^entry)\.(ts|js|tsx|jsx)$/i.test(fileName);
|
|
109
|
+
|
|
110
|
+
// If the file is a primary bundle entry point, flag its exports as protected public contracts
|
|
111
|
+
if (isPublicContractFile) {
|
|
112
|
+
for (const [symbolKey, metadata] of declaredExportsManifest.entries()) {
|
|
113
|
+
metadata.isLibraryContract = true;
|
|
114
|
+
}
|
|
115
|
+
return 'LIBRARY_CONSUMPTION_TARGET';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return 'INTERNAL_CODEBASE_ELEMENT';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Advanced TSConfig / JSConfig Compilation Path Alias Mapper
|
|
6
|
+
* Resolves deeply nested route mappings, wildcards, and base URL overrides.
|
|
7
|
+
*/
|
|
8
|
+
export class PathMapper {
|
|
9
|
+
constructor(context) {
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.baseUrl = '.';
|
|
12
|
+
this.absoluteBaseUrl = context.cwd;
|
|
13
|
+
this.mappings = []; // Collection of { prefix, suffix, targets[] }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Reads, cleans, and indexes custom alias entries from tsconfig.json files.
|
|
18
|
+
* @param {string} tsconfigFilename - Target designator (typically tsconfig.json)
|
|
19
|
+
*/
|
|
20
|
+
async loadMappings(tsconfigFilename = 'tsconfig.json') {
|
|
21
|
+
const configPath = path.resolve(this.context.cwd, tsconfigFilename);
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await fs.access(configPath);
|
|
25
|
+
const rawText = await fs.readFile(configPath, 'utf8');
|
|
26
|
+
|
|
27
|
+
// Strip inline single-line and block comments before parsing
|
|
28
|
+
const jsonCleanText = rawText.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1');
|
|
29
|
+
const tsconfig = JSON.parse(jsonCleanText);
|
|
30
|
+
|
|
31
|
+
if (!tsconfig.compilerOptions) return;
|
|
32
|
+
|
|
33
|
+
const opts = tsconfig.compilerOptions;
|
|
34
|
+
if (opts.baseUrl) {
|
|
35
|
+
this.baseUrl = opts.baseUrl;
|
|
36
|
+
this.absoluteBaseUrl = path.resolve(this.context.cwd, this.baseUrl);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (opts.paths) {
|
|
40
|
+
for (const [aliasPattern, targetArrays] of Object.entries(opts.paths)) {
|
|
41
|
+
this.registerPatternRule(aliasPattern, targetArrays);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (this.context.verbose) {
|
|
46
|
+
console.warn(`⚠️ [PathMapper Override] Proceeding without custom path configurations. Source: ${error.message}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Registers structural lookup parameters for alias strings.
|
|
53
|
+
*/
|
|
54
|
+
registerPatternRule(pattern, targets) {
|
|
55
|
+
const wildcardIndex = pattern.indexOf('*');
|
|
56
|
+
|
|
57
|
+
if (wildcardIndex === -1) {
|
|
58
|
+
this.mappings.push({
|
|
59
|
+
isExact: true,
|
|
60
|
+
pattern,
|
|
61
|
+
targets: targets.map(t => path.normalize(t))
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const prefix = pattern.slice(0, wildcardIndex);
|
|
67
|
+
const suffix = pattern.slice(wildcardIndex + 1);
|
|
68
|
+
|
|
69
|
+
this.mappings.push({
|
|
70
|
+
isExact: false,
|
|
71
|
+
prefix,
|
|
72
|
+
suffix,
|
|
73
|
+
targets: targets.map(t => path.normalize(t))
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolves a raw import specifier against mapped path patterns.
|
|
79
|
+
* @param {string} specifier - Raw text from import declaration (e.g., '@ui/button')
|
|
80
|
+
* @returns {Array<string>} Candidates of absolute filesystem paths to try resolving
|
|
81
|
+
*/
|
|
82
|
+
resolveCandidatePaths(specifier) {
|
|
83
|
+
const matchingCandidates = [];
|
|
84
|
+
|
|
85
|
+
for (const rule of this.mappings) {
|
|
86
|
+
if (rule.isExact) {
|
|
87
|
+
if (specifier === rule.pattern) {
|
|
88
|
+
rule.targets.forEach(target => {
|
|
89
|
+
matchingCandidates.push(path.resolve(this.absoluteBaseUrl, target));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
// Evaluate wildcard pattern matches
|
|
94
|
+
if (specifier.startsWith(rule.prefix) && specifier.endsWith(rule.suffix)) {
|
|
95
|
+
const extractedWildcardContent = specifier.slice(
|
|
96
|
+
rule.prefix.length,
|
|
97
|
+
specifier.length - rule.suffix.length
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
rule.targets.forEach(targetTemplate => {
|
|
101
|
+
const interpolatedTarget = targetTemplate.replace('*', extractedWildcardContent);
|
|
102
|
+
matchingCandidates.push(path.resolve(this.absoluteBaseUrl, interpolatedTarget));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Fall back to direct lookup relative to the base URL
|
|
109
|
+
if (!specifier.startsWith('.') && !path.isAbsolute(specifier)) {
|
|
110
|
+
matchingCandidates.push(path.resolve(this.absoluteBaseUrl, specifier));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return matchingCandidates;
|
|
114
|
+
}
|
|
115
|
+
}
|