pkg-scaffold 3.3.3 → 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.
@@ -1,175 +1,203 @@
1
1
  import ts from 'typescript';
2
- import fs from 'fs/promises';
3
- import path from 'path';
4
-
5
- /**
6
- * Enterprise AST Syntax Walker & Feature Extractor
7
- * Upgraded to use full TypeScript Compiler API (ts.createProgram + TypeChecker)
8
- * for type-aware cross-file analysis.
9
- */
10
- import { OxcAnalyzer } from './OxcAnalyzer.js';
11
2
 
12
3
  export class ASTAnalyzer {
13
4
  constructor(context) {
14
5
  this.context = context;
15
- this.program = null;
16
- this.checker = null;
17
- this.oxc = new OxcAnalyzer(context);
6
+ this.scopeStack = [];
18
7
  }
19
8
 
20
9
  /**
21
- * Initializes the TypeScript program for the entire project.
22
- * This is crucial for cross-file type resolution.
10
+ * Returns the TypeScript ScriptKind for a given file path.
11
+ * Exposed as a public method so WorkerTaskRunner can call it directly.
23
12
  */
24
- initProgram(filePaths, options = {}) {
25
- const defaultOptions = {
26
- target: ts.ScriptTarget.Latest,
27
- module: ts.ModuleKind.CommonJS,
28
- allowJs: true,
29
- checkJs: true,
30
- esModuleInterop: true,
31
- skipLibCheck: true
32
- };
33
-
34
- this.program = ts.createProgram(filePaths, { ...defaultOptions, ...options });
35
- this.checker = this.program.getTypeChecker();
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
+
19
+ parseFile(filePath, content, fileNode) {
20
+ if (this.context.verbose) {
21
+ console.log(`[AST] Parsing: ${filePath}`);
22
+ }
23
+
24
+ const sourceFile = ts.createSourceFile(
25
+ filePath,
26
+ content,
27
+ ts.ScriptTarget.Latest,
28
+ true,
29
+ this.getScriptKind(filePath)
30
+ );
31
+
32
+ this.currentScope = { symbols: new Map(), parent: null };
33
+ this.scopeStack.push(this.currentScope);
34
+
35
+ this.extractTopLevelJSDocSuppreessions(sourceFile, fileNode);
36
+ this.walkAST(sourceFile, fileNode, sourceFile);
37
+
38
+ this.scopeStack.pop(); // Pop the global scope
39
+ this.currentScope = null;
36
40
  }
37
41
 
38
42
  /**
39
- * Processes a file using the initialized program and type checker.
43
+ * Alias for walkAST used by WorkerTaskRunner (legacy API compatibility).
40
44
  */
41
- async processFile(filePath, fileNode) {
42
- // Fast Path: Use OXC for rapid scanning if type checking is not strictly required for this file
43
- if (this.context.fastMode && this.oxc.isAvailable) {
44
- const success = await this.oxc.processFile(filePath, fileNode);
45
- if (success) return true;
45
+ walkNode(node, sourceFile, fileNode) {
46
+ return this.walkAST(node, fileNode, sourceFile);
47
+ }
48
+
49
+ pushScope() {
50
+ const newScope = { symbols: new Map(), parent: this.currentScope };
51
+ this.scopeStack.push(newScope);
52
+ this.currentScope = newScope;
53
+ }
54
+
55
+ popScope() {
56
+ if (this.scopeStack.length > 1) {
57
+ this.scopeStack.pop();
58
+ this.currentScope = this.scopeStack[this.scopeStack.length - 1];
46
59
  }
60
+ }
47
61
 
48
- if (!this.program) {
49
- throw new Error('ASTAnalyzer must be initialized with initProgram() before processing files.');
62
+ addDeclaredSymbol(name, node, sourceFile) {
63
+ if (this.currentScope) {
64
+ const loc = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
65
+ this.currentScope.symbols.set(name, { node, line: loc.line + 1, column: loc.character + 1 });
50
66
  }
67
+ }
51
68
 
52
- const sourceFile = this.program.getSourceFile(filePath);
53
- if (!sourceFile) {
54
- if (this.context.verbose) {
55
- console.error(`[AST Error] Source file not found in program: ${filePath}`);
69
+ resolveSymbol(name) {
70
+ for (let i = this.scopeStack.length - 1; i >= 0; i--) {
71
+ const scope = this.scopeStack[i];
72
+ if (scope.symbols.has(name)) {
73
+ return scope.symbols.get(name);
56
74
  }
57
- return false;
58
75
  }
59
-
60
- this.extractTopLevelJSDocSuppreessions(sourceFile, fileNode);
61
- this.walkNode(sourceFile, sourceFile, fileNode);
62
-
63
- return true;
76
+ return null;
64
77
  }
65
78
 
66
- walkNode(sourceFile, node, fileNode) {
67
- if (!node) return;
68
-
69
- // Use type checker to resolve symbols if needed
70
- if (ts.isIdentifier(node) && !this.isNodeDeclarationName(node)) {
71
- const symbol = this.checker.getSymbolAtLocation(node);
72
- if (symbol) {
73
- const declarations = symbol.getDeclarations();
74
- if (declarations && declarations.length > 0) {
75
- const declFile = declarations[0].getSourceFile().fileName;
76
- const symbolName = symbol.getName();
77
-
78
- // Track sub-symbol usage (Property Access)
79
- if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
80
- const parentType = this.checker.getTypeAtLocation(node.parent.expression);
81
- const parentSymbol = parentType.getSymbol() || parentType.aliasSymbol;
82
- if (parentSymbol) {
83
- const parentDecl = parentSymbol.getDeclarations()?.[0];
84
- if (parentDecl) {
85
- const parentFile = parentDecl.getSourceFile().fileName;
86
- fileNode.memberUsage = fileNode.memberUsage || new Set();
87
- fileNode.memberUsage.add(`${parentFile}:${parentSymbol.getName()}.${symbolName}`);
88
- }
89
- }
90
- }
91
-
92
- if (declFile !== sourceFile.fileName) {
93
- fileNode.resolvedReferences = fileNode.resolvedReferences || new Set();
94
- fileNode.resolvedReferences.add(`${declFile}:${symbolName}`);
95
- }
96
- }
97
- }
79
+ walkAST(node, fileNode, sourceFile) {
80
+ // Handle scope entry for blocks, functions, classes, etc.
81
+ const isScopeNode = ts.isBlock(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isModuleDeclaration(node);
82
+ if (isScopeNode) {
83
+ this.pushScope();
98
84
  }
99
85
 
100
86
  switch (node.kind) {
101
- case ts.SyntaxKind.ImportDeclaration: {
102
- this.handleImportDeclaration(node, fileNode);
87
+ case ts.SyntaxKind.ImportDeclaration:
88
+ this.handleImportDeclaration(node, fileNode, sourceFile);
103
89
  break;
104
- }
105
- case ts.SyntaxKind.ExportDeclaration: {
90
+ case ts.SyntaxKind.ExportDeclaration:
106
91
  this.handleExportDeclaration(node, fileNode, sourceFile);
107
92
  break;
108
- }
93
+ case ts.SyntaxKind.ExportAssignment:
94
+ fileNode.internalExports.set('default', { type: 'default', start: node.getStart(sourceFile), end: node.getEnd() });
95
+ break;
96
+ case ts.SyntaxKind.VariableStatement:
97
+ this.handleVariableStatement(node, fileNode, sourceFile);
98
+ break;
109
99
  case ts.SyntaxKind.FunctionDeclaration:
110
100
  case ts.SyntaxKind.ClassDeclaration:
111
101
  case ts.SyntaxKind.InterfaceDeclaration:
112
102
  case ts.SyntaxKind.TypeAliasDeclaration:
113
103
  case ts.SyntaxKind.EnumDeclaration:
114
- case ts.SyntaxKind.ModuleDeclaration: {
104
+ case ts.SyntaxKind.ModuleDeclaration:
115
105
  this.handleNamedDeclaration(node, fileNode, sourceFile);
116
106
  break;
117
- }
118
- case ts.SyntaxKind.VariableStatement: {
119
- this.handleVariableStatement(node, fileNode, sourceFile);
107
+ case ts.SyntaxKind.Identifier:
108
+ fileNode.instantiatedIdentifiers.add(node.text);
120
109
  break;
121
- }
122
- case ts.SyntaxKind.CallExpression: {
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;
117
+ case ts.SyntaxKind.CallExpression:
123
118
  this.handleCallExpression(node, fileNode, sourceFile);
124
119
  break;
125
- }
120
+ case ts.SyntaxKind.JsxElement:
121
+ case ts.SyntaxKind.JsxSelfClosingElement:
122
+ this.handleJsxElement(node, fileNode, sourceFile);
123
+ break;
124
+ case ts.SyntaxKind.Decorator:
125
+ this.handleDecorator(node, fileNode, sourceFile);
126
+ break;
126
127
  }
127
128
 
128
- ts.forEachChild(node, child => this.walkNode(sourceFile, child, fileNode));
129
+ ts.forEachChild(node, child => this.walkAST(child, fileNode, sourceFile));
130
+
131
+ if (isScopeNode) {
132
+ this.popScope();
133
+ }
129
134
  }
130
135
 
131
- handleImportDeclaration(node, fileNode) {
136
+ handleImportDeclaration(node, fileNode, sourceFile) {
132
137
  if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
133
138
  const specifier = node.moduleSpecifier.text;
134
139
  fileNode.explicitImports.add(specifier);
135
140
 
141
+ // Track external package usage for dependency analysis
142
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
143
+ fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
144
+ }
145
+
136
146
  if (node.importClause) {
147
+ if (node.importClause.name) {
148
+ fileNode.importedSymbols.add(`${specifier}:default`);
149
+ }
137
150
  if (node.importClause.namedBindings) {
138
- 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)) {
139
154
  node.importClause.namedBindings.elements.forEach(element => {
140
- const importedName = element.name.text;
141
- const propertyName = element.propertyName ? element.propertyName.text : importedName;
142
- fileNode.importedSymbols.add(`${specifier}:${propertyName}`);
155
+ const importedName = element.propertyName ? element.propertyName.text : element.name.text;
156
+ fileNode.importedSymbols.add(`${specifier}:${importedName}`);
143
157
  });
144
- } else if (ts.isNamespaceImport(node.importClause.namedBindings)) {
145
- fileNode.importedSymbols.add(`${specifier}:*`);
146
158
  }
147
159
  }
148
- if (node.importClause.name) {
149
- fileNode.importedSymbols.add(`${specifier}:default`);
150
- }
151
160
  }
152
161
  }
153
162
  }
154
163
 
155
164
  handleExportDeclaration(node, fileNode, sourceFile) {
156
- // Handle re-exports: export { x } from './y'
157
- if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
158
- const specifier = node.moduleSpecifier.text;
159
- if (node.exportClause && ts.isNamedExports(node.exportClause)) {
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'
182
+ const name = node.exportClause.name.text;
183
+ fileNode.internalExports.set(name, { type: 're-export-namespace', source: specifier, originalName: '*', start: node.getStart(sourceFile), end: node.getEnd() });
184
+ fileNode.importedSymbols.add(`${specifier}:*`);
185
+ } else if (ts.isNamedExports(node.exportClause)) {
186
+ // export { x, y as z } from './module'
160
187
  node.exportClause.elements.forEach(element => {
161
- const name = element.name.text;
162
- fileNode.internalExports.set(name, {
163
- type: 're-export',
164
- source: specifier,
165
- start: node.getStart(sourceFile),
166
- end: node.getEnd()
167
- });
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}`);
168
192
  });
169
- } else {
170
- // export * from './y'
171
- fileNode.internalExports.set('*', { type: 're-export-all', source: specifier });
172
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
+ });
173
201
  }
174
202
  }
175
203
 
@@ -183,35 +211,56 @@ export class ASTAnalyzer {
183
211
  start: node.getStart(sourceFile),
184
212
  end: node.getEnd()
185
213
  };
186
-
187
- fileNode.internalExports.set(name, exportInfo);
188
214
 
189
- // Phase 4: Drill down into members
215
+ fileNode.internalExports.set(name, exportInfo);
216
+
190
217
  if (ts.isEnumDeclaration(node)) {
191
- exportInfo.members = node.members.map(m => m.name.getText(sourceFile));
218
+ exportInfo.members = node.members.map(m => ({
219
+ name: m.name.getText(sourceFile),
220
+ type: 'enumMember',
221
+ start: m.getStart(sourceFile),
222
+ end: m.getEnd()
223
+ }));
192
224
  } else if (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) {
193
225
  exportInfo.members = node.members
194
226
  .filter(m => m.name)
195
- .map(m => m.name.getText(sourceFile));
227
+ .map(m => ({
228
+ name: m.name.getText(sourceFile),
229
+ type: ts.SyntaxKind[m.kind].toLowerCase(),
230
+ start: m.getStart(sourceFile),
231
+ end: m.getEnd()
232
+ }));
196
233
  } else if (ts.isModuleDeclaration(node)) {
197
- // Handle Namespaces
198
234
  const members = [];
199
235
  if (node.body && ts.isModuleBlock(node.body)) {
200
236
  node.body.statements.forEach(stmt => {
201
237
  if (this.hasExportModifier(stmt) && (ts.isVariableStatement(stmt) || ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt))) {
202
238
  if (ts.isVariableStatement(stmt)) {
203
- stmt.declarationList.declarations.forEach(d => members.push(d.name.getText(sourceFile)));
239
+ stmt.declarationList.declarations.forEach(d => members.push({
240
+ name: d.name.getText(sourceFile),
241
+ type: 'variable',
242
+ start: d.getStart(sourceFile),
243
+ end: d.getEnd()
244
+ }));
204
245
  } else if (stmt.name) {
205
- members.push(stmt.name.getText(sourceFile));
246
+ members.push({
247
+ name: stmt.name.getText(sourceFile),
248
+ type: ts.SyntaxKind[stmt.kind].toLowerCase().replace('declaration', ''),
249
+ start: stmt.getStart(sourceFile),
250
+ end: stmt.getEnd()
251
+ });
206
252
  }
207
253
  }
208
254
  });
209
255
  }
210
256
  exportInfo.members = members;
211
257
  }
212
-
258
+
213
259
  const loc = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
214
260
  fileNode.symbolSourceLocations.set(name, { line: loc.line + 1, column: loc.character + 1 });
261
+ this.addDeclaredSymbol(name, node, sourceFile);
262
+ } else if (node.name && ts.isIdentifier(node.name)) {
263
+ this.addDeclaredSymbol(node.name.text, node, sourceFile);
215
264
  }
216
265
  }
217
266
 
@@ -225,32 +274,124 @@ export class ASTAnalyzer {
225
274
  start: decl.getStart(sourceFile),
226
275
  end: decl.getEnd()
227
276
  });
277
+ this.addDeclaredSymbol(name, decl, sourceFile);
278
+ } else if (decl.name && ts.isObjectBindingPattern(decl.name)) {
279
+ decl.name.elements.forEach(element => {
280
+ if(element.name && ts.isIdentifier(element.name)) {
281
+ const name = element.name.text;
282
+ fileNode.internalExports.set(name, {
283
+ type: 'variable',
284
+ start: element.getStart(sourceFile),
285
+ end: element.getEnd()
286
+ });
287
+ this.addDeclaredSymbol(name, element, sourceFile);
288
+ }
289
+ });
290
+ } else if (decl.name && ts.isArrayBindingPattern(decl.name)) {
291
+ decl.name.elements.forEach(element => {
292
+ if(ts.isBindingElement(element) && element.name && ts.isIdentifier(element.name)) {
293
+ const name = element.name.text;
294
+ fileNode.internalExports.set(name, {
295
+ type: 'variable',
296
+ start: element.getStart(sourceFile),
297
+ end: element.getEnd()
298
+ });
299
+ this.addDeclaredSymbol(name, element, sourceFile);
300
+ }
301
+ });
302
+ }
303
+ });
304
+ } else {
305
+ // Non-exported variable declarations also need to be added to scope
306
+ node.declarationList.declarations.forEach(decl => {
307
+ if (decl.name && ts.isIdentifier(decl.name)) {
308
+ this.addDeclaredSymbol(decl.name.text, decl, sourceFile);
309
+ } else if (decl.name && ts.isObjectBindingPattern(decl.name)) {
310
+ decl.name.elements.forEach(element => {
311
+ if (element.name && ts.isIdentifier(element.name)) {
312
+ this.addDeclaredSymbol(element.name.text, element, sourceFile);
313
+ }
314
+ });
315
+ } else if (decl.name && ts.isArrayBindingPattern(decl.name)) {
316
+ decl.name.elements.forEach(element => {
317
+ if (ts.isBindingElement(element) && element.name && ts.isIdentifier(element.name)) {
318
+ this.addDeclaredSymbol(element.name.text, element, sourceFile);
319
+ }
320
+ });
228
321
  }
229
322
  });
230
323
  }
231
324
  }
232
325
 
233
326
  handleCallExpression(node, fileNode, sourceFile) {
234
- // Trace dynamic imports
327
+ // Dynamic import(): import('./module').then(...)
235
328
  if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
329
+ const arg = node.arguments[0];
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
+ }
344
+ }
345
+ } else if (ts.isIdentifier(node.expression) && node.expression.text === 'require') {
236
346
  const arg = node.arguments[0];
237
347
  if (arg && ts.isStringLiteral(arg)) {
238
348
  fileNode.explicitImports.add(arg.text);
239
- fileNode.dynamicImports.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
+ }
240
353
  }
241
354
  }
242
355
  }
243
356
 
244
- hasExportModifier(node) {
245
- return node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
357
+ handleJsxElement(node, fileNode, sourceFile) {
358
+ const getElementName = (name) => {
359
+ if (ts.isIdentifier(name)) return name.text;
360
+ if (ts.isPropertyAccessExpression(name)) return name.name.text;
361
+ return 'unknown';
362
+ };
363
+
364
+ const tagName = getElementName(node.openingElement.tagName);
365
+ fileNode.jsxComponents.add(tagName);
366
+
367
+ node.openingElement.attributes.properties.forEach(attr => {
368
+ if (ts.isJsxAttribute(attr) && ts.isIdentifier(attr.name)) {
369
+ fileNode.jsxProps.add(`${tagName}:${attr.name.text}`);
370
+ }
371
+ });
372
+ }
373
+
374
+ handleDecorator(node, fileNode, sourceFile) {
375
+ const getDecoratorName = (expr) => {
376
+ if (ts.isIdentifier(expr)) return expr.text;
377
+ if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) return expr.expression.text;
378
+ if (ts.isCallExpression(expr) && ts.isPropertyAccessExpression(expr.expression)) return expr.expression.name.text;
379
+ return 'unknown';
380
+ };
381
+
382
+ const decoratorName = getDecoratorName(node.expression);
383
+ fileNode.decorators.add(decoratorName);
384
+
385
+ // Optionally, extract decorator arguments
386
+ if (ts.isCallExpression(node.expression)) {
387
+ node.expression.arguments.forEach(arg => {
388
+ // Further analysis of arguments can be done here if needed
389
+ });
390
+ }
246
391
  }
247
392
 
248
- isNodeDeclarationName(node) {
249
- const parent = node.parent;
250
- if (!parent) return false;
251
- return (ts.isVariableDeclaration(parent) || ts.isFunctionDeclaration(parent) ||
252
- ts.isClassDeclaration(parent) || ts.isInterfaceDeclaration(parent) ||
253
- ts.isEnumDeclaration(parent) || ts.isModuleDeclaration(parent)) && parent.name === node;
393
+ hasExportModifier(node) {
394
+ return node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
254
395
  }
255
396
 
256
397
  extractTopLevelJSDocSuppreessions(sourceFile, fileNode) {
@@ -264,4 +405,18 @@ export class ASTAnalyzer {
264
405
  }
265
406
  }
266
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
+ }
267
422
  }
@@ -99,6 +99,18 @@ export class BarrelParser {
99
99
  node.declarationList.declarations.forEach(decl => {
100
100
  if (ts.isIdentifier(decl.name)) {
101
101
  spec.declaredLocalExports.add(decl.name.text);
102
+ } else if (ts.isObjectBindingPattern(decl.name)) {
103
+ decl.name.elements.forEach(element => {
104
+ if (element.name && ts.isIdentifier(element.name)) {
105
+ spec.declaredLocalExports.add(element.name.text);
106
+ }
107
+ });
108
+ } else if (ts.isArrayBindingPattern(decl.name)) {
109
+ decl.name.elements.forEach(element => {
110
+ if (ts.isBindingElement(element) && element.name && ts.isIdentifier(element.name)) {
111
+ spec.declaredLocalExports.add(element.name.text);
112
+ }
113
+ });
102
114
  }
103
115
  });
104
116
  }
@@ -148,37 +160,57 @@ export class BarrelParser {
148
160
  if (spec.forwardedNamedExports.has(targetSymbolName)) {
149
161
  const routingRule = spec.forwardedNamedExports.get(targetSymbolName);
150
162
  const fullyResolvedPath = this.resolver.resolveModulePath(contextFilePath, routingRule.targetModule);
151
- return this.determineSymbolDeclarationOrigin(fullyResolvedPath, routingRule.sourceSymbol, activeProjectGraph, protectionStack);
163
+ if (fullyResolvedPath) {
164
+ return this.determineSymbolDeclarationOrigin(fullyResolvedPath, routingRule.sourceSymbol, activeProjectGraph, protectionStack);
165
+ }
152
166
  }
153
167
 
154
- // Rule C: Evaluate structural namespace alias groupings
155
- if (spec.namespacedWildcardExports.has(targetSymbolName)) {
156
- const relativeModule = spec.namespacedWildcardExports.get(targetSymbolName);
157
- const fullyResolvedPath = this.resolver.resolveModulePath(contextFilePath, relativeModule);
158
- return { originFile: fullyResolvedPath, originSymbol: '*' };
168
+ // Rule C: Evaluate structural namespace alias groupings (export * as name from 'module')
169
+ for (const [namespaceAlias, relativeModule] of spec.namespacedWildcardExports.entries()) {
170
+ // If the targetSymbolName is prefixed with the namespaceAlias, then it's a member of this namespace re-export
171
+ if (targetSymbolName.startsWith(`${namespaceAlias}.`)) {
172
+ const originalSymbol = targetSymbolName.substring(namespaceAlias.length + 1);
173
+ const fullyResolvedPath = this.resolver.resolveModulePath(contextFilePath, relativeModule);
174
+ if (fullyResolvedPath) {
175
+ return this.determineSymbolDeclarationOrigin(fullyResolvedPath, originalSymbol, activeProjectGraph, protectionStack);
176
+ }
177
+ }
159
178
  }
160
179
 
161
- // Rule D: Sweep through anonymous star re-exports vectors
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.
162
190
  for (const relativePath of spec.wildcardExports) {
163
191
  const fullyResolvedPath = this.resolver.resolveModulePath(contextFilePath, relativePath);
164
192
 
165
- // Look inside the graph metadata map configuration to verify child target availability
166
- const targetSubSpec = await this.parseBarrelSpecification(fullyResolvedPath);
167
-
168
- if (targetSubSpec.declaredLocalExports.has(targetSymbolName) ||
169
- targetSubSpec.forwardedNamedExports.has(targetSymbolName)) {
170
- return this.determineSymbolDeclarationOrigin(fullyResolvedPath, targetSymbolName, activeProjectGraph, protectionStack);
171
- }
172
-
173
- // Dynamic recursive trace for multi-tier nested barrel file layouts
174
- if (targetSubSpec.isBarrelInstance) {
193
+ if (fullyResolvedPath) {
175
194
  const continuousResolutionTrace = await this.determineSymbolDeclarationOrigin(
176
195
  fullyResolvedPath,
177
196
  targetSymbolName,
178
197
  activeProjectGraph,
179
- protectionStack
198
+ new Set(protectionStack) // Use a copy so sibling branches don't block each other
180
199
  );
181
- 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) {
182
214
  return continuousResolutionTrace;
183
215
  }
184
216
  }