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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![Logo](./logo.png)
4
4
 
5
- > **The Ultimate Enterprise Codebase Janitor.** Faster than Knip with OXC integration, type-aware analysis, and self-healing capabilities. Fully standalone - solving what Knip cannot.
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
  ![Version](https://img.shields.io/npm/v/pkg-scaffold) ![License](https://img.shields.io/badge/license-Apache--2.0-green.svg) ![Performance](https://img.shields.io/badge/performance-OXC--Inside-blueviolet.svg)
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
- * **šŸ” Secret Detection:** Scans for hardcoded secrets (API keys, tokens) using AST and Regex.
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
- yarn add -D pkg-scaffold
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 --run
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.2');
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.2'} Engine Activation`));
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",
4
- "description": "The Ultimate Enterprise Codebase Janitor. Faster than Knip with OXC integration, type-aware analysis, and self-healing capabilities. Fully standalone - solving what Knip cannot.",
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": "node bin/cli.js",
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
- "self-healing",
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",
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * ============================================================================
3
- * šŸ“¦ pkg-scaffold v3.3.0: Enterprise In-Memory Codebase State Manifest
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
- // Security & Compliance Anomaly Matrices
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
- const importKey = `${path.relative(path.dirname(parentPath), this.filePath).replace(/\\/g, '/')}:${symbolName}`;
68
- const importKeyAlt = `${path.relative(path.dirname(parentPath), this.filePath).replace(/\\/g, '/').replace(/\.(js|ts|tsx|jsx)$/, '')}:${symbolName}`;
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(importKey) || parentNode.importedSymbols.has(importKeyAlt)) {
73
+ if (parentNode.importedSymbols.has(absoluteImportKey) || parentNode.importedSymbols.has(absoluteStarKey)) {
71
74
  return true;
72
75
  }
73
76
 
74
- // Check for star imports or namespace imports
75
- const starKey = `${path.relative(path.dirname(parentPath), this.filePath).replace(/\\/g, '/')}:*`;
76
- const starKeyAlt = `${path.relative(path.dirname(parentPath), this.filePath).replace(/\\/g, '/').replace(/\.(js|ts|tsx|jsx)$/, '')}:*`;
77
- if (parentNode.importedSymbols.has(starKey) || parentNode.importedSymbols.has(starKeyAlt)) {
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
- if (!this.usedExternalPackages.has(dep)) {
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
  }
@@ -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
- filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS
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.isNamedImports(node.importClause.namedBindings)) {
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
- const propertyName = element.propertyName ? element.propertyName.text : importedName;
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
- if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
145
- const specifier = node.moduleSpecifier.text;
146
- if (node.exportClause && ts.isNamedExports(node.exportClause)) {
147
- node.exportClause.elements.forEach(element => {
148
- const name = element.name.text;
149
- const propertyName = element.propertyName ? element.propertyName.text : name;
150
- fileNode.internalExports.set(name, {
151
- type: 're-export',
152
- source: specifier,
153
- originalName: propertyName,
154
- start: node.getStart(sourceFile),
155
- end: node.getEnd()
156
- });
157
- });
158
- } else if (node.exportClause && ts.isNamespaceExport(node.exportClause)) {
159
- // export * as name from 'module'
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
- } else {
163
- // export * from 'module'
164
- fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
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 name = element.name.text;
169
- const propertyName = element.propertyName ? element.propertyName.text : name;
170
- fileNode.internalExports.set(name, {
171
- type: 'export',
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 && ts.isStringLiteral(arg)) {
331
- fileNode.explicitImports.add(arg.text);
332
- fileNode.dynamicImports.add(arg.text);
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
  }
@@ -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
- // If a resolution was found in a child barrel, return it
193
- if (continuousResolutionTrace && continuousResolutionTrace.originFile !== fullyResolvedPath) {
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
  }