pkg-scaffold 3.3.4 ā 3.3.5
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 +3 -4
- package/bin/cli.js +2 -2
- package/package.json +4 -3
- package/src/EngineContext.js +33 -19
- package/src/ast/ASTAnalyzer.js +105 -77
- package/src/ast/BarrelParser.js +24 -4
- package/src/ast/MagicDetector.js +111 -13
- package/src/ast/OxcAnalyzer.js +72 -13
- package/src/healing/GitSandbox.js +44 -122
- package/src/healing/SelfHealer.js +29 -130
- package/src/index.js +115 -98
- package/src/performance/WorkerTaskRunner.js +17 -5
- package/src/plugins/PluginRegistry.js +27 -1
- package/src/resolution/DependencyProfiler.js +261 -9
- package/src/resolution/WorkSpaceGraph.js +133 -34
- package/src/performance/SecretDetector.js +0 -378
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
> **The Ultimate Enterprise Codebase Janitor.** Faster than Knip with OXC integration, type-aware analysis, and
|
|
5
|
+
> **The Ultimate Enterprise Codebase Janitor.** Faster than Knip with OXC integration, type-aware analysis, and automated structural healing. Fully standalone - solving what Knip cannot.
|
|
6
6
|
|
|
7
7
|
  
|
|
8
8
|
|
|
@@ -15,8 +15,7 @@
|
|
|
15
15
|
* **š True Dead Code Detection:** Advanced graph-based reachability analysis to find truly dead files and unused exports, even deep within your codebase.
|
|
16
16
|
* **š Circular Dependency Detection:** High-performance Tarjan-based algorithm to detect and report circular dependencies.
|
|
17
17
|
* **š”ļø Supply Chain Guard:** Detects typosquatting and verifies integrity lockfile hashes.
|
|
18
|
-
*
|
|
19
|
-
* **š ļø Self-Healing:** Not just reporting, but automatically fixing structural issues (removing dead files, pruning unused dependencies).
|
|
18
|
+
* **š ļø Automated Structural Healing:** Not just reporting, but automatically fixing structural issues (removing dead files, pruning unused dependencies) with git-based rollback protection.
|
|
20
19
|
* **āļø Flexible Configuration:** Supports `pkg-scaffold.json`, `pkg-scaffold.ts`, `scaffold.config.js`, and more.
|
|
21
20
|
|
|
22
21
|
## š¦ Installation
|
|
@@ -26,7 +25,7 @@ npm install -D pkg-scaffold
|
|
|
26
25
|
# or
|
|
27
26
|
pnpm add -D pkg-scaffold
|
|
28
27
|
# or
|
|
29
|
-
|
|
28
|
+
pnpm add -D pkg-scaffold
|
|
30
29
|
```
|
|
31
30
|
|
|
32
31
|
## š Usage
|
package/bin/cli.js
CHANGED
|
@@ -28,7 +28,7 @@ async function bootstrap() {
|
|
|
28
28
|
program
|
|
29
29
|
.name('pkg-scaffold')
|
|
30
30
|
.description(ansis.cyan('Enterprise-Grade AST Syntax Refactoring & Self-Healing Engine'))
|
|
31
|
-
.version(packageJsonContent.version || '3.3.
|
|
31
|
+
.version(packageJsonContent.version || '3.3.5');
|
|
32
32
|
|
|
33
33
|
program
|
|
34
34
|
.option('-c, --cwd <path>', 'Specify the execution context root directory', process.cwd())
|
|
@@ -130,7 +130,7 @@ async function bootstrap() {
|
|
|
130
130
|
}, timeoutMs);
|
|
131
131
|
timeoutTimer.unref(); // Allow process to exit if work finishes
|
|
132
132
|
|
|
133
|
-
console.log(ansis.bold.green(`\nš¦ pkg-scaffold v${packageJsonContent.version || '3.3.
|
|
133
|
+
console.log(ansis.bold.green(`\nš¦ pkg-scaffold v${packageJsonContent.version || '3.3.5'} Engine Activation`));
|
|
134
134
|
console.log(ansis.dim('------------------------------------------------------------'));
|
|
135
135
|
console.log(`${ansis.bold('Target Workspace Root :')} ${ansis.blue(targetCwd)}`);
|
|
136
136
|
console.log(`${ansis.bold('Refactoring Mode :')} ${options.fix ? ansis.yellow('Active Fixing & Self-Healing Enabled') : ansis.gray('Dry-Run Reporting Only')}`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pkg-scaffold",
|
|
3
|
-
"version": "3.3.
|
|
4
|
-
"description": "The Ultimate Enterprise Codebase Janitor. Faster than Knip with OXC integration, type-aware analysis, and
|
|
3
|
+
"version": "3.3.5",
|
|
4
|
+
"description": "The Ultimate Enterprise Codebase Janitor. Faster than Knip with OXC integration, type-aware analysis, and automated structural healing. Fully standalone - solving what Knip cannot.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"package.json",
|
|
49
49
|
"packages",
|
|
50
50
|
"scan",
|
|
51
|
-
|
|
51
|
+
|
|
52
|
+
"structural-healing",
|
|
52
53
|
"setup",
|
|
53
54
|
"type",
|
|
54
55
|
"types",
|
package/src/EngineContext.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ============================================================================
|
|
3
|
-
* š¦ pkg-scaffold v3.3.
|
|
3
|
+
* š¦ pkg-scaffold v3.3.5: Enterprise In-Memory Codebase State Manifest
|
|
4
4
|
* ============================================================================
|
|
5
5
|
* Implements a high-density, centralized graph database context for tracking
|
|
6
6
|
* software engineering debt, dependencies, types, and vulnerabilities.
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import fs from 'fs/promises';
|
|
11
|
+
import { DependencyProfiler } from './resolution/DependencyProfiler.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* High-Fidelity Graph Element Node representing a single file asset boundary.
|
|
@@ -42,8 +43,7 @@ export class GraphNode {
|
|
|
42
43
|
this.incomingEdges = new Set(); // Set of absolute filePaths depending on this component
|
|
43
44
|
this.outgoingEdges = new Set(); // Set of absolute internal filePaths this component calls
|
|
44
45
|
|
|
45
|
-
//
|
|
46
|
-
this.securityThreats = [];
|
|
46
|
+
// Structural Syntax Boundaries
|
|
47
47
|
this.calculatedDynamicImports = [];
|
|
48
48
|
this.localSuppressedRules = new Set();
|
|
49
49
|
this.externalPackageUsage = new Set(); // Tracked third-party package names
|
|
@@ -160,6 +160,9 @@ export class EngineContext {
|
|
|
160
160
|
};
|
|
161
161
|
this.usedExternalPackages = new Set(); // Global set of used npm packages
|
|
162
162
|
this.manifestDependencies = new Map(); // Package.json path -> { dependencies, devDependencies, peerDependencies, optionalDependencies }
|
|
163
|
+
|
|
164
|
+
// DependencyProfiler instance for implicit invocation tracing
|
|
165
|
+
this._depProfiler = new DependencyProfiler(this);
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
/**
|
|
@@ -237,7 +240,7 @@ export class EngineContext {
|
|
|
237
240
|
* Processes the entire active dependency map to compile structural issue indices.
|
|
238
241
|
* Evaluates orphaned components, dead exports, and supply-chain threats.
|
|
239
242
|
*/
|
|
240
|
-
generateSummaryReport() {
|
|
243
|
+
async generateSummaryReport() {
|
|
241
244
|
this.metrics.endTime = Date.now();
|
|
242
245
|
const durationSeconds = ((this.metrics.endTime - this.metrics.startTime) / 1000).toFixed(2);
|
|
243
246
|
|
|
@@ -254,7 +257,6 @@ export class EngineContext {
|
|
|
254
257
|
structuralIssuesDetected: {
|
|
255
258
|
deadFiles: [],
|
|
256
259
|
deadExports: [],
|
|
257
|
-
securityThreats: [],
|
|
258
260
|
unusedDependencies: []
|
|
259
261
|
},
|
|
260
262
|
modificationsExecuted: {
|
|
@@ -266,6 +268,11 @@ export class EngineContext {
|
|
|
266
268
|
for (const [filePath, node] of this.graph.entries()) {
|
|
267
269
|
// Skip package control files from standard structural dead-code checks
|
|
268
270
|
if (filePath.endsWith('package.json')) continue;
|
|
271
|
+
|
|
272
|
+
// Fix: Always track external package usage from all files, even if they are orphaned,
|
|
273
|
+
// to prevent false-positive unused dependency reports.
|
|
274
|
+
node.externalPackageUsage.forEach(pkg => this.usedExternalPackages.add(pkg));
|
|
275
|
+
|
|
269
276
|
if (this.isPathIgnored(filePath)) continue;
|
|
270
277
|
|
|
271
278
|
const relativePath = path.relative(this.cwd, filePath);
|
|
@@ -299,28 +306,34 @@ export class EngineContext {
|
|
|
299
306
|
}
|
|
300
307
|
}
|
|
301
308
|
|
|
302
|
-
// Category C: Security Vulnerabilities
|
|
303
|
-
if (node.securityThreats && node.securityThreats.length > 0) {
|
|
304
|
-
node.securityThreats.forEach(threat => {
|
|
305
|
-
summary.structuralIssuesDetected.securityThreats.push({
|
|
306
|
-
file: relativePath,
|
|
307
|
-
identifier: threat.variableKey,
|
|
308
|
-
riskCode: threat.riskCode || 'HIGH_RISK_SECRET_LEAK',
|
|
309
|
-
entropy: threat.entropyValue,
|
|
310
|
-
line: threat.line || 1
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
309
|
}
|
|
315
310
|
|
|
316
311
|
// Category D: Unused Dependencies Audit (Enhanced Classification)
|
|
317
312
|
for (const [manifestPath, manifestData] of this.manifestDependencies.entries()) {
|
|
318
313
|
const relativeManifest = path.relative(this.cwd, manifestPath);
|
|
319
|
-
|
|
314
|
+
const packageRoot = path.dirname(manifestPath);
|
|
315
|
+
|
|
316
|
+
// Collect packages that are implicitly used via scripts / config files
|
|
317
|
+
const implicitlyUsed = await this._depProfiler.traceImplicitInvocations(packageRoot);
|
|
318
|
+
|
|
319
|
+
// Resolve peer dependencies of all used packages so they are not flagged
|
|
320
|
+
const allUsedForPeerResolution = new Set([...this.usedExternalPackages, ...implicitlyUsed]);
|
|
321
|
+
const peerDepsOfUsed = await this._depProfiler.resolvePeerDependencies(allUsedForPeerResolution, packageRoot);
|
|
322
|
+
|
|
320
323
|
const checkDeps = (deps, type) => {
|
|
321
324
|
if (!deps) return;
|
|
322
325
|
for (const dep of deps) {
|
|
323
|
-
|
|
326
|
+
// Skip peer and optional dependencies ā they are never "unused" in the
|
|
327
|
+
// traditional sense because they are not required to be imported directly.
|
|
328
|
+
if (this._depProfiler.shouldExcludeFromUnusedCheck(dep, type)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const isUsedInCode = this.usedExternalPackages.has(dep);
|
|
333
|
+
const isUsedImplicitly = implicitlyUsed.has(dep);
|
|
334
|
+
const isRequiredAsPeer = peerDepsOfUsed.has(dep);
|
|
335
|
+
|
|
336
|
+
if (!isUsedInCode && !isUsedImplicitly && !isRequiredAsPeer) {
|
|
324
337
|
summary.structuralIssuesDetected.unusedDependencies.push({
|
|
325
338
|
manifest: relativeManifest,
|
|
326
339
|
package: dep,
|
|
@@ -333,6 +346,7 @@ export class EngineContext {
|
|
|
333
346
|
|
|
334
347
|
checkDeps(manifestData.dependencies, 'dependency');
|
|
335
348
|
checkDeps(manifestData.devDependencies, 'devDependency');
|
|
349
|
+
// peerDependencies and optionalDependencies are excluded via shouldExcludeFromUnusedCheck
|
|
336
350
|
checkDeps(manifestData.peerDependencies, 'peerDependency');
|
|
337
351
|
checkDeps(manifestData.optionalDependencies, 'optionalDependency');
|
|
338
352
|
}
|
package/src/ast/ASTAnalyzer.js
CHANGED
|
@@ -6,6 +6,16 @@ export class ASTAnalyzer {
|
|
|
6
6
|
this.scopeStack = [];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Returns the TypeScript ScriptKind for a given file path.
|
|
11
|
+
* Exposed as a public method so WorkerTaskRunner can call it directly.
|
|
12
|
+
*/
|
|
13
|
+
getScriptKind(filePath) {
|
|
14
|
+
if (filePath.endsWith('.tsx') || filePath.endsWith('.jsx')) return ts.ScriptKind.TSX;
|
|
15
|
+
if (filePath.endsWith('.js') || filePath.endsWith('.mjs') || filePath.endsWith('.cjs')) return ts.ScriptKind.JS;
|
|
16
|
+
return ts.ScriptKind.TS;
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
parseFile(filePath, content, fileNode) {
|
|
10
20
|
if (this.context.verbose) {
|
|
11
21
|
console.log(`[AST] Parsing: ${filePath}`);
|
|
@@ -16,7 +26,7 @@ export class ASTAnalyzer {
|
|
|
16
26
|
content,
|
|
17
27
|
ts.ScriptTarget.Latest,
|
|
18
28
|
true,
|
|
19
|
-
|
|
29
|
+
this.getScriptKind(filePath)
|
|
20
30
|
);
|
|
21
31
|
|
|
22
32
|
this.currentScope = { symbols: new Map(), parent: null };
|
|
@@ -29,6 +39,13 @@ export class ASTAnalyzer {
|
|
|
29
39
|
this.currentScope = null;
|
|
30
40
|
}
|
|
31
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Alias for walkAST used by WorkerTaskRunner (legacy API compatibility).
|
|
44
|
+
*/
|
|
45
|
+
walkNode(node, sourceFile, fileNode) {
|
|
46
|
+
return this.walkAST(node, fileNode, sourceFile);
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
pushScope() {
|
|
33
50
|
const newScope = { symbols: new Map(), parent: this.currentScope };
|
|
34
51
|
this.scopeStack.push(newScope);
|
|
@@ -87,6 +104,16 @@ export class ASTAnalyzer {
|
|
|
87
104
|
case ts.SyntaxKind.ModuleDeclaration:
|
|
88
105
|
this.handleNamedDeclaration(node, fileNode, sourceFile);
|
|
89
106
|
break;
|
|
107
|
+
case ts.SyntaxKind.Identifier:
|
|
108
|
+
fileNode.instantiatedIdentifiers.add(node.text);
|
|
109
|
+
break;
|
|
110
|
+
case ts.SyntaxKind.StringLiteral:
|
|
111
|
+
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
112
|
+
fileNode.rawStringReferences.add(node.text);
|
|
113
|
+
break;
|
|
114
|
+
case ts.SyntaxKind.PropertyAccessExpression:
|
|
115
|
+
fileNode.propertyAccessChains.add(node.getText(sourceFile));
|
|
116
|
+
break;
|
|
90
117
|
case ts.SyntaxKind.CallExpression:
|
|
91
118
|
this.handleCallExpression(node, fileNode, sourceFile);
|
|
92
119
|
break;
|
|
@@ -97,13 +124,6 @@ export class ASTAnalyzer {
|
|
|
97
124
|
case ts.SyntaxKind.Decorator:
|
|
98
125
|
this.handleDecorator(node, fileNode, sourceFile);
|
|
99
126
|
break;
|
|
100
|
-
case ts.SyntaxKind.Identifier:
|
|
101
|
-
// Track usage of identifiers
|
|
102
|
-
const symbol = this.resolveSymbol(node.text);
|
|
103
|
-
if (symbol) {
|
|
104
|
-
// fileNode.usedSymbols.add(`${symbol.node.parent.kind}:${node.text}`); // More granular tracking needed
|
|
105
|
-
}
|
|
106
|
-
break;
|
|
107
127
|
}
|
|
108
128
|
|
|
109
129
|
ts.forEachChild(node, child => this.walkAST(child, fileNode, sourceFile));
|
|
@@ -117,88 +137,67 @@ export class ASTAnalyzer {
|
|
|
117
137
|
if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
118
138
|
const specifier = node.moduleSpecifier.text;
|
|
119
139
|
fileNode.explicitImports.add(specifier);
|
|
140
|
+
|
|
141
|
+
// Track external package usage for dependency analysis
|
|
142
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
143
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
144
|
+
}
|
|
120
145
|
|
|
121
146
|
if (node.importClause) {
|
|
147
|
+
if (node.importClause.name) {
|
|
148
|
+
fileNode.importedSymbols.add(`${specifier}:default`);
|
|
149
|
+
}
|
|
122
150
|
if (node.importClause.namedBindings) {
|
|
123
|
-
if (ts.
|
|
151
|
+
if (ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
152
|
+
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
153
|
+
} else if (ts.isNamedImports(node.importClause.namedBindings)) {
|
|
124
154
|
node.importClause.namedBindings.elements.forEach(element => {
|
|
125
|
-
const importedName = element.name.text;
|
|
126
|
-
|
|
127
|
-
fileNode.importedSymbols.add(`${specifier}:${propertyName}`);
|
|
128
|
-
this.addDeclaredSymbol(element.name.text, element, sourceFile); // Add local import name to scope
|
|
155
|
+
const importedName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
156
|
+
fileNode.importedSymbols.add(`${specifier}:${importedName}`);
|
|
129
157
|
});
|
|
130
|
-
} else if (ts.isNamespaceImport(node.importClause.namedBindings)) {
|
|
131
|
-
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
132
|
-
this.addDeclaredSymbol(node.importClause.namedBindings.name.text, node.importClause.namedBindings, sourceFile); // Add namespace import to scope
|
|
133
158
|
}
|
|
134
159
|
}
|
|
135
|
-
if (node.importClause.name) {
|
|
136
|
-
fileNode.importedSymbols.add(`${specifier}:default`);
|
|
137
|
-
this.addDeclaredSymbol(node.importClause.name.text, node.importClause.name, sourceFile); // Add default import to scope
|
|
138
|
-
}
|
|
139
160
|
}
|
|
140
161
|
}
|
|
141
162
|
}
|
|
142
163
|
|
|
143
164
|
handleExportDeclaration(node, fileNode, sourceFile) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
const specifier = node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : null;
|
|
166
|
+
|
|
167
|
+
if (specifier) {
|
|
168
|
+
// Re-export from source: export * from './module' or export { x } from './module'
|
|
169
|
+
fileNode.explicitImports.add(specifier);
|
|
170
|
+
|
|
171
|
+
// Track external package usage from re-exports
|
|
172
|
+
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
173
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!node.exportClause) {
|
|
177
|
+
// export * from './module'
|
|
178
|
+
fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
|
|
179
|
+
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
180
|
+
} else if (ts.isNamespaceExport(node.exportClause)) {
|
|
181
|
+
// export * as name from './module'
|
|
160
182
|
const name = node.exportClause.name.text;
|
|
161
183
|
fileNode.internalExports.set(name, { type: 're-export-namespace', source: specifier, originalName: '*', start: node.getStart(sourceFile), end: node.getEnd() });
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
} else if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
184
|
+
fileNode.importedSymbols.add(`${specifier}:*`);
|
|
185
|
+
} else if (ts.isNamedExports(node.exportClause)) {
|
|
186
|
+
// export { x, y as z } from './module'
|
|
167
187
|
node.exportClause.elements.forEach(element => {
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
fileNode.internalExports.set(
|
|
171
|
-
|
|
172
|
-
originalName: propertyName,
|
|
173
|
-
start: node.getStart(sourceFile),
|
|
174
|
-
end: node.getEnd()
|
|
175
|
-
});
|
|
188
|
+
const originalName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
189
|
+
const exportedName = element.name.text;
|
|
190
|
+
fileNode.internalExports.set(exportedName, { type: 're-export', source: specifier, originalName, start: element.getStart(sourceFile), end: element.getEnd() });
|
|
191
|
+
fileNode.importedSymbols.add(`${specifier}:${originalName}`);
|
|
176
192
|
});
|
|
177
|
-
} else if (node.declaration) {
|
|
178
|
-
// Direct export of a declaration (e.g., export const x = 1;)
|
|
179
|
-
if (ts.isVariableStatement(node.declaration)) {
|
|
180
|
-
node.declaration.declarationList.declarations.forEach(decl => {
|
|
181
|
-
if (decl.name && ts.isIdentifier(decl.name)) {
|
|
182
|
-
const name = decl.name.text;
|
|
183
|
-
fileNode.internalExports.set(name, {
|
|
184
|
-
type: 'variable',
|
|
185
|
-
start: decl.getStart(sourceFile),
|
|
186
|
-
end: decl.getEnd()
|
|
187
|
-
});
|
|
188
|
-
this.addDeclaredSymbol(name, decl, sourceFile);
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
} else if (ts.isFunctionDeclaration(node.declaration) || ts.isClassDeclaration(node.declaration)) {
|
|
192
|
-
const name = node.declaration.name?.text;
|
|
193
|
-
if (name) {
|
|
194
|
-
fileNode.internalExports.set(name, {
|
|
195
|
-
type: ts.SyntaxKind[node.declaration.kind].toLowerCase().replace('declaration', ''),
|
|
196
|
-
start: node.declaration.getStart(sourceFile),
|
|
197
|
-
end: node.declaration.getEnd()
|
|
198
|
-
});
|
|
199
|
-
this.addDeclaredSymbol(name, node.declaration, sourceFile);
|
|
200
|
-
}
|
|
201
193
|
}
|
|
194
|
+
} else if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
195
|
+
// Local named exports: export { x, y as z }
|
|
196
|
+
node.exportClause.elements.forEach(element => {
|
|
197
|
+
const localName = element.propertyName ? element.propertyName.text : element.name.text;
|
|
198
|
+
const exportedName = element.name.text;
|
|
199
|
+
fileNode.internalExports.set(exportedName, { type: 'export', originalName: localName, start: element.getStart(sourceFile), end: element.getEnd() });
|
|
200
|
+
});
|
|
202
201
|
}
|
|
203
202
|
}
|
|
204
203
|
|
|
@@ -325,16 +324,32 @@ export class ASTAnalyzer {
|
|
|
325
324
|
}
|
|
326
325
|
|
|
327
326
|
handleCallExpression(node, fileNode, sourceFile) {
|
|
327
|
+
// Dynamic import(): import('./module').then(...)
|
|
328
328
|
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
329
329
|
const arg = node.arguments[0];
|
|
330
|
-
if (arg
|
|
331
|
-
|
|
332
|
-
|
|
330
|
+
if (arg) {
|
|
331
|
+
if (ts.isStringLiteral(arg)) {
|
|
332
|
+
fileNode.explicitImports.add(arg.text);
|
|
333
|
+
fileNode.dynamicImports.add(arg.text);
|
|
334
|
+
// Track external package usage from dynamic imports
|
|
335
|
+
if (!arg.text.startsWith('.') && !arg.text.startsWith('/')) {
|
|
336
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(arg.text));
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
// Dynamic import with a non-literal expression (e.g., variable or template literal).
|
|
340
|
+
if (fileNode.calculatedDynamicImports) {
|
|
341
|
+
fileNode.calculatedDynamicImports.push({ kind: ts.SyntaxKind[arg.kind], start: arg.getStart(sourceFile) });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
333
344
|
}
|
|
334
345
|
} else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
335
346
|
const arg = node.arguments[0];
|
|
336
347
|
if (arg && ts.isStringLiteral(arg)) {
|
|
337
348
|
fileNode.explicitImports.add(arg.text);
|
|
349
|
+
// Track external package usage from require() calls
|
|
350
|
+
if (!arg.text.startsWith('.') && !arg.text.startsWith('/')) {
|
|
351
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(arg.text));
|
|
352
|
+
}
|
|
338
353
|
}
|
|
339
354
|
}
|
|
340
355
|
}
|
|
@@ -371,7 +386,6 @@ export class ASTAnalyzer {
|
|
|
371
386
|
if (ts.isCallExpression(node.expression)) {
|
|
372
387
|
node.expression.arguments.forEach(arg => {
|
|
373
388
|
// Further analysis of arguments can be done here if needed
|
|
374
|
-
// e.g., if (ts.isStringLiteral(arg)) fileNode.decoratorArgs.add(`${decoratorName}:${arg.text}`);
|
|
375
389
|
});
|
|
376
390
|
}
|
|
377
391
|
}
|
|
@@ -391,4 +405,18 @@ export class ASTAnalyzer {
|
|
|
391
405
|
}
|
|
392
406
|
}
|
|
393
407
|
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Extracts the root npm package name from an import specifier.
|
|
411
|
+
* Handles scoped packages (@scope/pkg) and subpath imports (pkg/utils, @scope/pkg/utils).
|
|
412
|
+
*/
|
|
413
|
+
_extractPackageName(specifier) {
|
|
414
|
+
if (specifier.startsWith('@')) {
|
|
415
|
+
// Scoped package: @scope/name or @scope/name/subpath
|
|
416
|
+
const parts = specifier.split('/');
|
|
417
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
|
|
418
|
+
}
|
|
419
|
+
// Regular package: name or name/subpath
|
|
420
|
+
return specifier.split('/')[0];
|
|
421
|
+
}
|
|
394
422
|
}
|
package/src/ast/BarrelParser.js
CHANGED
|
@@ -178,19 +178,39 @@ export class BarrelParser {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// Rule D: Sweep through anonymous star re-exports vectors (export * from 'module')
|
|
181
|
+
//
|
|
182
|
+
// Algorithm:
|
|
183
|
+
// 1. For each `export * from './child'`, recursively resolve the symbol in the child.
|
|
184
|
+
// 2. A non-barrel child immediately returns `{ originFile: childPath }` regardless of
|
|
185
|
+
// whether it actually declares the symbol. We therefore must verify that the
|
|
186
|
+
// returned origin file actually contains the symbol in its declaredLocalExports
|
|
187
|
+
// before accepting the result.
|
|
188
|
+
// 3. If the child is itself a barrel, the recursive call already performs the full
|
|
189
|
+
// chain walk, so we only need to verify the final origin.
|
|
181
190
|
for (const relativePath of spec.wildcardExports) {
|
|
182
191
|
const fullyResolvedPath = this.resolver.resolveModulePath(contextFilePath, relativePath);
|
|
183
192
|
|
|
184
193
|
if (fullyResolvedPath) {
|
|
185
|
-
// Look inside the graph metadata map configuration to verify child target availability
|
|
186
194
|
const continuousResolutionTrace = await this.determineSymbolDeclarationOrigin(
|
|
187
195
|
fullyResolvedPath,
|
|
188
196
|
targetSymbolName,
|
|
189
197
|
activeProjectGraph,
|
|
190
|
-
protectionStack
|
|
198
|
+
new Set(protectionStack) // Use a copy so sibling branches don't block each other
|
|
191
199
|
);
|
|
192
|
-
|
|
193
|
-
if (continuousResolutionTrace
|
|
200
|
+
|
|
201
|
+
if (!continuousResolutionTrace) continue;
|
|
202
|
+
if (continuousResolutionTrace.originFile === contextFilePath) continue;
|
|
203
|
+
|
|
204
|
+
// Verify that the resolved origin actually declares the symbol.
|
|
205
|
+
// This prevents a non-barrel sibling (e.g. constants.ts) from being
|
|
206
|
+
// incorrectly returned for a symbol it does not export (e.g. formatData).
|
|
207
|
+
const originSpec = await this.parseBarrelSpecification(continuousResolutionTrace.originFile);
|
|
208
|
+
if (originSpec.declaredLocalExports.has(continuousResolutionTrace.originSymbol)) {
|
|
209
|
+
return continuousResolutionTrace;
|
|
210
|
+
}
|
|
211
|
+
// The origin spec is itself a barrel (isBarrelInstance = true) and the
|
|
212
|
+
// recursive call already resolved through it ā accept the result.
|
|
213
|
+
if (originSpec.isBarrelInstance) {
|
|
194
214
|
return continuousResolutionTrace;
|
|
195
215
|
}
|
|
196
216
|
}
|
package/src/ast/MagicDetector.js
CHANGED
|
@@ -6,6 +6,15 @@ import { PluginRegistry } from '../plugins/PluginRegistry.js';
|
|
|
6
6
|
* Ecosystem Entry Point Manifest & Dynamic Framework Router Heuristic Validator
|
|
7
7
|
* Intercepts implicit conventions to handle cases where direct import statements are absent.
|
|
8
8
|
* Now refactored to use a pluggable architecture.
|
|
9
|
+
*
|
|
10
|
+
* Improvements over v1:
|
|
11
|
+
* - Extended config-file detection list (Biome, Oxlint, tsup, unbuild, etc.)
|
|
12
|
+
* - Next.js App Router conventions (page.tsx, layout.tsx, loading.tsx, error.tsx, etc.)
|
|
13
|
+
* - Remix conventions (route files under app/routes/)
|
|
14
|
+
* - SvelteKit conventions (+page.svelte, +layout.svelte, etc.)
|
|
15
|
+
* - Astro page/layout conventions
|
|
16
|
+
* - Common entry-point patterns (bin/, cli.ts, server.ts, main.ts, app.ts)
|
|
17
|
+
* - Test file patterns extended to cover Vitest workspace files
|
|
9
18
|
*/
|
|
10
19
|
export class MagicDetector {
|
|
11
20
|
constructor(context) {
|
|
@@ -60,20 +69,109 @@ export class MagicDetector {
|
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
isCoreToolingSuiteElement(normalizedPath) {
|
|
63
|
-
//
|
|
64
|
-
if (/\.(test|spec|e2e|cy)\.(js|ts|tsx|jsx
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
'.
|
|
71
|
-
'
|
|
72
|
-
|
|
73
|
-
'
|
|
72
|
+
// āā Test / spec files āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
73
|
+
if (/\.(test|spec|e2e|cy)\.(js|ts|tsx|jsx)$/i.test(normalizedPath)) return true;
|
|
74
|
+
if (/\.stories\.(js|ts|tsx|jsx)$/i.test(normalizedPath)) return true;
|
|
75
|
+
|
|
76
|
+
// āā Build / bundler config files āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
77
|
+
const configFragments = [
|
|
78
|
+
// Test runners
|
|
79
|
+
'jest.config.', 'vitest.config.', 'vitest.workspace.',
|
|
80
|
+
'playwright.config.', 'cypress.config.',
|
|
81
|
+
// Bundlers
|
|
82
|
+
'webpack.config.', 'vite.config.', 'rollup.config.',
|
|
83
|
+
'esbuild.config.', 'parcel.config.',
|
|
84
|
+
'tsup.config.', 'unbuild.config.', 'pkgroll.config.',
|
|
85
|
+
// CSS / styling
|
|
86
|
+
'tailwind.config.', 'postcss.config.', '.postcssrc.',
|
|
87
|
+
// Linters / formatters
|
|
88
|
+
'.eslintrc.', 'eslint.config.', 'prettier.config.', '.prettierrc.',
|
|
89
|
+
'.stylelintrc.', 'stylelint.config.',
|
|
90
|
+
'biome.json', '.oxlintrc.',
|
|
91
|
+
// Babel / transpilation
|
|
92
|
+
'.babelrc.', 'babel.config.',
|
|
93
|
+
// Commit / git hooks
|
|
94
|
+
'.commitlintrc.', 'commitlint.config.',
|
|
95
|
+
'.lintstagedrc.', 'lint-staged.config.',
|
|
96
|
+
// Documentation
|
|
97
|
+
'typedoc.config.', 'typedoc.json',
|
|
98
|
+
// Monorepo tools
|
|
99
|
+
'turbo.json', 'nx.json', 'lerna.json',
|
|
100
|
+
// Misc tooling
|
|
101
|
+
'knip.config.', 'knip.json',
|
|
102
|
+
'syncpack.config.',
|
|
103
|
+
// Internal worker
|
|
104
|
+
'WorkerTaskRunner.js'
|
|
74
105
|
];
|
|
75
|
-
|
|
76
|
-
|
|
106
|
+
if (configFragments.some(f => normalizedPath.includes(f))) return true;
|
|
107
|
+
|
|
108
|
+
// āā Common application entry points āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
109
|
+
const entryPatterns = [
|
|
110
|
+
// CLI binaries
|
|
111
|
+
'/bin/cli.js', '/bin/cli.ts', '/bin/cli.mjs',
|
|
112
|
+
'/bin/index.js', '/bin/index.ts',
|
|
113
|
+
// Server / app entry points
|
|
114
|
+
'/src/server.ts', '/src/server.js',
|
|
115
|
+
'/src/main.ts', '/src/main.js',
|
|
116
|
+
'/src/app.ts', '/src/app.js',
|
|
117
|
+
'/src/index.ts', '/src/index.tsx',
|
|
118
|
+
'/src/index.js', '/src/index.jsx',
|
|
119
|
+
'/server.ts', '/server.js',
|
|
120
|
+
'/main.ts', '/main.js',
|
|
121
|
+
'/app.ts', '/app.js',
|
|
122
|
+
'/index.ts', '/index.tsx',
|
|
123
|
+
'/index.js', '/index.jsx',
|
|
124
|
+
];
|
|
125
|
+
if (entryPatterns.some(p => normalizedPath.endsWith(p))) return true;
|
|
126
|
+
|
|
127
|
+
// āā Next.js App Router conventions āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
128
|
+
// Files under app/ directory with Next.js special names
|
|
129
|
+
if (/\/app\/(page|layout|loading|error|not-found|template|default|route|middleware)\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
|
|
130
|
+
// Next.js Pages Router
|
|
131
|
+
if (/\/pages\/.*\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
|
|
132
|
+
// Next.js API routes
|
|
133
|
+
if (/\/pages\/api\/.*\.(js|ts)$/.test(normalizedPath)) return true;
|
|
134
|
+
// Next.js middleware
|
|
135
|
+
if (/\/middleware\.(js|ts)$/.test(normalizedPath)) return true;
|
|
136
|
+
// Next.js config
|
|
137
|
+
if (/\/next\.config\.(js|ts|mjs|cjs)$/.test(normalizedPath)) return true;
|
|
138
|
+
|
|
139
|
+
// āā Remix conventions āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
140
|
+
if (/\/app\/routes\/.*\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
|
|
141
|
+
if (/\/app\/root\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
|
|
142
|
+
if (/\/app\/entry\.(client|server)\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
|
|
143
|
+
|
|
144
|
+
// āā SvelteKit conventions āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
145
|
+
if (/\/\+page(\.(server|client))?\.(svelte|js|ts)$/.test(normalizedPath)) return true;
|
|
146
|
+
if (/\/\+layout(\.(server|client))?\.(svelte|js|ts)$/.test(normalizedPath)) return true;
|
|
147
|
+
if (/\/\+error\.(svelte|js|ts)$/.test(normalizedPath)) return true;
|
|
148
|
+
if (/\/\+server\.(js|ts)$/.test(normalizedPath)) return true;
|
|
149
|
+
if (/\/svelte\.config\.(js|ts|mjs)$/.test(normalizedPath)) return true;
|
|
150
|
+
|
|
151
|
+
// āā Astro conventions āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
152
|
+
if (/\/src\/pages\/.*\.astro$/.test(normalizedPath)) return true;
|
|
153
|
+
if (/\/src\/layouts\/.*\.astro$/.test(normalizedPath)) return true;
|
|
154
|
+
if (/\/astro\.config\.(mjs|js|ts)$/.test(normalizedPath)) return true;
|
|
155
|
+
|
|
156
|
+
// āā Nuxt conventions āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
157
|
+
if (/\/pages\/.*\.vue$/.test(normalizedPath)) return true;
|
|
158
|
+
if (/\/layouts\/.*\.vue$/.test(normalizedPath)) return true;
|
|
159
|
+
if (/\/server\/api\/.*\.(js|ts)$/.test(normalizedPath)) return true;
|
|
160
|
+
if (/\/nuxt\.config\.(js|ts|mjs)$/.test(normalizedPath)) return true;
|
|
161
|
+
|
|
162
|
+
// āā React / Vite entry points āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
163
|
+
if (/\/vite\.config\.(js|ts|mjs)$/.test(normalizedPath)) return true;
|
|
164
|
+
|
|
165
|
+
// āā Angular conventions āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
166
|
+
if (/\/main\.(ts|js)$/.test(normalizedPath)) return true;
|
|
167
|
+
if (/\/app\.module\.(ts|js)$/.test(normalizedPath)) return true;
|
|
168
|
+
if (/\/angular\.json$/.test(normalizedPath)) return true;
|
|
169
|
+
|
|
170
|
+
// āā Expo / React Native āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
171
|
+
if (/\/app\/_layout\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
|
|
172
|
+
if (/\/app\/index\.(js|ts|tsx|jsx)$/.test(normalizedPath)) return true;
|
|
173
|
+
|
|
174
|
+
return false;
|
|
77
175
|
}
|
|
78
176
|
|
|
79
177
|
/**
|