pkg-scaffold 3.3.4 ā 3.3.6
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 -5
- package/bin/cli.js +4 -4
- package/package.json +10 -4
- package/src/EngineContext.js +52 -27
- package/src/ast/ASTAnalyzer.js +111 -77
- package/src/ast/BarrelParser.js +24 -4
- package/src/ast/MagicDetector.js +106 -13
- package/src/ast/OxcAnalyzer.js +121 -20
- package/src/ast/SecretScanner.js +304 -0
- package/src/healing/GitSandbox.js +44 -122
- package/src/healing/SelfHealer.js +29 -130
- package/src/index.js +175 -97
- package/src/performance/WorkerPool.js +6 -3
- package/src/plugins/PluginRegistry.js +27 -1
- package/src/resolution/DependencyProfiler.js +261 -9
- package/src/resolution/WorkSpaceGraph.js +142 -34
- package/src/performance/SecretDetector.js +0 -378
- package/src/performance/WorkerTaskRunner.js +0 -71
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
|
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
* **š Massive Plugin Ecosystem:** Over 20+ built-in plugins (Next.js, Nuxt, SvelteKit, Tailwind, Jest, Vitest, Playwright, GitHub Actions, Webpack, Babel, Rollup, ESLint, Prettier, Husky, and many more).
|
|
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
|
+
* **š Secrets Scanning:** Automatically detects hardcoded API keys, tokens, and credentials (v3.3.6+).
|
|
17
18
|
* **š”ļø 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).
|
|
19
|
+
* **š ļø Automated Structural Healing:** Not just reporting, but automatically fixing structural issues (removing dead files, pruning unused dependencies) with git-based rollback protection.
|
|
20
20
|
* **āļø Flexible Configuration:** Supports `pkg-scaffold.json`, `pkg-scaffold.ts`, `scaffold.config.js`, and more.
|
|
21
21
|
|
|
22
22
|
## š¦ Installation
|
|
@@ -26,7 +26,7 @@ npm install -D pkg-scaffold
|
|
|
26
26
|
# or
|
|
27
27
|
pnpm add -D pkg-scaffold
|
|
28
28
|
# or
|
|
29
|
-
|
|
29
|
+
pnpm add -D pkg-scaffold
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
## š Usage
|
|
@@ -34,9 +34,11 @@ yarn add -D pkg-scaffold
|
|
|
34
34
|
Run the CLI at the root of your project:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
npx pkg-scaffold
|
|
37
|
+
npx pkg-scaffold -r
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
> **Note**: Always use the `-r` flag to start the analysis loop. v3.3.6+ also features **Auto-Detection for Monorepos**.
|
|
41
|
+
|
|
40
42
|
### CLI Options
|
|
41
43
|
|
|
42
44
|
* `-c, --cwd <path>`: Specify the execution context root directory.
|
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())
|
|
@@ -64,7 +64,7 @@ async function bootstrap() {
|
|
|
64
64
|
const answer = await rl.question(ansis.bold.yellow('ā No "pkg-scaffold:run" script found in package.json. Install it? (y/n): '));
|
|
65
65
|
if (answer.toLowerCase() === 'y') {
|
|
66
66
|
pkgJson.scripts = pkgJson.scripts || {};
|
|
67
|
-
pkgJson.scripts['pkg-scaffold:run'] = 'pkg-scaffold --fix';
|
|
67
|
+
pkgJson.scripts['pkg-scaffold:run'] = 'npx pkg-scaffold --fix';
|
|
68
68
|
await fs.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
69
69
|
console.log(ansis.green('ā
"pkg-scaffold:run" script added to package.json.'));
|
|
70
70
|
}
|
|
@@ -95,7 +95,7 @@ async function bootstrap() {
|
|
|
95
95
|
|
|
96
96
|
if (pkgJson?.scripts?.['pkg-scaffold:run'] || configInstalled) {
|
|
97
97
|
console.log(ansis.bold.cyan('\nš Setup complete! To start the engine, run:'));
|
|
98
|
-
console.log(ansis.white(` - pkg-scaffold -r`));
|
|
98
|
+
console.log(ansis.white(` - npx pkg-scaffold -r`));
|
|
99
99
|
console.log(ansis.white(` - npm run pkg-scaffold:run\n`));
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -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.6",
|
|
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": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "node bin/cli.js",
|
|
12
|
-
"pkg-scaffold:run": "
|
|
12
|
+
"pkg-scaffold:run": "npx pkg-scaffold --fix",
|
|
13
13
|
"test": "echo \"Error: no test specified\" && exit 0",
|
|
14
14
|
"test:stability": "npm run test",
|
|
15
15
|
"docs:dev": "vitepress dev docs",
|
|
@@ -48,7 +48,13 @@
|
|
|
48
48
|
"package.json",
|
|
49
49
|
"packages",
|
|
50
50
|
"scan",
|
|
51
|
-
"
|
|
51
|
+
"security",
|
|
52
|
+
"secret-scanner",
|
|
53
|
+
"secrets",
|
|
54
|
+
"hardcoded-secrets",
|
|
55
|
+
"credentials",
|
|
56
|
+
"audit",
|
|
57
|
+
"structural-healing",
|
|
52
58
|
"setup",
|
|
53
59
|
"type",
|
|
54
60
|
"types",
|
package/src/EngineContext.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ============================================================================
|
|
3
|
-
* š¦ pkg-scaffold v3.3.
|
|
3
|
+
* š¦ pkg-scaffold v3.3.6: 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,14 +43,15 @@ 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
|
|
50
50
|
|
|
51
51
|
// Detailed AST Location Diagnostics (Symbol -> Structural Location Mapping)
|
|
52
52
|
this.symbolSourceLocations = new Map(); // Symbol -> { line: number, column: number, length: number }
|
|
53
|
+
// Security threat findings for this file
|
|
54
|
+
this.securityThreats = [];
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/**
|
|
@@ -64,17 +66,25 @@ export class GraphNode {
|
|
|
64
66
|
if (!parentNode) continue;
|
|
65
67
|
|
|
66
68
|
// Check if the symbol is explicitly imported by the parent
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
+
// Strategy 1: Check absolute path based tokens (more reliable)
|
|
70
|
+
const absoluteImportKey = `${this.filePath}:${symbolName}`;
|
|
71
|
+
const absoluteStarKey = `${this.filePath}:*`;
|
|
69
72
|
|
|
70
|
-
if (parentNode.importedSymbols.has(
|
|
73
|
+
if (parentNode.importedSymbols.has(absoluteImportKey) || parentNode.importedSymbols.has(absoluteStarKey)) {
|
|
71
74
|
return true;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
// Check
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
77
|
+
// Strategy 2: Check relative path based tokens (legacy/compatibility)
|
|
78
|
+
const relativePath = path.relative(path.dirname(parentPath), this.filePath).replace(/\\/g, '/');
|
|
79
|
+
const relativePathNoExt = relativePath.replace(/\.(js|ts|tsx|jsx)$/, '');
|
|
80
|
+
|
|
81
|
+
const importKey = `${relativePath}:${symbolName}`;
|
|
82
|
+
const importKeyAlt = `${relativePathNoExt}:${symbolName}`;
|
|
83
|
+
const starKey = `${relativePath}:*`;
|
|
84
|
+
const starKeyAlt = `${relativePathNoExt}:*`;
|
|
85
|
+
|
|
86
|
+
if (parentNode.importedSymbols.has(importKey) || parentNode.importedSymbols.has(importKeyAlt) ||
|
|
87
|
+
parentNode.importedSymbols.has(starKey) || parentNode.importedSymbols.has(starKeyAlt)) {
|
|
78
88
|
return true;
|
|
79
89
|
}
|
|
80
90
|
|
|
@@ -117,7 +127,7 @@ export class GraphNode {
|
|
|
117
127
|
incomingDependenciesCount: this.incomingEdges.size,
|
|
118
128
|
outgoingDependenciesCount: this.outgoingEdges.size,
|
|
119
129
|
isDanglingOrphan: this.incomingEdges.size === 0 && !this.isLibraryEntry,
|
|
120
|
-
trackedThreatsCount: this.securityThreats.length
|
|
130
|
+
trackedThreatsCount: (this.securityThreats || []).length
|
|
121
131
|
};
|
|
122
132
|
}
|
|
123
133
|
}
|
|
@@ -160,6 +170,10 @@ export class EngineContext {
|
|
|
160
170
|
};
|
|
161
171
|
this.usedExternalPackages = new Set(); // Global set of used npm packages
|
|
162
172
|
this.manifestDependencies = new Map(); // Package.json path -> { dependencies, devDependencies, peerDependencies, optionalDependencies }
|
|
173
|
+
this.allSecretFindings = []; // Aggregated hardcoded secret findings across all scanned files
|
|
174
|
+
|
|
175
|
+
// DependencyProfiler instance for implicit invocation tracing
|
|
176
|
+
this._depProfiler = new DependencyProfiler(this);
|
|
163
177
|
}
|
|
164
178
|
|
|
165
179
|
/**
|
|
@@ -237,7 +251,7 @@ export class EngineContext {
|
|
|
237
251
|
* Processes the entire active dependency map to compile structural issue indices.
|
|
238
252
|
* Evaluates orphaned components, dead exports, and supply-chain threats.
|
|
239
253
|
*/
|
|
240
|
-
generateSummaryReport() {
|
|
254
|
+
async generateSummaryReport() {
|
|
241
255
|
this.metrics.endTime = Date.now();
|
|
242
256
|
const durationSeconds = ((this.metrics.endTime - this.metrics.startTime) / 1000).toFixed(2);
|
|
243
257
|
|
|
@@ -254,7 +268,6 @@ export class EngineContext {
|
|
|
254
268
|
structuralIssuesDetected: {
|
|
255
269
|
deadFiles: [],
|
|
256
270
|
deadExports: [],
|
|
257
|
-
securityThreats: [],
|
|
258
271
|
unusedDependencies: []
|
|
259
272
|
},
|
|
260
273
|
modificationsExecuted: {
|
|
@@ -266,6 +279,11 @@ export class EngineContext {
|
|
|
266
279
|
for (const [filePath, node] of this.graph.entries()) {
|
|
267
280
|
// Skip package control files from standard structural dead-code checks
|
|
268
281
|
if (filePath.endsWith('package.json')) continue;
|
|
282
|
+
|
|
283
|
+
// Fix: Always track external package usage from all files, even if they are orphaned,
|
|
284
|
+
// to prevent false-positive unused dependency reports.
|
|
285
|
+
node.externalPackageUsage.forEach(pkg => this.usedExternalPackages.add(pkg));
|
|
286
|
+
|
|
269
287
|
if (this.isPathIgnored(filePath)) continue;
|
|
270
288
|
|
|
271
289
|
const relativePath = path.relative(this.cwd, filePath);
|
|
@@ -299,28 +317,34 @@ export class EngineContext {
|
|
|
299
317
|
}
|
|
300
318
|
}
|
|
301
319
|
|
|
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
320
|
}
|
|
315
321
|
|
|
316
322
|
// Category D: Unused Dependencies Audit (Enhanced Classification)
|
|
317
323
|
for (const [manifestPath, manifestData] of this.manifestDependencies.entries()) {
|
|
318
324
|
const relativeManifest = path.relative(this.cwd, manifestPath);
|
|
319
|
-
|
|
325
|
+
const packageRoot = path.dirname(manifestPath);
|
|
326
|
+
|
|
327
|
+
// Collect packages that are implicitly used via scripts / config files
|
|
328
|
+
const implicitlyUsed = await this._depProfiler.traceImplicitInvocations(packageRoot);
|
|
329
|
+
|
|
330
|
+
// Resolve peer dependencies of all used packages so they are not flagged
|
|
331
|
+
const allUsedForPeerResolution = new Set([...this.usedExternalPackages, ...implicitlyUsed]);
|
|
332
|
+
const peerDepsOfUsed = await this._depProfiler.resolvePeerDependencies(allUsedForPeerResolution, packageRoot);
|
|
333
|
+
|
|
320
334
|
const checkDeps = (deps, type) => {
|
|
321
335
|
if (!deps) return;
|
|
322
336
|
for (const dep of deps) {
|
|
323
|
-
|
|
337
|
+
// Skip peer and optional dependencies ā they are never "unused" in the
|
|
338
|
+
// traditional sense because they are not required to be imported directly.
|
|
339
|
+
if (this._depProfiler.shouldExcludeFromUnusedCheck(dep, type)) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const isUsedInCode = this.usedExternalPackages.has(dep);
|
|
344
|
+
const isUsedImplicitly = implicitlyUsed.has(dep);
|
|
345
|
+
const isRequiredAsPeer = peerDepsOfUsed.has(dep);
|
|
346
|
+
|
|
347
|
+
if (!isUsedInCode && !isUsedImplicitly && !isRequiredAsPeer) {
|
|
324
348
|
summary.structuralIssuesDetected.unusedDependencies.push({
|
|
325
349
|
manifest: relativeManifest,
|
|
326
350
|
package: dep,
|
|
@@ -333,6 +357,7 @@ export class EngineContext {
|
|
|
333
357
|
|
|
334
358
|
checkDeps(manifestData.dependencies, 'dependency');
|
|
335
359
|
checkDeps(manifestData.devDependencies, 'devDependency');
|
|
360
|
+
// peerDependencies and optionalDependencies are excluded via shouldExcludeFromUnusedCheck
|
|
336
361
|
checkDeps(manifestData.peerDependencies, 'peerDependency');
|
|
337
362
|
checkDeps(manifestData.optionalDependencies, 'optionalDependency');
|
|
338
363
|
}
|
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
|
|
|
@@ -275,6 +274,8 @@ export class ASTAnalyzer {
|
|
|
275
274
|
start: decl.getStart(sourceFile),
|
|
276
275
|
end: decl.getEnd()
|
|
277
276
|
});
|
|
277
|
+
const loc = sourceFile.getLineAndCharacterOfPosition(decl.getStart(sourceFile));
|
|
278
|
+
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
278
279
|
this.addDeclaredSymbol(name, decl, sourceFile);
|
|
279
280
|
} else if (decl.name && ts.isObjectBindingPattern(decl.name)) {
|
|
280
281
|
decl.name.elements.forEach(element => {
|
|
@@ -285,6 +286,8 @@ export class ASTAnalyzer {
|
|
|
285
286
|
start: element.getStart(sourceFile),
|
|
286
287
|
end: element.getEnd()
|
|
287
288
|
});
|
|
289
|
+
const loc = sourceFile.getLineAndCharacterOfPosition(element.getStart(sourceFile));
|
|
290
|
+
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
288
291
|
this.addDeclaredSymbol(name, element, sourceFile);
|
|
289
292
|
}
|
|
290
293
|
});
|
|
@@ -297,6 +300,8 @@ export class ASTAnalyzer {
|
|
|
297
300
|
start: element.getStart(sourceFile),
|
|
298
301
|
end: element.getEnd()
|
|
299
302
|
});
|
|
303
|
+
const loc = sourceFile.getLineAndCharacterOfPosition(element.getStart(sourceFile));
|
|
304
|
+
fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
|
|
300
305
|
this.addDeclaredSymbol(name, element, sourceFile);
|
|
301
306
|
}
|
|
302
307
|
});
|
|
@@ -325,16 +330,32 @@ export class ASTAnalyzer {
|
|
|
325
330
|
}
|
|
326
331
|
|
|
327
332
|
handleCallExpression(node, fileNode, sourceFile) {
|
|
333
|
+
// Dynamic import(): import('./module').then(...)
|
|
328
334
|
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
329
335
|
const arg = node.arguments[0];
|
|
330
|
-
if (arg
|
|
331
|
-
|
|
332
|
-
|
|
336
|
+
if (arg) {
|
|
337
|
+
if (ts.isStringLiteral(arg)) {
|
|
338
|
+
fileNode.explicitImports.add(arg.text);
|
|
339
|
+
fileNode.dynamicImports.add(arg.text);
|
|
340
|
+
// Track external package usage from dynamic imports
|
|
341
|
+
if (!arg.text.startsWith('.') && !arg.text.startsWith('/')) {
|
|
342
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(arg.text));
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
// Dynamic import with a non-literal expression (e.g., variable or template literal).
|
|
346
|
+
if (fileNode.calculatedDynamicImports) {
|
|
347
|
+
fileNode.calculatedDynamicImports.push({ kind: ts.SyntaxKind[arg.kind], start: arg.getStart(sourceFile) });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
333
350
|
}
|
|
334
351
|
} else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
|
|
335
352
|
const arg = node.arguments[0];
|
|
336
353
|
if (arg && ts.isStringLiteral(arg)) {
|
|
337
354
|
fileNode.explicitImports.add(arg.text);
|
|
355
|
+
// Track external package usage from require() calls
|
|
356
|
+
if (!arg.text.startsWith('.') && !arg.text.startsWith('/')) {
|
|
357
|
+
fileNode.externalPackageUsage.add(this._extractPackageName(arg.text));
|
|
358
|
+
}
|
|
338
359
|
}
|
|
339
360
|
}
|
|
340
361
|
}
|
|
@@ -371,7 +392,6 @@ export class ASTAnalyzer {
|
|
|
371
392
|
if (ts.isCallExpression(node.expression)) {
|
|
372
393
|
node.expression.arguments.forEach(arg => {
|
|
373
394
|
// Further analysis of arguments can be done here if needed
|
|
374
|
-
// e.g., if (ts.isStringLiteral(arg)) fileNode.decoratorArgs.add(`${decoratorName}:${arg.text}`);
|
|
375
395
|
});
|
|
376
396
|
}
|
|
377
397
|
}
|
|
@@ -391,4 +411,18 @@ export class ASTAnalyzer {
|
|
|
391
411
|
}
|
|
392
412
|
}
|
|
393
413
|
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Extracts the root npm package name from an import specifier.
|
|
417
|
+
* Handles scoped packages (@scope/pkg) and subpath imports (pkg/utils, @scope/pkg/utils).
|
|
418
|
+
*/
|
|
419
|
+
_extractPackageName(specifier) {
|
|
420
|
+
if (specifier.startsWith('@')) {
|
|
421
|
+
// Scoped package: @scope/name or @scope/name/subpath
|
|
422
|
+
const parts = specifier.split('/');
|
|
423
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
|
|
424
|
+
}
|
|
425
|
+
// Regular package: name or name/subpath
|
|
426
|
+
return specifier.split('/')[0];
|
|
427
|
+
}
|
|
394
428
|
}
|
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
|
}
|