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
|
@@ -1,31 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ============================================================================
|
|
3
|
-
* Circular Dependency Detector for pkg-scaffold v3.3.0
|
|
4
|
-
*
|
|
5
|
-
* Copyright (C) 2026 DreamLongYT
|
|
6
|
-
* Licensed under the Apache License, Version 2.0.
|
|
7
|
-
* "The Original Code was made by DreamLongYT"
|
|
8
|
-
* ============================================================================
|
|
9
|
-
* Implements a high-performance Tarjan-based algorithm to
|
|
10
|
-
* detect circular dependencies in the codebase graph.
|
|
11
|
-
* Addresses Knip Issue #1734.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
1
|
export class CircularDetector {
|
|
15
2
|
constructor(context) {
|
|
16
3
|
this.context = context;
|
|
17
4
|
this.cycles = [];
|
|
18
5
|
}
|
|
19
6
|
|
|
20
|
-
/**
|
|
21
|
-
* Detects cycles in the provided dependency graph using Tarjan's SCC algorithm
|
|
22
|
-
* @param {Map} graph - The codebase dependency graph
|
|
23
|
-
* @returns {Array} List of detected cycles
|
|
24
|
-
*/
|
|
25
7
|
detectCycles(graph, context = null) {
|
|
26
8
|
if (context) this.context = context;
|
|
27
9
|
this.cwd = context?.cwd || this.context?.cwd || process.cwd();
|
|
28
10
|
this.cycles = [];
|
|
11
|
+
|
|
29
12
|
let index = 0;
|
|
30
13
|
const stack = [];
|
|
31
14
|
const indices = new Map();
|
|
@@ -63,7 +46,6 @@ export class CircularDetector {
|
|
|
63
46
|
if (component.length > 1) {
|
|
64
47
|
this.cycles.push(component.reverse());
|
|
65
48
|
} else {
|
|
66
|
-
// Check for self-loops
|
|
67
49
|
const node = graph.get(v);
|
|
68
50
|
if (node && node.outgoingEdges && node.outgoingEdges.has(v)) {
|
|
69
51
|
this.cycles.push([v]);
|
|
@@ -81,18 +63,11 @@ export class CircularDetector {
|
|
|
81
63
|
return this.cycles;
|
|
82
64
|
}
|
|
83
65
|
|
|
84
|
-
/**
|
|
85
|
-
* Formats cycles for reporting with file paths
|
|
86
|
-
*/
|
|
87
66
|
formatCycles() {
|
|
88
67
|
return this.cycles.map(cycle => {
|
|
89
68
|
const paths = cycle.map(p => {
|
|
90
|
-
// Extract relative path for readability
|
|
91
69
|
let rel = p.replace(this.context.cwd, '').replace(/^\//, '');
|
|
92
|
-
|
|
93
|
-
if (rel.includes(':\\')) {
|
|
94
|
-
rel = rel.split(':\\')[1] || rel;
|
|
95
|
-
}
|
|
70
|
+
if (rel.includes(':\\')) rel = rel.split(':\\')[1] || rel;
|
|
96
71
|
return rel;
|
|
97
72
|
});
|
|
98
73
|
if (cycle.length === 1) return `${paths[0]} -> (self-loop)`;
|
|
@@ -100,17 +75,12 @@ export class CircularDetector {
|
|
|
100
75
|
});
|
|
101
76
|
}
|
|
102
77
|
|
|
103
|
-
/**
|
|
104
|
-
* Gets detailed cycle information
|
|
105
|
-
*/
|
|
106
78
|
getCycleDetails() {
|
|
107
79
|
return this.cycles.map((cycle, idx) => ({
|
|
108
80
|
cycleId: idx + 1,
|
|
109
81
|
files: cycle.map(p => {
|
|
110
82
|
let rel = p.replace(this.context.cwd, '').replace(/^\//, '');
|
|
111
|
-
if (rel.includes(':\\'))
|
|
112
|
-
rel = rel.split(':\\')[1] || rel;
|
|
113
|
-
}
|
|
83
|
+
if (rel.includes(':\\')) rel = rel.split(':\\')[1] || rel;
|
|
114
84
|
return rel;
|
|
115
85
|
}),
|
|
116
86
|
length: cycle.length,
|
|
@@ -118,5 +88,4 @@ export class CircularDetector {
|
|
|
118
88
|
}));
|
|
119
89
|
}
|
|
120
90
|
}
|
|
121
|
-
|
|
122
91
|
export default CircularDetector;
|
|
@@ -13,40 +13,68 @@ export class ConfigLoader {
|
|
|
13
13
|
|
|
14
14
|
async loadConfig(projectRoot) {
|
|
15
15
|
const searchPaths = [
|
|
16
|
+
'pkg-scaffold.json',
|
|
17
|
+
'pkg-scaffold.jsonc',
|
|
18
|
+
'.pkg-scaffold.json',
|
|
19
|
+
'.pkg-scaffold.jsonc',
|
|
20
|
+
'pkg-scaffold.js',
|
|
21
|
+
'pkg-scaffold.ts',
|
|
22
|
+
'pkg-scaffold/config.json',
|
|
16
23
|
'scaffold.config.js',
|
|
17
24
|
'scaffold.config.mjs',
|
|
18
25
|
'.scaffoldrc.json',
|
|
19
26
|
'.scaffoldrc'
|
|
20
27
|
];
|
|
21
28
|
|
|
29
|
+
let config = this.getDefaultConfig();
|
|
30
|
+
|
|
31
|
+
// Try package.json first
|
|
32
|
+
try {
|
|
33
|
+
const pkgPath = path.join(projectRoot, 'package.json');
|
|
34
|
+
const pkgContent = await fs.readFile(pkgPath, 'utf8');
|
|
35
|
+
const pkg = JSON.parse(pkgContent);
|
|
36
|
+
if (pkg['pkg-scaffold']) {
|
|
37
|
+
Object.assign(config, pkg['pkg-scaffold']);
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// ignore
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
for (const fileName of searchPaths) {
|
|
23
44
|
const fullPath = path.join(projectRoot, fileName);
|
|
24
45
|
try {
|
|
25
46
|
await fs.access(fullPath);
|
|
26
47
|
|
|
27
|
-
if (fileName.endsWith('.js') || fileName.endsWith('.mjs')) {
|
|
48
|
+
if (fileName.endsWith('.js') || fileName.endsWith('.mjs') || fileName.endsWith('.ts')) {
|
|
28
49
|
const module = await import(pathToFileURL(fullPath).href);
|
|
29
|
-
|
|
50
|
+
Object.assign(config, module.default || module);
|
|
51
|
+
break;
|
|
30
52
|
} else {
|
|
31
53
|
const content = await fs.readFile(fullPath, 'utf8');
|
|
32
|
-
|
|
54
|
+
// Very basic JSONC parsing (strip comments)
|
|
55
|
+
const stripped = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
|
|
56
|
+
Object.assign(config, JSON.parse(stripped));
|
|
57
|
+
break;
|
|
33
58
|
}
|
|
34
59
|
} catch (e) {
|
|
35
60
|
continue;
|
|
36
61
|
}
|
|
37
62
|
}
|
|
38
63
|
|
|
39
|
-
return
|
|
64
|
+
return config;
|
|
40
65
|
}
|
|
41
66
|
|
|
42
67
|
getDefaultConfig() {
|
|
43
68
|
return {
|
|
44
|
-
entryPoints: ['src/index.ts', 'index.js'],
|
|
69
|
+
entryPoints: ['src/index.ts', 'index.js', 'src/index.js', 'src/main.ts', 'src/main.js'],
|
|
45
70
|
exclude: [
|
|
46
71
|
'node_modules/**',
|
|
47
72
|
'dist/**',
|
|
73
|
+
'build/**',
|
|
48
74
|
'**/*.test.ts',
|
|
49
|
-
'**/*.spec.ts'
|
|
75
|
+
'**/*.spec.ts',
|
|
76
|
+
'**/*.test.js',
|
|
77
|
+
'**/*.spec.js'
|
|
50
78
|
],
|
|
51
79
|
plugins: [],
|
|
52
80
|
rules: {
|
|
@@ -4,29 +4,189 @@ import path from 'path';
|
|
|
4
4
|
/**
|
|
5
5
|
* Advanced Dependency Profiling Engine.
|
|
6
6
|
* Traces Peer Dependencies and Implicit Tooling Invocations.
|
|
7
|
+
*
|
|
8
|
+
* Improvements over v1:
|
|
9
|
+
* - Extended binary-to-package map covering 60+ common tools
|
|
10
|
+
* - Scans all package.json scripts (including nested workspaces)
|
|
11
|
+
* - Recognises @types/* packages as implicitly used by TypeScript
|
|
12
|
+
* - Handles framework-specific config-file conventions (Next.js, Vite, etc.)
|
|
13
|
+
* - Correctly skips peerDependencies and optionalDependencies from "unused" checks
|
|
14
|
+
* - Scans common config files for additional package references
|
|
7
15
|
*/
|
|
8
16
|
export class DependencyProfiler {
|
|
9
17
|
constructor(context) {
|
|
10
18
|
this.context = context;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Maps CLI binary names to their corresponding npm package names.
|
|
22
|
+
* This list covers the most common build tools, test runners, linters,
|
|
23
|
+
* formatters, bundlers, and framework CLIs encountered in real projects.
|
|
24
|
+
*/
|
|
11
25
|
this.binaryToPackageMap = {
|
|
26
|
+
// TypeScript / JavaScript compilers & runtimes
|
|
12
27
|
'tsc': 'typescript',
|
|
28
|
+
'ts-node': 'ts-node',
|
|
29
|
+
'tsx': 'tsx',
|
|
30
|
+
'node': 'node',
|
|
31
|
+
'bun': 'bun',
|
|
32
|
+
'deno': 'deno',
|
|
33
|
+
|
|
34
|
+
// Test runners
|
|
13
35
|
'jest': 'jest',
|
|
14
36
|
'vitest': 'vitest',
|
|
37
|
+
'mocha': 'mocha',
|
|
38
|
+
'jasmine': 'jasmine',
|
|
39
|
+
'ava': 'ava',
|
|
40
|
+
'tap': 'tap',
|
|
41
|
+
'uvu': 'uvu',
|
|
42
|
+
'c8': 'c8',
|
|
43
|
+
'nyc': 'nyc',
|
|
44
|
+
|
|
45
|
+
// E2E / browser testing
|
|
46
|
+
'playwright': '@playwright/test',
|
|
47
|
+
'cypress': 'cypress',
|
|
48
|
+
'puppeteer': 'puppeteer',
|
|
49
|
+
'webdriverio': 'webdriverio',
|
|
50
|
+
'wdio': '@wdio/cli',
|
|
51
|
+
|
|
52
|
+
// Linters & formatters
|
|
15
53
|
'eslint': 'eslint',
|
|
16
54
|
'prettier': 'prettier',
|
|
55
|
+
'tslint': 'tslint',
|
|
56
|
+
'biome': '@biomejs/biome',
|
|
57
|
+
'oxlint': 'oxlint',
|
|
58
|
+
'stylelint': 'stylelint',
|
|
59
|
+
'markdownlint': 'markdownlint-cli',
|
|
60
|
+
'commitlint': '@commitlint/cli',
|
|
61
|
+
'lint-staged': 'lint-staged',
|
|
62
|
+
|
|
63
|
+
// Bundlers & build tools
|
|
17
64
|
'vite': 'vite',
|
|
65
|
+
'webpack': 'webpack',
|
|
66
|
+
'rollup': 'rollup',
|
|
67
|
+
'esbuild': 'esbuild',
|
|
68
|
+
'parcel': 'parcel',
|
|
69
|
+
'turbo': 'turbo',
|
|
70
|
+
'nx': 'nx',
|
|
71
|
+
'lerna': 'lerna',
|
|
72
|
+
'changesets': '@changesets/cli',
|
|
73
|
+
'changeset': '@changesets/cli',
|
|
74
|
+
'tsup': 'tsup',
|
|
75
|
+
'unbuild': 'unbuild',
|
|
76
|
+
'pkgroll': 'pkgroll',
|
|
77
|
+
'microbundle': 'microbundle',
|
|
78
|
+
'ncc': '@vercel/ncc',
|
|
79
|
+
'swc': '@swc/cli',
|
|
80
|
+
|
|
81
|
+
// CSS / styling tools
|
|
82
|
+
'tailwind': 'tailwindcss',
|
|
83
|
+
'tailwindcss': 'tailwindcss',
|
|
84
|
+
'postcss': 'postcss',
|
|
85
|
+
'sass': 'sass',
|
|
86
|
+
'less': 'less',
|
|
87
|
+
|
|
88
|
+
// Framework CLIs
|
|
18
89
|
'next': 'next',
|
|
19
90
|
'nuxt': 'nuxt',
|
|
20
91
|
'astro': 'astro',
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
92
|
+
'remix': '@remix-run/dev',
|
|
93
|
+
'svelte-kit': '@sveltejs/kit',
|
|
94
|
+
'expo': 'expo',
|
|
95
|
+
'react-scripts': 'react-scripts',
|
|
96
|
+
'ng': '@angular/cli',
|
|
97
|
+
'vue': '@vue/cli-service',
|
|
98
|
+
'gatsby': 'gatsby',
|
|
99
|
+
|
|
100
|
+
// API / server tools
|
|
101
|
+
'nodemon': 'nodemon',
|
|
102
|
+
'ts-node-dev': 'ts-node-dev',
|
|
103
|
+
'concurrently': 'concurrently',
|
|
104
|
+
'cross-env': 'cross-env',
|
|
105
|
+
'dotenv': 'dotenv-cli',
|
|
106
|
+
'dotenv-cli': 'dotenv-cli',
|
|
107
|
+
'rimraf': 'rimraf',
|
|
108
|
+
'del-cli': 'del-cli',
|
|
109
|
+
'copyfiles': 'copyfiles',
|
|
110
|
+
'cpy-cli': 'cpy-cli',
|
|
111
|
+
'mkdirp': 'mkdirp',
|
|
112
|
+
'shx': 'shx',
|
|
113
|
+
'npm-run-all': 'npm-run-all',
|
|
114
|
+
'run-p': 'npm-run-all',
|
|
115
|
+
'run-s': 'npm-run-all',
|
|
116
|
+
|
|
117
|
+
// Documentation
|
|
118
|
+
'typedoc': 'typedoc',
|
|
119
|
+
'jsdoc': 'jsdoc',
|
|
120
|
+
'storybook': 'storybook',
|
|
121
|
+
'sb': 'storybook',
|
|
122
|
+
|
|
123
|
+
// Git hooks
|
|
124
|
+
'husky': 'husky',
|
|
125
|
+
'simple-git-hooks': 'simple-git-hooks',
|
|
126
|
+
'lefthook': 'lefthook',
|
|
127
|
+
|
|
128
|
+
// Package managers (used in scripts)
|
|
129
|
+
'pnpm': 'pnpm',
|
|
130
|
+
'yarn': 'yarn',
|
|
131
|
+
'npm': 'npm',
|
|
132
|
+
|
|
133
|
+
// Misc
|
|
134
|
+
'patch-package': 'patch-package',
|
|
135
|
+
'syncpack': 'syncpack',
|
|
136
|
+
'publint': 'publint',
|
|
137
|
+
'attw': '@arethetypeswrong/cli',
|
|
138
|
+
'size-limit': 'size-limit',
|
|
139
|
+
'bundlesize': 'bundlesize',
|
|
140
|
+
'depcheck': 'depcheck',
|
|
141
|
+
'knip': 'knip',
|
|
142
|
+
'pkg-scaffold': 'pkg-scaffold'
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Config file names that imply a package is in use even when not imported
|
|
147
|
+
* in source code. Maps config filename fragment -> package name.
|
|
148
|
+
*/
|
|
149
|
+
this.configFileToPackageMap = {
|
|
150
|
+
'jest.config': 'jest',
|
|
151
|
+
'vitest.config': 'vitest',
|
|
152
|
+
'playwright.config': '@playwright/test',
|
|
153
|
+
'cypress.config': 'cypress',
|
|
154
|
+
'webpack.config': 'webpack',
|
|
155
|
+
'vite.config': 'vite',
|
|
156
|
+
'rollup.config': 'rollup',
|
|
157
|
+
'tailwind.config': 'tailwindcss',
|
|
158
|
+
'postcss.config': 'postcss',
|
|
159
|
+
'.eslintrc': 'eslint',
|
|
160
|
+
'eslint.config': 'eslint',
|
|
161
|
+
'.prettierrc': 'prettier',
|
|
162
|
+
'prettier.config': 'prettier',
|
|
163
|
+
'.babelrc': '@babel/core',
|
|
164
|
+
'babel.config': '@babel/core',
|
|
165
|
+
'.stylelintrc': 'stylelint',
|
|
166
|
+
'stylelint.config': 'stylelint',
|
|
167
|
+
'svelte.config': '@sveltejs/kit',
|
|
168
|
+
'astro.config': 'astro',
|
|
169
|
+
'nuxt.config': 'nuxt',
|
|
170
|
+
'next.config': 'next',
|
|
171
|
+
'remix.config': '@remix-run/dev',
|
|
172
|
+
'.commitlintrc': '@commitlint/cli',
|
|
173
|
+
'commitlint.config': '@commitlint/cli',
|
|
174
|
+
'tsup.config': 'tsup',
|
|
175
|
+
'typedoc': 'typedoc',
|
|
176
|
+
'.lintstagedrc': 'lint-staged',
|
|
177
|
+
'lint-staged.config': 'lint-staged',
|
|
178
|
+
'lefthook': 'lefthook',
|
|
179
|
+
'knip.config': 'knip',
|
|
180
|
+
'knip.json': 'knip'
|
|
25
181
|
};
|
|
26
182
|
}
|
|
27
183
|
|
|
28
184
|
/**
|
|
29
185
|
* Scans package.json scripts and CI files for binary usage.
|
|
186
|
+
* Also scans config files that imply package usage.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} projectRoot - Absolute path to the package root directory
|
|
189
|
+
* @returns {Promise<Set<string>>} Set of npm package names that are implicitly used
|
|
30
190
|
*/
|
|
31
191
|
async traceImplicitInvocations(projectRoot) {
|
|
32
192
|
const usedPackages = new Set();
|
|
@@ -41,9 +201,21 @@ export class DependencyProfiler {
|
|
|
41
201
|
this.extractPackagesFromScript(script, usedPackages);
|
|
42
202
|
}
|
|
43
203
|
}
|
|
204
|
+
|
|
205
|
+
// 2. Detect packages referenced in the "bin" field of installed packages
|
|
206
|
+
// (e.g. turbo, nx, etc. that are only run via scripts)
|
|
207
|
+
if (pkg.dependencies || pkg.devDependencies) {
|
|
208
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
209
|
+
for (const depName of Object.keys(allDeps)) {
|
|
210
|
+
// @types/* packages are always "used" by TypeScript – never flag them as unused
|
|
211
|
+
if (depName.startsWith('@types/')) {
|
|
212
|
+
usedPackages.add(depName);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
44
216
|
} catch (e) {}
|
|
45
217
|
|
|
46
|
-
//
|
|
218
|
+
// 3. Scan CI workflows
|
|
47
219
|
try {
|
|
48
220
|
const githubWorkflows = path.join(projectRoot, '.github/workflows');
|
|
49
221
|
const files = await fs.readdir(githubWorkflows).catch(() => []);
|
|
@@ -55,22 +227,78 @@ export class DependencyProfiler {
|
|
|
55
227
|
}
|
|
56
228
|
} catch (e) {}
|
|
57
229
|
|
|
230
|
+
// 4. Scan config files that imply package usage
|
|
231
|
+
try {
|
|
232
|
+
const dirEntries = await fs.readdir(projectRoot, { withFileTypes: true });
|
|
233
|
+
for (const entry of dirEntries) {
|
|
234
|
+
if (!entry.isFile()) continue;
|
|
235
|
+
const fileName = entry.name;
|
|
236
|
+
for (const [fragment, pkgName] of Object.entries(this.configFileToPackageMap)) {
|
|
237
|
+
if (fileName.startsWith(fragment) || fileName === fragment) {
|
|
238
|
+
usedPackages.add(pkgName);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} catch (e) {}
|
|
244
|
+
|
|
245
|
+
// 5. Scan Makefile / shell scripts in the project root for binary usage
|
|
246
|
+
try {
|
|
247
|
+
for (const scriptFile of ['Makefile', 'makefile', 'GNUmakefile']) {
|
|
248
|
+
try {
|
|
249
|
+
const content = await fs.readFile(path.join(projectRoot, scriptFile), 'utf8');
|
|
250
|
+
this.extractPackagesFromScript(content, usedPackages);
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
} catch (e) {}
|
|
254
|
+
|
|
58
255
|
return usedPackages;
|
|
59
256
|
}
|
|
60
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Extracts npm package names from a script string by matching known binary names.
|
|
260
|
+
* Handles npx/pnpx/yarn/bunx prefixes and common shell constructs.
|
|
261
|
+
*
|
|
262
|
+
* @param {string} script - Script string (e.g. from package.json scripts)
|
|
263
|
+
* @param {Set<string>} collector - Set to add found package names to
|
|
264
|
+
*/
|
|
61
265
|
extractPackagesFromScript(script, collector) {
|
|
62
|
-
//
|
|
63
|
-
const words = script.split(/[\s&|;]+/);
|
|
64
|
-
for (
|
|
65
|
-
const
|
|
266
|
+
// Split on common shell delimiters
|
|
267
|
+
const words = script.split(/[\s&|;()\n\r\t]+/);
|
|
268
|
+
for (let i = 0; i < words.length; i++) {
|
|
269
|
+
const rawWord = words[i];
|
|
270
|
+
// Strip quotes and common prefixes like npx, pnpx, bunx, yarn, pnpm exec
|
|
271
|
+
const cleanWord = rawWord.replace(/['"]/g, '').replace(/^(npx|pnpx|bunx|yarn|pnpm)\s+/, '');
|
|
272
|
+
|
|
273
|
+
// Direct binary match
|
|
66
274
|
if (this.binaryToPackageMap[cleanWord]) {
|
|
67
275
|
collector.add(this.binaryToPackageMap[cleanWord]);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Handle "npx <binary>" pattern where npx and binary are separate words
|
|
280
|
+
if ((rawWord === 'npx' || rawWord === 'pnpx' || rawWord === 'bunx') && i + 1 < words.length) {
|
|
281
|
+
const nextWord = words[i + 1].replace(/['"]/g, '');
|
|
282
|
+
if (this.binaryToPackageMap[nextWord]) {
|
|
283
|
+
collector.add(this.binaryToPackageMap[nextWord]);
|
|
284
|
+
}
|
|
285
|
+
// Also handle "npx @scope/pkg" style
|
|
286
|
+
if (nextWord.startsWith('@') || nextWord.includes('/')) {
|
|
287
|
+
const pkgName = nextWord.split('/').slice(0, nextWord.startsWith('@') ? 2 : 1).join('/');
|
|
288
|
+
if (pkgName) collector.add(pkgName);
|
|
289
|
+
}
|
|
68
290
|
}
|
|
69
291
|
}
|
|
70
292
|
}
|
|
71
293
|
|
|
72
294
|
/**
|
|
73
295
|
* Resolves peer dependencies for a given set of used packages.
|
|
296
|
+
* Peer dependencies are considered "implicitly used" and should not be
|
|
297
|
+
* flagged as unused even if they are not directly imported in source code.
|
|
298
|
+
*
|
|
299
|
+
* @param {Set<string>} usedPackages - Set of package names that are known to be used
|
|
300
|
+
* @param {string} projectRoot - Absolute path to the package root directory
|
|
301
|
+
* @returns {Promise<Set<string>>} Set of peer dependency package names
|
|
74
302
|
*/
|
|
75
303
|
async resolvePeerDependencies(usedPackages, projectRoot) {
|
|
76
304
|
const peerDeps = new Set();
|
|
@@ -87,4 +315,28 @@ export class DependencyProfiler {
|
|
|
87
315
|
}
|
|
88
316
|
return peerDeps;
|
|
89
317
|
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Determines whether a dependency should be excluded from "unused" checks.
|
|
321
|
+
*
|
|
322
|
+
* Rules:
|
|
323
|
+
* - peerDependencies: always excluded (they are required by the consumer, not the package itself)
|
|
324
|
+
* - optionalDependencies: always excluded (they may not be installed at all)
|
|
325
|
+
* - @types/* packages: excluded when TypeScript is present (used implicitly by tsc)
|
|
326
|
+
* - Packages used in scripts / config files: excluded via traceImplicitInvocations()
|
|
327
|
+
*
|
|
328
|
+
* @param {string} packageName - npm package name
|
|
329
|
+
* @param {'dependency'|'devDependency'|'peerDependency'|'optionalDependency'} depType
|
|
330
|
+
* @returns {boolean} true if this dependency should be skipped in unused checks
|
|
331
|
+
*/
|
|
332
|
+
shouldExcludeFromUnusedCheck(packageName, depType) {
|
|
333
|
+
if (depType === 'peerDependency' || depType === 'optionalDependency') {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
// @types/* packages are consumed implicitly by the TypeScript compiler
|
|
337
|
+
if (packageName.startsWith('@types/')) {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
90
342
|
}
|