pkg-scaffold 3.3.3 → 3.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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
- // Convert absolute Windows paths
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
- return module.default || module;
50
+ Object.assign(config, module.default || module);
51
+ break;
30
52
  } else {
31
53
  const content = await fs.readFile(fullPath, 'utf8');
32
- return JSON.parse(content);
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 this.getDefaultConfig();
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
- 'playwright': 'playwright',
22
- 'cypress': 'cypress',
23
- 'tailwind': 'tailwindcss',
24
- 'postcss': 'postcss'
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
- // 2. Scan CI workflows
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
- // Basic regex to find binary-like words
63
- const words = script.split(/[\s&|;]+/);
64
- for (const word of words) {
65
- const cleanWord = word.replace(/['"]/g, '');
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
  }