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.
@@ -0,0 +1,73 @@
1
+ export class DeadCodeDetector {
2
+ constructor(context) {
3
+ this.context = context;
4
+ }
5
+
6
+ detectDeadCode(graph) {
7
+ const deadFiles = [];
8
+ const deadExports = [];
9
+
10
+ // Find all entry points
11
+ const entryPoints = new Set();
12
+ for (const [filePath, node] of graph.entries()) {
13
+ if (node.isEntry || node.isNextJsRoute || node.isSvelteComponent || node.isAstroPage) {
14
+ entryPoints.add(filePath);
15
+ }
16
+ }
17
+
18
+ // Traverse from entry points to find used files
19
+ const usedFiles = new Set();
20
+ const queue = Array.from(entryPoints);
21
+
22
+ while (queue.length > 0) {
23
+ const current = queue.shift();
24
+ if (!usedFiles.has(current)) {
25
+ usedFiles.add(current);
26
+ const node = graph.get(current);
27
+ if (node && node.outgoingEdges) {
28
+ for (const edge of node.outgoingEdges) {
29
+ queue.push(edge);
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ // Identify dead files
36
+ for (const filePath of graph.keys()) {
37
+ if (!usedFiles.has(filePath)) {
38
+ deadFiles.push(filePath);
39
+ }
40
+ }
41
+
42
+ // Identify dead exports in used files
43
+ for (const filePath of usedFiles) {
44
+ const node = graph.get(filePath);
45
+ if (!node) continue;
46
+
47
+ // If it's an entry point, we consider its exports used (unless strictly configured otherwise)
48
+ if (node.isEntry) continue;
49
+
50
+ for (const [exportName, exportInfo] of node.internalExports.entries()) {
51
+ if (exportName === '*' || exportName === 'default') continue; // Skip wildcards for now
52
+
53
+ let isUsed = false;
54
+ // Check incoming edges if they import this specific symbol
55
+ for (const [otherPath, otherNode] of graph.entries()) {
56
+ if (otherNode.outgoingEdges.has(filePath)) {
57
+ if (otherNode.importedSymbols.has(`${filePath}:${exportName}`) ||
58
+ otherNode.importedSymbols.has(`${filePath}:*`)) {
59
+ isUsed = true;
60
+ break;
61
+ }
62
+ }
63
+ }
64
+
65
+ if (!isUsed) {
66
+ deadExports.push({ file: filePath, symbol: exportName, line: exportInfo.start });
67
+ }
68
+ }
69
+ }
70
+
71
+ return { deadFiles, deadExports };
72
+ }
73
+ }
@@ -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,18 +69,109 @@ export class MagicDetector {
60
69
  }
61
70
 
62
71
  isCoreToolingSuiteElement(normalizedPath) {
63
- // Testing and execution matrices rules configuration keys
64
- if (/\.(test|spec|e2e|cy)\.(js|ts|tsx|jsx|stories\.tsx|stories\.ts)$/i.test(normalizedPath)) return true;
65
-
66
- // Testing tools and structural environment frameworks configuration keys
67
- const testEnvironments = [
68
- 'jest.config.', 'vitest.config.', 'playwright.config.', 'cypress.config.',
69
- 'webpack.config.', 'vite.config.', 'rollup.config.', 'tailwind.config.',
70
- '.eslintrc.', 'prettier.config.', '.postcssrc.', 'postcss.config.',
71
- 'bin/cli.js', 'index.js', 'WorkerTaskRunner.js'
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'
72
105
  ];
73
-
74
- return testEnvironments.some(matchPattern => normalizedPath.includes(matchPattern));
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;
75
175
  }
76
176
 
77
177
  /**
@@ -1,124 +1,295 @@
1
- let oxcParser;
2
- try {
3
- oxcParser = await import('oxc-parser');
4
- } catch (e) {
5
- // OXC is optional; will fall back to TypeScript in ASTAnalyzer
6
- }
7
-
8
- import fs from 'fs/promises';
9
-
10
- /**
11
- * High-Performance AST Analyzer using OXC (Rust-based)
12
- * Designed to outpace Knip v6 by utilizing the fastest parser in the JS ecosystem.
13
- * Includes automatic fallback if OXC is not available in the environment.
14
- */
15
1
  export class OxcAnalyzer {
16
2
  constructor(context) {
17
3
  this.context = context;
18
- this.isAvailable = !!oxcParser;
19
- }
20
-
21
- async processFile(filePath, fileNode) {
22
- if (!this.isAvailable) {
4
+ try {
5
+ this.oxc = require("oxc-parser");
6
+ this.isAvailable = true;
7
+ } catch (e) {
8
+ this.isAvailable = false;
23
9
  if (this.context.verbose) {
24
- console.warn(`[OXC] Library not available, skipping fast-scan for: ${filePath}`);
10
+ console.warn("[OxcAnalyzer] oxc-parser not found, falling back to TypeScript compiler API.");
25
11
  }
26
- return false;
27
12
  }
13
+ }
14
+
15
+ parseFile(filePath, content, fileNode) {
16
+ if (!this.isAvailable) return false;
28
17
 
29
18
  try {
30
- const sourceText = await fs.readFile(filePath, 'utf8');
31
-
32
- const { program } = oxcParser.parseSync(sourceText, {
19
+ if (this.context.verbose) {
20
+ console.log(`[OXC] Fast-parsing: ${filePath}`);
21
+ }
22
+
23
+ const ast = this.oxc.parseSync(content, {
24
+ sourceType: "module",
33
25
  sourceFilename: filePath,
34
- sourceType: filePath.endsWith('.ts') || filePath.endsWith('.tsx') ? 'typescript' : 'script'
26
+ ecmaVersion: "latest",
35
27
  });
36
28
 
37
- this.walkOxcNode(program, fileNode);
29
+ // Initialize new properties for JSX and Decorator analysis
30
+ fileNode.jsxComponents = new Set();
31
+ fileNode.jsxProps = new Set();
32
+ fileNode.decorators = new Set();
33
+
34
+ this.walkOxcAst(ast.program, fileNode, content);
38
35
  return true;
39
- } catch (err) {
36
+ } catch (e) {
40
37
  if (this.context.verbose) {
41
- console.error(`[OXC Error] Failed parsing: ${filePath}. Reason: ${err.message}`);
38
+ console.warn(`[OXC] Failed to parse ${filePath}: ${e.message}`);
42
39
  }
43
40
  return false;
44
41
  }
45
42
  }
46
43
 
47
- walkOxcNode(node, fileNode) {
44
+ walkOxcAst(node, fileNode, content) {
48
45
  if (!node) return;
49
46
 
50
- if (node.type === 'ImportDeclaration') {
51
- const specifier = node.source.value;
52
- fileNode.explicitImports.add(specifier);
53
- node.specifiers.forEach(s => {
54
- if (s.type === 'ImportSpecifier') {
55
- fileNode.importedSymbols.add(`${specifier}:${s.imported.name}`);
56
- } else if (s.type === 'ImportDefaultSpecifier') {
47
+ switch (node.type) {
48
+ case "ImportDeclaration":
49
+ this.handleImportDeclaration(node, fileNode);
50
+ break;
51
+ case "ExportNamedDeclaration":
52
+ case "ExportDefaultDeclaration":
53
+ case "ExportAllDeclaration":
54
+ this.handleExportDeclaration(node, fileNode, content);
55
+ break;
56
+ case "CallExpression":
57
+ this.handleCallExpression(node, fileNode);
58
+ break;
59
+ case "JSXElement":
60
+ case "JSXFragment": // Consider fragments as well
61
+ this.handleJsxElement(node, fileNode);
62
+ break;
63
+ case "Decorator":
64
+ this.handleDecorator(node, fileNode);
65
+ break;
66
+ }
67
+
68
+ // Traverse children
69
+ for (const key in node) {
70
+ if (node[key] && typeof node[key] === "object") {
71
+ if (Array.isArray(node[key])) {
72
+ node[key].forEach((child) => this.walkOxcAst(child, fileNode, content));
73
+ } else {
74
+ this.walkOxcAst(node[key], fileNode, content);
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ handleImportDeclaration(node, fileNode) {
81
+ const specifier = node.source.value;
82
+ fileNode.explicitImports.add(specifier);
83
+
84
+ // Track external package usage for dependency analysis
85
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
86
+ fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
87
+ }
88
+
89
+ if (node.specifiers) {
90
+ node.specifiers.forEach((spec) => {
91
+ if (spec.type === "ImportSpecifier") {
92
+ const importedName = spec.imported.name;
93
+ fileNode.importedSymbols.add(`${specifier}:${importedName}`);
94
+ } else if (spec.type === "ImportDefaultSpecifier") {
57
95
  fileNode.importedSymbols.add(`${specifier}:default`);
58
- } else if (s.type === 'ImportNamespaceSpecifier') {
96
+ } else if (spec.type === "ImportNamespaceSpecifier") {
59
97
  fileNode.importedSymbols.add(`${specifier}:*`);
60
98
  }
61
99
  });
62
100
  }
101
+ }
102
+
103
+ handleExportDeclaration(node, fileNode, content) {
104
+ if (node.type === "ExportDefaultDeclaration") {
105
+ fileNode.internalExports.set("default", { type: "default", start: node.start, end: node.end });
106
+ return;
107
+ }
108
+
109
+ if (node.type === "ExportAllDeclaration") {
110
+ const sourceSpecifier = node.source ? node.source.value : null;
111
+ if (sourceSpecifier) {
112
+ // FIX: Register re-export source as an explicit import so the graph linker
113
+ // creates an incomingEdge on the re-exported file.
114
+ fileNode.explicitImports.add(sourceSpecifier);
63
115
 
64
- if (node.type === 'ExportNamedDeclaration') {
65
- if (node.declaration) {
66
- this.handleOxcDeclaration(node.declaration, fileNode);
116
+ // Track external package usage from re-exports
117
+ if (!sourceSpecifier.startsWith('.') && !sourceSpecifier.startsWith('/')) {
118
+ fileNode.externalPackageUsage.add(this._extractPackageName(sourceSpecifier));
119
+ }
120
+
121
+ if (node.exported) {
122
+ // export * as name from 'module'
123
+ const name = node.exported.name || (node.exported.type === "Identifier" ? node.exported.name : null);
124
+ if (name) {
125
+ fileNode.internalExports.set(name, { type: "re-export-namespace", source: sourceSpecifier, originalName: "*", start: node.start, end: node.end });
126
+ fileNode.importedSymbols.add(`${sourceSpecifier}:*`);
127
+ }
128
+ } else {
129
+ // export * from 'module'
130
+ fileNode.internalExports.set("*", { type: "re-export-all", source: sourceSpecifier });
131
+ // FIX: Register as wildcard importedSymbol so graph linker creates incomingEdge
132
+ fileNode.importedSymbols.add(`${sourceSpecifier}:*`);
133
+ }
67
134
  }
135
+ return;
136
+ }
137
+
138
+ if (node.source) {
139
+ // Re-export with source: export { x } from 'module'
140
+ const specifier = node.source.value;
141
+ // FIX: Register re-export source as an explicit import
142
+ fileNode.explicitImports.add(specifier);
143
+
144
+ // Track external package usage from re-exports
145
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
146
+ fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
147
+ }
148
+
68
149
  if (node.specifiers) {
69
- node.specifiers.forEach(s => {
70
- fileNode.internalExports.set(s.exported.name, { type: 'export-specifier' });
150
+ node.specifiers.forEach((spec) => {
151
+ const exportedName = spec.exported.name;
152
+ const localName = spec.local.name;
153
+ fileNode.internalExports.set(exportedName, {
154
+ type: "re-export",
155
+ source: specifier,
156
+ originalName: localName,
157
+ start: node.start,
158
+ end: node.end,
159
+ });
160
+ // FIX: Register as importedSymbol so barrel-tracer can resolve origin file
161
+ fileNode.importedSymbols.add(`${specifier}:${localName}`);
71
162
  });
72
163
  }
164
+ } else if (node.declaration) {
165
+ // Direct export
166
+ const decl = node.declaration;
167
+ if (decl.type === "VariableDeclaration") {
168
+ decl.declarations.forEach((d) => {
169
+ if (d.id.type === "Identifier") {
170
+ fileNode.internalExports.set(d.id.name, { type: "variable", start: d.start, end: d.end });
171
+ } else if (d.id.type === "ObjectPattern") {
172
+ d.id.properties.forEach((p) => {
173
+ if (p.type === "Property" && p.value.type === "Identifier") {
174
+ fileNode.internalExports.set(p.value.name, { type: "variable", start: p.start, end: p.end });
175
+ }
176
+ });
177
+ } else if (d.id.type === "ArrayPattern") {
178
+ d.id.elements.forEach((e) => {
179
+ if (e && e.type === "Identifier") {
180
+ fileNode.internalExports.set(e.name, { type: "variable", start: e.start, end: e.end });
181
+ }
182
+ });
183
+ }
184
+ });
185
+ } else if (decl.id && decl.id.name) {
186
+ let type = "unknown";
187
+ if (decl.type === "FunctionDeclaration") type = "function";
188
+ else if (decl.type === "ClassDeclaration") type = "class";
189
+ else if (decl.type === "TSEnumDeclaration") type = "enum";
190
+ else if (decl.type === "TSInterfaceDeclaration") type = "interface";
191
+ else if (decl.type === "TSTypeAliasDeclaration") type = "type";
192
+ else if (decl.type === "TSModuleDeclaration") type = "namespace";
193
+
194
+ const exportInfo = { type, start: decl.start, end: decl.end };
195
+ fileNode.internalExports.set(decl.id.name, exportInfo);
196
+
197
+ if (decl.type === "TSEnumDeclaration") {
198
+ exportInfo.members = decl.members.map((m) => m.id.name || (m.id.type === "Identifier" ? m.id.name : ""));
199
+ } else if (decl.type === "TSInterfaceDeclaration" || decl.type === "ClassDeclaration") {
200
+ exportInfo.members = decl.body.body.filter((m) => m.key && m.key.name).map((m) => m.key.name);
201
+ }
202
+ }
203
+ } else if (node.specifiers) {
204
+ node.specifiers.forEach((spec) => {
205
+ const exportedName = spec.exported.name;
206
+ const localName = spec.local.name;
207
+ fileNode.internalExports.set(exportedName, {
208
+ type: "export",
209
+ originalName: localName,
210
+ start: node.start,
211
+ end: node.end,
212
+ });
213
+ });
73
214
  }
215
+ }
74
216
 
75
- if (node.type === 'ExportDefaultDeclaration') {
76
- fileNode.internalExports.set('default', { type: 'default-export' });
217
+ handleCallExpression(node, fileNode) {
218
+ // Dynamic import(): import('./module')
219
+ if (node.callee.type === "Import" && node.arguments.length > 0) {
220
+ const arg = node.arguments[0];
221
+ if (arg.type === "StringLiteral") {
222
+ const specifier = arg.value;
223
+ fileNode.explicitImports.add(specifier);
224
+ fileNode.dynamicImports.add(specifier);
225
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
226
+ fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
227
+ }
228
+ } else {
229
+ // Non-literal dynamic import: record as calculated
230
+ if (fileNode.calculatedDynamicImports) {
231
+ fileNode.calculatedDynamicImports.push({ kind: arg.type, start: arg.start });
232
+ }
233
+ }
234
+ } else if (node.callee.type === "Identifier" && node.callee.name === "require" && node.arguments.length > 0 && node.arguments[0].type === "StringLiteral") {
235
+ const specifier = node.arguments[0].value;
236
+ fileNode.explicitImports.add(specifier);
237
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
238
+ fileNode.externalPackageUsage.add(this._extractPackageName(specifier));
239
+ }
77
240
  }
241
+ }
242
+
243
+ handleJsxElement(node, fileNode) {
244
+ const getElementName = (nameNode) => {
245
+ if (nameNode.type === "JSXIdentifier") return nameNode.name;
246
+ if (nameNode.type === "JSXMemberExpression") return `${getElementName(nameNode.object)}.${nameNode.property.name}`;
247
+ return "unknown";
248
+ };
78
249
 
79
- if (node.type === 'TSModuleDeclaration' && node.id.type === 'Identifier') {
80
- const namespaceName = node.id.name;
81
- if (node.body && node.body.type === 'TSModuleBlock') {
82
- node.body.body.forEach(innerNode => {
83
- if (innerNode.type === 'ExportNamedDeclaration' && innerNode.declaration) {
84
- const decl = innerNode.declaration;
85
- let memberName;
86
- if (decl.type === 'VariableDeclaration') {
87
- memberName = decl.declarations[0].id.name;
88
- } else if (decl.id) {
89
- memberName = decl.id.name;
90
- }
91
- if (memberName) {
92
- fileNode.namespaceMembers = fileNode.namespaceMembers || new Set();
93
- fileNode.namespaceMembers.add(`${namespaceName}.${memberName}`);
94
- }
250
+ if (node.openingElement) {
251
+ const tagName = getElementName(node.openingElement.name);
252
+ fileNode.jsxComponents.add(tagName);
253
+
254
+ if (node.openingElement.attributes) {
255
+ node.openingElement.attributes.forEach(attr => {
256
+ if (attr.type === "JSXAttribute" && attr.name.type === "JSXIdentifier") {
257
+ fileNode.jsxProps.add(`${tagName}:${attr.name.name}`);
95
258
  }
96
259
  });
97
260
  }
98
261
  }
262
+ }
99
263
 
100
- for (const key in node) {
101
- const child = node[key];
102
- if (child && typeof child === 'object') {
103
- if (Array.isArray(child)) {
104
- child.forEach(c => this.walkOxcNode(c, fileNode));
105
- } else {
106
- this.walkOxcNode(child, fileNode);
107
- }
108
- }
264
+ handleDecorator(node, fileNode) {
265
+ const getDecoratorName = (expr) => {
266
+ if (expr.type === "Identifier") return expr.name;
267
+ if (expr.type === "CallExpression" && expr.callee.type === "Identifier") return expr.callee.name;
268
+ if (expr.type === "CallExpression" && expr.callee.type === "MemberExpression" && expr.callee.property.type === "Identifier") return expr.callee.property.name;
269
+ return "unknown";
270
+ };
271
+
272
+ const decoratorName = getDecoratorName(node.expression);
273
+ fileNode.decorators.add(decoratorName);
274
+
275
+ // Optionally, extract decorator arguments
276
+ if (node.expression.type === "CallExpression") {
277
+ node.expression.arguments.forEach(arg => {
278
+ // Further analysis of arguments can be done here if needed
279
+ // e.g., if (arg.type === "StringLiteral") fileNode.decoratorArgs.add(`${decoratorName}:${arg.value}`);
280
+ });
109
281
  }
110
282
  }
111
283
 
112
- handleOxcDeclaration(decl, fileNode) {
113
- let name;
114
- if (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration' || decl.type === 'TSEnumDeclaration' || decl.type === 'TSInterfaceDeclaration' || decl.type === 'TSTypeAliasDeclaration') {
115
- name = decl.id?.name;
116
- } else if (decl.type === 'VariableDeclaration') {
117
- name = decl.declarations[0].id.name;
118
- }
119
-
120
- if (name) {
121
- fileNode.internalExports.set(name, { type: decl.type.toLowerCase() });
284
+ /**
285
+ * Extracts the root npm package name from an import specifier.
286
+ * Handles scoped packages (@scope/pkg) and subpath imports (pkg/utils, @scope/pkg/utils).
287
+ */
288
+ _extractPackageName(specifier) {
289
+ if (specifier.startsWith('@')) {
290
+ const parts = specifier.split('/');
291
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
122
292
  }
293
+ return specifier.split('/')[0];
123
294
  }
124
295
  }