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.
- package/README.md +62 -22
- package/bin/cli.js +7 -7
- package/package.json +5 -4
- package/pnpm-workspace.yaml +2 -0
- package/src/EngineContext.js +47 -19
- package/src/ast/ASTAnalyzer.js +287 -132
- package/src/ast/BarrelParser.js +51 -19
- package/src/ast/DeadCodeDetector.js +73 -0
- package/src/ast/MagicDetector.js +111 -11
- package/src/ast/OxcAnalyzer.js +250 -79
- package/src/healing/GitSandbox.js +44 -122
- package/src/healing/SelfHealer.js +29 -130
- package/src/index.js +124 -108
- package/src/performance/WorkerTaskRunner.js +17 -5
- package/src/plugins/PluginRegistry.js +28 -1
- package/src/plugins/ecosystems/MorePlugins.js +184 -0
- package/src/plugins/ecosystems/PluginLoader.js +20 -0
- package/src/resolution/CircularDetector.js +3 -34
- package/src/resolution/ConfigLoader.js +34 -6
- package/src/resolution/DependencyProfiler.js +261 -9
- package/src/resolution/WorkSpaceGraph.js +148 -35
- package/src/performance/SecretDetector.js +0 -378
|
@@ -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
|
+
}
|
package/src/ast/MagicDetector.js
CHANGED
|
@@ -6,6 +6,15 @@ import { PluginRegistry } from '../plugins/PluginRegistry.js';
|
|
|
6
6
|
* Ecosystem Entry Point Manifest & Dynamic Framework Router Heuristic Validator
|
|
7
7
|
* Intercepts implicit conventions to handle cases where direct import statements are absent.
|
|
8
8
|
* Now refactored to use a pluggable architecture.
|
|
9
|
+
*
|
|
10
|
+
* Improvements over v1:
|
|
11
|
+
* - Extended config-file detection list (Biome, Oxlint, tsup, unbuild, etc.)
|
|
12
|
+
* - Next.js App Router conventions (page.tsx, layout.tsx, loading.tsx, error.tsx, etc.)
|
|
13
|
+
* - Remix conventions (route files under app/routes/)
|
|
14
|
+
* - SvelteKit conventions (+page.svelte, +layout.svelte, etc.)
|
|
15
|
+
* - Astro page/layout conventions
|
|
16
|
+
* - Common entry-point patterns (bin/, cli.ts, server.ts, main.ts, app.ts)
|
|
17
|
+
* - Test file patterns extended to cover Vitest workspace files
|
|
9
18
|
*/
|
|
10
19
|
export class MagicDetector {
|
|
11
20
|
constructor(context) {
|
|
@@ -60,18 +69,109 @@ export class MagicDetector {
|
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
isCoreToolingSuiteElement(normalizedPath) {
|
|
63
|
-
//
|
|
64
|
-
if (/\.(test|spec|e2e|cy)\.(js|ts|tsx|jsx
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
'.
|
|
71
|
-
'
|
|
72
|
+
// ── 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
|
-
|
|
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
|
/**
|
package/src/ast/OxcAnalyzer.js
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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(
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
26
|
+
ecmaVersion: "latest",
|
|
35
27
|
});
|
|
36
28
|
|
|
37
|
-
|
|
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 (
|
|
36
|
+
} catch (e) {
|
|
40
37
|
if (this.context.verbose) {
|
|
41
|
-
console.
|
|
38
|
+
console.warn(`[OXC] Failed to parse ${filePath}: ${e.message}`);
|
|
42
39
|
}
|
|
43
40
|
return false;
|
|
44
41
|
}
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
walkOxcAst(node, fileNode, content) {
|
|
48
45
|
if (!node) return;
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 (
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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(
|
|
70
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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.
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
}
|