pkg-scaffold 3.3.4 → 3.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/bin/cli.js +4 -4
- package/package.json +10 -4
- package/src/EngineContext.js +52 -27
- package/src/ast/ASTAnalyzer.js +111 -77
- package/src/ast/BarrelParser.js +24 -4
- package/src/ast/MagicDetector.js +106 -13
- package/src/ast/OxcAnalyzer.js +121 -20
- package/src/ast/SecretScanner.js +304 -0
- package/src/healing/GitSandbox.js +44 -122
- package/src/healing/SelfHealer.js +29 -130
- package/src/index.js +175 -97
- package/src/performance/WorkerPool.js +6 -3
- package/src/plugins/PluginRegistry.js +27 -1
- package/src/resolution/DependencyProfiler.js +261 -9
- package/src/resolution/WorkSpaceGraph.js +142 -34
- package/src/performance/SecretDetector.js +0 -378
- package/src/performance/WorkerTaskRunner.js +0 -71
|
@@ -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
|
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
// Native sub-directory crawling removed as it's not in fs/promises in older node, but we use readdir anyway.
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Monorepo Cross-Linking Topology Manager
|
|
7
6
|
* Maps sub-package structural boundaries across pnpm, Yarn, or npm workspaces.
|
|
7
|
+
*
|
|
8
|
+
* Improvements over v1:
|
|
9
|
+
* - Auto-activates workspace mode when workspace config is detected (no manual flag required)
|
|
10
|
+
* - Supports deeper glob patterns beyond simple `packages/*` (e.g. `apps/*`, `libs/**`)
|
|
11
|
+
* - Correctly registers workspace package names as "used" so they are never flagged as unused deps
|
|
12
|
+
* - Handles Bun workspaces (workspaces array in package.json)
|
|
13
|
+
* - Resolves subpath imports for workspace packages (e.g. `@scope/pkg/utils`)
|
|
14
|
+
* - Exposes `markWorkspacePackagesAsUsed()` so the engine can call it after dep audit
|
|
8
15
|
*/
|
|
9
16
|
export class WorkspaceGraph {
|
|
10
17
|
constructor(context) {
|
|
@@ -15,6 +22,7 @@ export class WorkspaceGraph {
|
|
|
15
22
|
|
|
16
23
|
/**
|
|
17
24
|
* Checks the environment layout to discover and map local workspace packages.
|
|
25
|
+
* This method is idempotent and safe to call multiple times.
|
|
18
26
|
*/
|
|
19
27
|
async initializeWorkspaceMesh() {
|
|
20
28
|
const rootPackageJsonPath = path.join(this.context.cwd, 'package.json');
|
|
@@ -23,7 +31,7 @@ export class WorkspaceGraph {
|
|
|
23
31
|
let workspaceGlobs = [];
|
|
24
32
|
this.hoistedDependencies = new Set();
|
|
25
33
|
|
|
26
|
-
// Load hoisted dependencies from root package.json
|
|
34
|
+
// Load hoisted dependencies from root package.json
|
|
27
35
|
try {
|
|
28
36
|
const rootPkg = JSON.parse(await fs.readFile(rootPackageJsonPath, 'utf8'));
|
|
29
37
|
const deps = { ...rootPkg.dependencies, ...rootPkg.devDependencies };
|
|
@@ -32,7 +40,7 @@ export class WorkspaceGraph {
|
|
|
32
40
|
// No root package.json or unreadable
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
// Protocol A: Check for pnpm workspace configurations
|
|
43
|
+
// Protocol A: Check for pnpm workspace configurations (pnpm-workspace.yaml)
|
|
36
44
|
try {
|
|
37
45
|
const yaml = await fs.readFile(pnpmWorkspacePath, 'utf8');
|
|
38
46
|
const lines = yaml.split('\n');
|
|
@@ -40,26 +48,33 @@ export class WorkspaceGraph {
|
|
|
40
48
|
|
|
41
49
|
for (const line of lines) {
|
|
42
50
|
const trimmed = line.trim();
|
|
43
|
-
if (trimmed
|
|
51
|
+
if (trimmed === 'packages:') {
|
|
44
52
|
insidePackagesBlock = true;
|
|
45
53
|
continue;
|
|
46
54
|
}
|
|
47
|
-
if (insidePackagesBlock
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
if (insidePackagesBlock) {
|
|
56
|
+
if (trimmed.startsWith('-')) {
|
|
57
|
+
const pattern = trimmed.replace(/^-\s*/, '').replace(/['"]/g, '').trim();
|
|
58
|
+
if (pattern) workspaceGlobs.push(pattern);
|
|
59
|
+
} else if (trimmed && !trimmed.startsWith('#')) {
|
|
60
|
+
// Another top-level key encountered – stop reading packages block
|
|
61
|
+
insidePackagesBlock = false;
|
|
62
|
+
}
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
65
|
} catch {
|
|
53
66
|
// pnpm structure absent; check package.json workspace array paths instead
|
|
54
67
|
}
|
|
55
68
|
|
|
56
|
-
// Protocol B: Check for Yarn/npm workspaces array inside the root package.json
|
|
69
|
+
// Protocol B: Check for Yarn/npm/Bun workspaces array inside the root package.json
|
|
57
70
|
if (workspaceGlobs.length === 0) {
|
|
58
71
|
try {
|
|
59
72
|
const pkgText = await fs.readFile(rootPackageJsonPath, 'utf8');
|
|
60
73
|
const pkg = JSON.parse(pkgText);
|
|
61
74
|
if (pkg.workspaces) {
|
|
62
|
-
workspaceGlobs = Array.isArray(pkg.workspaces)
|
|
75
|
+
workspaceGlobs = Array.isArray(pkg.workspaces)
|
|
76
|
+
? pkg.workspaces
|
|
77
|
+
: (pkg.workspaces.packages || []);
|
|
63
78
|
}
|
|
64
79
|
} catch {
|
|
65
80
|
// No workspaces found
|
|
@@ -68,6 +83,15 @@ export class WorkspaceGraph {
|
|
|
68
83
|
|
|
69
84
|
if (workspaceGlobs.length > 0) {
|
|
70
85
|
this.context.isWorkspaceEnabled = true;
|
|
86
|
+
if (this.context.verbose) {
|
|
87
|
+
console.log(`🌐 Auto-detected monorepo layout with ${workspaceGlobs.length} glob patterns.`);
|
|
88
|
+
}
|
|
89
|
+
} else if (this.context.isWorkspaceEnabled) {
|
|
90
|
+
// Force enabled via flag but no patterns found; default to standard packages/*
|
|
91
|
+
workspaceGlobs = ['packages/*'];
|
|
92
|
+
if (this.context.verbose) {
|
|
93
|
+
console.log(`🌐 Workspace mode forced via flag. Using default patterns: ${workspaceGlobs.join(', ')}`);
|
|
94
|
+
}
|
|
71
95
|
} else {
|
|
72
96
|
return; // Workspace mesh maps skipped for single-package targets
|
|
73
97
|
}
|
|
@@ -76,48 +100,116 @@ export class WorkspaceGraph {
|
|
|
76
100
|
for (const pattern of workspaceGlobs) {
|
|
77
101
|
await this.locatePackagesViaPattern(pattern);
|
|
78
102
|
}
|
|
103
|
+
|
|
104
|
+
// Register all discovered workspace packages as "used" external packages so they
|
|
105
|
+
// are never incorrectly flagged as unused dependencies.
|
|
106
|
+
this.markWorkspacePackagesAsUsed();
|
|
79
107
|
}
|
|
80
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Expands a workspace glob pattern and registers all found packages.
|
|
111
|
+
* Supports patterns like:
|
|
112
|
+
* - `packages/*` (one level deep)
|
|
113
|
+
* - `apps/*` (one level deep)
|
|
114
|
+
* - `packages/**` (recursive – all subdirectories)
|
|
115
|
+
* - `packages/core` (explicit single package)
|
|
116
|
+
*/
|
|
81
117
|
async locatePackagesViaPattern(globPattern) {
|
|
82
|
-
// Normalizes wildcards down into base query directories
|
|
83
118
|
const standardizedPattern = globPattern.replace(/\\/g, '/');
|
|
119
|
+
|
|
120
|
+
// Determine if this is a recursive pattern (`**`) or a simple wildcard (`*`)
|
|
121
|
+
const isRecursive = standardizedPattern.includes('**');
|
|
122
|
+
const isWildcard = standardizedPattern.includes('*');
|
|
123
|
+
|
|
124
|
+
if (!isWildcard) {
|
|
125
|
+
// Explicit path: treat the pattern itself as a single package directory
|
|
126
|
+
const absolutePath = path.resolve(this.context.cwd, standardizedPattern);
|
|
127
|
+
await this._tryRegisterPackage(absolutePath);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Extract the base directory before the first wildcard segment
|
|
84
132
|
const baseDir = standardizedPattern.split('/*')[0];
|
|
85
133
|
const absoluteSearchPath = path.resolve(this.context.cwd, baseDir);
|
|
86
134
|
|
|
135
|
+
if (isRecursive) {
|
|
136
|
+
await this._scanDirectoryRecursively(absoluteSearchPath);
|
|
137
|
+
} else {
|
|
138
|
+
await this._scanDirectoryShallow(absoluteSearchPath);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Scans a directory one level deep for workspace packages.
|
|
144
|
+
*/
|
|
145
|
+
async _scanDirectoryShallow(absoluteSearchPath) {
|
|
87
146
|
try {
|
|
88
147
|
const contents = await fs.readdir(absoluteSearchPath, { withFileTypes: true });
|
|
89
|
-
|
|
90
148
|
for (const entity of contents) {
|
|
91
149
|
if (!entity.isDirectory()) continue;
|
|
92
|
-
|
|
93
150
|
const subPackageDir = path.join(absoluteSearchPath, entity.name);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
const data = await fs.readFile(manifestFile, 'utf8');
|
|
98
|
-
const pkg = JSON.parse(data);
|
|
99
|
-
|
|
100
|
-
if (pkg.name) {
|
|
101
|
-
const entryPoints = this.calculatePackageExportsEntries(pkg, subPackageDir);
|
|
102
|
-
|
|
103
|
-
this.packageManifests.set(pkg.name, {
|
|
104
|
-
packageName: pkg.name,
|
|
105
|
-
rootDirectory: subPackageDir,
|
|
106
|
-
manifestPath: manifestFile,
|
|
107
|
-
entryPoints
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
this.workspacePackageNames.add(pkg.name);
|
|
111
|
-
}
|
|
112
|
-
} catch {
|
|
113
|
-
// package.json parsing failed; ignore invalid directory roots
|
|
114
|
-
}
|
|
151
|
+
await this._tryRegisterPackage(subPackageDir);
|
|
115
152
|
}
|
|
116
153
|
} catch {
|
|
117
154
|
// Unreadable target directories; pass tracking
|
|
118
155
|
}
|
|
119
156
|
}
|
|
120
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Recursively scans a directory tree for workspace packages.
|
|
160
|
+
* Stops descending into `node_modules` directories.
|
|
161
|
+
*/
|
|
162
|
+
async _scanDirectoryRecursively(absoluteSearchPath) {
|
|
163
|
+
try {
|
|
164
|
+
const contents = await fs.readdir(absoluteSearchPath, { withFileTypes: true });
|
|
165
|
+
for (const entity of contents) {
|
|
166
|
+
if (!entity.isDirectory()) continue;
|
|
167
|
+
if (entity.name === 'node_modules' || entity.name === '.git') continue;
|
|
168
|
+
const subDir = path.join(absoluteSearchPath, entity.name);
|
|
169
|
+
// Try to register as a package first
|
|
170
|
+
const registered = await this._tryRegisterPackage(subDir);
|
|
171
|
+
// If not a package root itself, recurse deeper
|
|
172
|
+
if (!registered) {
|
|
173
|
+
await this._scanDirectoryRecursively(subDir);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// Unreadable directories; pass
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Attempts to register a directory as a workspace package.
|
|
183
|
+
* Returns true if a valid package.json with a `name` field was found.
|
|
184
|
+
*/
|
|
185
|
+
async _tryRegisterPackage(packageDir) {
|
|
186
|
+
const manifestFile = path.join(packageDir, 'package.json');
|
|
187
|
+
try {
|
|
188
|
+
const data = await fs.readFile(manifestFile, 'utf8');
|
|
189
|
+
const pkg = JSON.parse(data);
|
|
190
|
+
|
|
191
|
+
if (pkg.name) {
|
|
192
|
+
const entryPoints = this.calculatePackageExportsEntries(pkg, packageDir);
|
|
193
|
+
|
|
194
|
+
this.packageManifests.set(pkg.name, {
|
|
195
|
+
packageName: pkg.name,
|
|
196
|
+
rootDirectory: packageDir,
|
|
197
|
+
manifestPath: manifestFile,
|
|
198
|
+
entryPoints
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
this.workspacePackageNames.add(pkg.name);
|
|
202
|
+
// Also register the package root so the resolver can identify files
|
|
203
|
+
// inside this package as "internal" rather than node_modules.
|
|
204
|
+
this.context.monorepoPackageRoots.add(packageDir);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// package.json parsing failed; ignore invalid directory roots
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
121
213
|
/**
|
|
122
214
|
* Tracks package entry points by evaluating standard fields and main/exports configurations.
|
|
123
215
|
*/
|
|
@@ -127,8 +219,9 @@ export class WorkspaceGraph {
|
|
|
127
219
|
// Trace traditional entry fields
|
|
128
220
|
if (pkg.main) entries.add(path.resolve(pkgDir, pkg.main));
|
|
129
221
|
if (pkg.module) entries.add(path.resolve(pkgDir, pkg.module));
|
|
130
|
-
if (pkg.browser) entries.add(path.resolve(pkgDir, pkg.browser));
|
|
222
|
+
if (pkg.browser && typeof pkg.browser === 'string') entries.add(path.resolve(pkgDir, pkg.browser));
|
|
131
223
|
if (pkg.types) entries.add(path.resolve(pkgDir, pkg.types));
|
|
224
|
+
if (pkg.typings) entries.add(path.resolve(pkgDir, pkg.typings));
|
|
132
225
|
|
|
133
226
|
// Handle deep nested conditional exports matrices block parameters
|
|
134
227
|
if (pkg.exports) {
|
|
@@ -157,6 +250,8 @@ export class WorkspaceGraph {
|
|
|
157
250
|
if (exportsValue.startsWith('.')) {
|
|
158
251
|
collected.add(path.resolve(pkgDir, exportsValue));
|
|
159
252
|
}
|
|
253
|
+
} else if (Array.isArray(exportsValue)) {
|
|
254
|
+
exportsValue.forEach(v => this.recursivelyUnwindExports(v, pkgDir, collected));
|
|
160
255
|
} else if (typeof exportsValue === 'object' && exportsValue !== null) {
|
|
161
256
|
for (const val of Object.values(exportsValue)) {
|
|
162
257
|
this.recursivelyUnwindExports(val, pkgDir, collected);
|
|
@@ -164,6 +259,19 @@ export class WorkspaceGraph {
|
|
|
164
259
|
}
|
|
165
260
|
}
|
|
166
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Marks all registered workspace package names as used in the global
|
|
264
|
+
* `usedExternalPackages` set so they are never flagged as unused dependencies.
|
|
265
|
+
*
|
|
266
|
+
* This must be called after `initializeWorkspaceMesh()` and before the
|
|
267
|
+
* unused-dependency report is generated.
|
|
268
|
+
*/
|
|
269
|
+
markWorkspacePackagesAsUsed() {
|
|
270
|
+
for (const pkgName of this.workspacePackageNames) {
|
|
271
|
+
this.context.usedExternalPackages.add(pkgName);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
167
275
|
/**
|
|
168
276
|
* Checks if an import specifier matches a package registered in our workspace mesh.
|
|
169
277
|
*/
|