pkg-scaffold 1.1.1 ā 2.0.0
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/.github/workflows/npm-publish.yml +20 -0
- package/LICENSE +21 -0
- package/README.md +95 -15
- package/index.js +946 -301
- package/package.json +10 -3
package/index.js
CHANGED
|
@@ -10,24 +10,210 @@ import readline from 'readline/promises';
|
|
|
10
10
|
import * as acorn from 'acorn';
|
|
11
11
|
import * as walk from 'acorn-walk';
|
|
12
12
|
|
|
13
|
-
const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.turbo', 'coverage', 'out']);
|
|
14
|
-
const VALID_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
|
|
13
|
+
const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.turbo', 'coverage', 'out', '.next', '.nuxt', '.svelte-kit', 'storybook-static', '.cache']);
|
|
14
|
+
const VALID_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue', '.svelte']);
|
|
15
15
|
|
|
16
16
|
// --- Refined Target Signature Dictionaries ---
|
|
17
17
|
const REGEX_PATTERNS = {
|
|
18
18
|
env: /(?:process\.env|import\.meta\.env)\.([A-Z_][A-Z0-9_]*)/g,
|
|
19
|
-
testFile: /\.(test|spec)\.(js|ts|jsx|tsx)$/i,
|
|
20
|
-
|
|
19
|
+
testFile: /\.(test|spec)\.(js|ts|jsx|tsx|mjs|cjs)$/i,
|
|
20
|
+
configFile: /^(vite|webpack|rollup|babel|jest|vitest|tailwind|postcss|next|nuxt|svelte|astro)\.config\./i,
|
|
21
|
+
|
|
21
22
|
// Modern Quality & Structural Code Smell Monitors
|
|
22
23
|
legacyVar: /\bvar\s+[a-zA-Z_]/g,
|
|
23
24
|
dangerousEval: /\beval\s*\(/g,
|
|
24
25
|
syncFsCalls: /\.readFileSync|\.writeFileSync|\.mkdirSync|\.existsSync/g,
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
// Cryptographic Risk & Hardcoded Keyholes
|
|
27
28
|
secretKeys: /\b(secret|passwd|password|token|api_?key|private_?key)\s*=\s*['"`]([a-zA-Z0-9_\-\.]{8,})['"`]/gi
|
|
28
29
|
};
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
// ============================================================
|
|
32
|
+
// COMPREHENSIVE BINARY-TO-PACKAGE MAPPING (Knip-style)
|
|
33
|
+
// Maps CLI binary names ā npm package names
|
|
34
|
+
// ============================================================
|
|
35
|
+
const BINARY_TO_PACKAGE_MAP = {
|
|
36
|
+
// TypeScript / JavaScript compilers & runtimes
|
|
37
|
+
'tsc': 'typescript',
|
|
38
|
+
'ts-node': 'ts-node',
|
|
39
|
+
'tsx': 'tsx',
|
|
40
|
+
'tsup': 'tsup',
|
|
41
|
+
'esbuild': 'esbuild',
|
|
42
|
+
'swc': '@swc/cli',
|
|
43
|
+
|
|
44
|
+
// Test runners
|
|
45
|
+
'jest': 'jest',
|
|
46
|
+
'vitest': 'vitest',
|
|
47
|
+
'mocha': 'mocha',
|
|
48
|
+
'jasmine': 'jasmine',
|
|
49
|
+
'ava': 'ava',
|
|
50
|
+
'tap': 'tap',
|
|
51
|
+
'c8': 'c8',
|
|
52
|
+
'nyc': 'nyc',
|
|
53
|
+
|
|
54
|
+
// Linters & formatters
|
|
55
|
+
'eslint': 'eslint',
|
|
56
|
+
'prettier': 'prettier',
|
|
57
|
+
'biome': '@biomejs/biome',
|
|
58
|
+
'oxlint': 'oxlint',
|
|
59
|
+
'tslint': 'tslint',
|
|
60
|
+
'xo': 'xo',
|
|
61
|
+
'standard': 'standard',
|
|
62
|
+
|
|
63
|
+
// Bundlers & dev servers
|
|
64
|
+
'vite': 'vite',
|
|
65
|
+
'webpack': 'webpack',
|
|
66
|
+
'rollup': 'rollup',
|
|
67
|
+
'parcel': 'parcel',
|
|
68
|
+
'turbo': 'turbo',
|
|
69
|
+
'nx': 'nx',
|
|
70
|
+
|
|
71
|
+
// Process managers & watchers
|
|
72
|
+
'nodemon': 'nodemon',
|
|
73
|
+
'pm2': 'pm2',
|
|
74
|
+
'concurrently': 'concurrently',
|
|
75
|
+
'cross-env': 'cross-env',
|
|
76
|
+
'dotenv-cli': 'dotenv-cli',
|
|
77
|
+
'env-cmd': 'env-cmd',
|
|
78
|
+
|
|
79
|
+
// Code generation & scaffolding
|
|
80
|
+
'hygen': 'hygen',
|
|
81
|
+
'plop': 'plop',
|
|
82
|
+
'prisma': 'prisma',
|
|
83
|
+
'drizzle-kit': 'drizzle-kit',
|
|
84
|
+
'typeorm': 'typeorm',
|
|
85
|
+
'sequelize': 'sequelize-cli',
|
|
86
|
+
'knex': 'knex',
|
|
87
|
+
'mikro-orm': '@mikro-orm/cli',
|
|
88
|
+
|
|
89
|
+
// Build & deployment tools
|
|
90
|
+
'rimraf': 'rimraf',
|
|
91
|
+
'copyfiles': 'copyfiles',
|
|
92
|
+
'mkdirp': 'mkdirp',
|
|
93
|
+
'shx': 'shx',
|
|
94
|
+
'ncp': 'ncp',
|
|
95
|
+
'cpx': 'cpx',
|
|
96
|
+
'npm-run-all': 'npm-run-all',
|
|
97
|
+
'run-s': 'npm-run-all',
|
|
98
|
+
'run-p': 'npm-run-all',
|
|
99
|
+
|
|
100
|
+
// Documentation
|
|
101
|
+
'typedoc': 'typedoc',
|
|
102
|
+
'jsdoc': 'jsdoc',
|
|
103
|
+
'storybook': 'storybook',
|
|
104
|
+
'sb': 'storybook',
|
|
105
|
+
|
|
106
|
+
// Misc
|
|
107
|
+
'husky': 'husky',
|
|
108
|
+
'lint-staged': 'lint-staged',
|
|
109
|
+
'commitlint': '@commitlint/cli',
|
|
110
|
+
'release-it': 'release-it',
|
|
111
|
+
'semantic-release': 'semantic-release',
|
|
112
|
+
'changeset': '@changesets/cli',
|
|
113
|
+
'changesets': '@changesets/cli',
|
|
114
|
+
'np': 'np',
|
|
115
|
+
'bumpp': 'bumpp',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// ============================================================
|
|
119
|
+
// EXTENDED DEV TOOLING ECOSYSTEM (never flagged as unused)
|
|
120
|
+
// ============================================================
|
|
121
|
+
const DEV_TOOLING_ECOSYSTEM = new Set([
|
|
122
|
+
// Linters & formatters
|
|
123
|
+
'eslint', 'prettier', 'biome', '@biomejs/biome', 'oxlint', 'tslint', 'xo', 'standard',
|
|
124
|
+
// TypeScript
|
|
125
|
+
'typescript', 'typescript-eslint', '@eslint/js', 'ts-node', 'tsx', 'tsup', 'esbuild', '@swc/cli',
|
|
126
|
+
// Test runners
|
|
127
|
+
'jest', 'vitest', 'mocha', 'jasmine', 'ava', 'tap', 'c8', 'nyc',
|
|
128
|
+
// Bundlers
|
|
129
|
+
'vite', 'webpack', 'rollup', 'parcel', 'turbo', 'nx',
|
|
130
|
+
// Process managers
|
|
131
|
+
'nodemon', 'pm2', 'concurrently', 'cross-env', 'dotenv-cli', 'env-cmd',
|
|
132
|
+
// Build helpers
|
|
133
|
+
'rimraf', 'copyfiles', 'mkdirp', 'shx', 'ncp', 'cpx', 'npm-run-all',
|
|
134
|
+
// Docs
|
|
135
|
+
'typedoc', 'jsdoc', 'storybook',
|
|
136
|
+
// Release
|
|
137
|
+
'husky', 'lint-staged', '@commitlint/cli', 'release-it', 'semantic-release', '@changesets/cli', 'np', 'bumpp',
|
|
138
|
+
// ORM CLI tools
|
|
139
|
+
'prisma', 'drizzle-kit', 'typeorm', 'sequelize-cli', 'knex', '@mikro-orm/cli',
|
|
140
|
+
// Scaffolding
|
|
141
|
+
'hygen', 'plop',
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
// ============================================================
|
|
145
|
+
// KNOWN PACKAGE ALIASES (package name ā common import name)
|
|
146
|
+
// e.g. "lodash" is imported as "_", "express" as "app", etc.
|
|
147
|
+
// This helps avoid false positives in unused detection
|
|
148
|
+
// ============================================================
|
|
149
|
+
const PACKAGE_IMPORT_ALIASES = {
|
|
150
|
+
'lodash': ['_', 'lodash'],
|
|
151
|
+
'lodash-es': ['_', 'lodash'],
|
|
152
|
+
'underscore': ['_'],
|
|
153
|
+
'jquery': ['$', 'jQuery'],
|
|
154
|
+
'moment': ['moment'],
|
|
155
|
+
'dayjs': ['dayjs'],
|
|
156
|
+
'date-fns': ['dateFns'],
|
|
157
|
+
'ramda': ['R'],
|
|
158
|
+
'rxjs': ['Rx'],
|
|
159
|
+
'three': ['THREE'],
|
|
160
|
+
'chart.js': ['Chart'],
|
|
161
|
+
'socket.io': ['io', 'Server'],
|
|
162
|
+
'socket.io-client': ['io'],
|
|
163
|
+
'mongoose': ['mongoose'],
|
|
164
|
+
'sequelize': ['Sequelize'],
|
|
165
|
+
'typeorm': ['typeorm'],
|
|
166
|
+
'prisma': ['prisma', 'PrismaClient'],
|
|
167
|
+
'@prisma/client': ['prisma', 'PrismaClient'],
|
|
168
|
+
'knex': ['knex'],
|
|
169
|
+
'redis': ['redis', 'createClient'],
|
|
170
|
+
'ioredis': ['Redis'],
|
|
171
|
+
'pg': ['Pool', 'Client', 'pg'],
|
|
172
|
+
'mysql2': ['mysql', 'createConnection', 'createPool'],
|
|
173
|
+
'sqlite3': ['sqlite3'],
|
|
174
|
+
'express': ['app', 'express', 'router'],
|
|
175
|
+
'fastify': ['fastify'],
|
|
176
|
+
'koa': ['Koa', 'koa'],
|
|
177
|
+
'hapi': ['Hapi'],
|
|
178
|
+
'axios': ['axios'],
|
|
179
|
+
'node-fetch': ['fetch'],
|
|
180
|
+
'got': ['got'],
|
|
181
|
+
'superagent': ['request'],
|
|
182
|
+
'chalk': ['chalk'],
|
|
183
|
+
'ora': ['ora'],
|
|
184
|
+
'inquirer': ['inquirer'],
|
|
185
|
+
'commander': ['program', 'Command'],
|
|
186
|
+
'yargs': ['yargs'],
|
|
187
|
+
'minimist': ['argv'],
|
|
188
|
+
'dotenv': ['dotenv'],
|
|
189
|
+
'winston': ['winston', 'logger'],
|
|
190
|
+
'pino': ['pino', 'logger'],
|
|
191
|
+
'morgan': ['morgan'],
|
|
192
|
+
'helmet': ['helmet'],
|
|
193
|
+
'cors': ['cors'],
|
|
194
|
+
'compression': ['compression'],
|
|
195
|
+
'body-parser': ['bodyParser'],
|
|
196
|
+
'multer': ['multer', 'upload'],
|
|
197
|
+
'passport': ['passport'],
|
|
198
|
+
'jsonwebtoken': ['jwt'],
|
|
199
|
+
'bcrypt': ['bcrypt'],
|
|
200
|
+
'bcryptjs': ['bcrypt'],
|
|
201
|
+
'crypto-js': ['CryptoJS'],
|
|
202
|
+
'uuid': ['uuid', 'v4', 'uuidv4'],
|
|
203
|
+
'nanoid': ['nanoid'],
|
|
204
|
+
'zod': ['z', 'zod'],
|
|
205
|
+
'joi': ['Joi'],
|
|
206
|
+
'yup': ['yup'],
|
|
207
|
+
'valibot': ['v'],
|
|
208
|
+
'class-validator': ['IsEmail', 'IsString', 'IsNumber'],
|
|
209
|
+
'react': ['React'],
|
|
210
|
+
'react-dom': ['ReactDOM'],
|
|
211
|
+
'vue': ['Vue', 'createApp'],
|
|
212
|
+
'svelte': ['svelte'],
|
|
213
|
+
'@angular/core': ['Component', 'NgModule'],
|
|
214
|
+
'next': ['next'],
|
|
215
|
+
'nuxt': ['nuxt'],
|
|
216
|
+
};
|
|
31
217
|
|
|
32
218
|
function getGitIdentity() {
|
|
33
219
|
const identity = { name: "Developer", author: "Developer", repository: "" };
|
|
@@ -38,6 +224,10 @@ function getGitIdentity() {
|
|
|
38
224
|
identity.name = name;
|
|
39
225
|
identity.author = email ? `${name} <${email}>` : name;
|
|
40
226
|
}
|
|
227
|
+
try {
|
|
228
|
+
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
229
|
+
identity.repository = remoteUrl.replace(/\.git$/, '');
|
|
230
|
+
} catch (e) {}
|
|
41
231
|
} catch (e) {}
|
|
42
232
|
return identity;
|
|
43
233
|
}
|
|
@@ -47,11 +237,13 @@ function detectPackageManager(targetDir, stats = null) {
|
|
|
47
237
|
if (fs.existsSync(path.join(targetDir, 'pnpm-lock.yaml'))) detectedLockfiles.push('pnpm-lock.yaml');
|
|
48
238
|
if (fs.existsSync(path.join(targetDir, 'yarn.lock'))) detectedLockfiles.push('yarn.lock');
|
|
49
239
|
if (fs.existsSync(path.join(targetDir, 'package-lock.json'))) detectedLockfiles.push('package-lock.json');
|
|
240
|
+
if (fs.existsSync(path.join(targetDir, 'bun.lockb')) || fs.existsSync(path.join(targetDir, 'bun.lock'))) detectedLockfiles.push('bun.lock');
|
|
50
241
|
|
|
51
242
|
if (detectedLockfiles.length > 1 && stats) {
|
|
52
243
|
stats.conflictingLockfiles = detectedLockfiles;
|
|
53
244
|
}
|
|
54
245
|
|
|
246
|
+
if (detectedLockfiles.some(l => l.startsWith('bun'))) return 'bun';
|
|
55
247
|
if (detectedLockfiles.includes('pnpm-lock.yaml')) return 'pnpm';
|
|
56
248
|
if (detectedLockfiles.includes('yarn.lock')) return 'yarn';
|
|
57
249
|
if (detectedLockfiles.includes('package-lock.json')) return 'npm';
|
|
@@ -83,6 +275,22 @@ function analyzeCodeStyle(content, stats) {
|
|
|
83
275
|
if (REGEX_PATTERNS.syncFsCalls.test(content)) stats.quality.syncFsCount += (content.match(REGEX_PATTERNS.syncFsCalls) || []).length;
|
|
84
276
|
}
|
|
85
277
|
|
|
278
|
+
function getBinariesFromPackageJson(packageJsonContent) {
|
|
279
|
+
const binaries = new Set();
|
|
280
|
+
if (packageJsonContent && packageJsonContent.scripts) {
|
|
281
|
+
for (const script of Object.values(packageJsonContent.scripts)) {
|
|
282
|
+
const commands = String(script).split(/\s*&&\s*|\s*;\s*|\s*\|\|\s*/);
|
|
283
|
+
for (const cmd of commands) {
|
|
284
|
+
const firstWord = cmd.trim().split(/\s+/)[0];
|
|
285
|
+
if (firstWord && !['npm', 'yarn', 'pnpm', 'bun', 'node', 'npx', 'bunx', 'echo', 'exit', 'cd', 'mkdir', 'rm', 'cp', 'mv', 'cat', 'grep', 'sed', 'awk', 'find', 'sh', 'bash', 'zsh'].includes(firstWord)) {
|
|
286
|
+
binaries.add(firstWord);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return Array.from(binaries);
|
|
292
|
+
}
|
|
293
|
+
|
|
86
294
|
function cleanPackageName(importString) {
|
|
87
295
|
if (!importString || /^[./~\\]/.test(importString)) return null;
|
|
88
296
|
if (importString.startsWith('@')) return importString.split('/').slice(0, 2).join('/');
|
|
@@ -92,7 +300,7 @@ function cleanPackageName(importString) {
|
|
|
92
300
|
function smartPrepend(originalCode, declarationBlock) {
|
|
93
301
|
const lines = originalCode.split(/\r?\n/);
|
|
94
302
|
let insertIdx = 0;
|
|
95
|
-
|
|
303
|
+
|
|
96
304
|
while (insertIdx < lines.length) {
|
|
97
305
|
const line = lines[insertIdx].trim();
|
|
98
306
|
if (line.startsWith('#!') || line === '"use strict";' || line === "'use strict';" || line === '`use strict`;') {
|
|
@@ -103,7 +311,7 @@ function smartPrepend(originalCode, declarationBlock) {
|
|
|
103
311
|
break;
|
|
104
312
|
}
|
|
105
313
|
}
|
|
106
|
-
|
|
314
|
+
|
|
107
315
|
lines.splice(insertIdx, 0, declarationBlock);
|
|
108
316
|
return lines.join('\n');
|
|
109
317
|
}
|
|
@@ -111,16 +319,16 @@ function smartPrepend(originalCode, declarationBlock) {
|
|
|
111
319
|
async function inspectNpmPackage(pkgName) {
|
|
112
320
|
try {
|
|
113
321
|
const response = await fetch(`https://registry.npmjs.org/${pkgName}/latest`, {
|
|
114
|
-
headers: { 'User-Agent': 'pkg-scaffold-dx-client/
|
|
322
|
+
headers: { 'User-Agent': 'pkg-scaffold-dx-client/2.0' },
|
|
115
323
|
signal: AbortSignal.timeout(4000)
|
|
116
324
|
});
|
|
117
325
|
if (response.status === 200) {
|
|
118
326
|
const data = await response.json();
|
|
119
|
-
return { version: data.version, error: null };
|
|
327
|
+
return { version: data.version, deprecated: data.deprecated || null, error: null };
|
|
120
328
|
}
|
|
121
|
-
if (response.status === 404) return { version: null, error: 'NOT_FOUND' };
|
|
329
|
+
if (response.status === 404) return { version: null, deprecated: null, error: 'NOT_FOUND' };
|
|
122
330
|
} catch (e) {
|
|
123
|
-
return { version: 'latest', error: 'NETWORK_FAIL' };
|
|
331
|
+
return { version: 'latest', deprecated: null, error: 'NETWORK_FAIL' };
|
|
124
332
|
}
|
|
125
333
|
return null;
|
|
126
334
|
}
|
|
@@ -128,7 +336,7 @@ async function inspectNpmPackage(pkgName) {
|
|
|
128
336
|
async function fetchRemoteLicense(licenseKey) {
|
|
129
337
|
try {
|
|
130
338
|
const response = await fetch(`https://api.github.com/licenses/${licenseKey.toLowerCase()}`, {
|
|
131
|
-
headers: { 'User-Agent': 'pkg-scaffold-dx-client/
|
|
339
|
+
headers: { 'User-Agent': 'pkg-scaffold-dx-client/2.0' },
|
|
132
340
|
signal: AbortSignal.timeout(5000)
|
|
133
341
|
});
|
|
134
342
|
if (response.status === 200) {
|
|
@@ -151,12 +359,12 @@ function buildAsciiTree(dir, prefix = '') {
|
|
|
151
359
|
try {
|
|
152
360
|
const files = fs.readdirSync(dir);
|
|
153
361
|
const filtered = files.filter(f => !IGNORED_DIRS.has(f) && !f.startsWith('.'));
|
|
154
|
-
|
|
362
|
+
|
|
155
363
|
filtered.forEach((file, index) => {
|
|
156
364
|
const isLast = index === filtered.length - 1;
|
|
157
365
|
const marker = isLast ? 'āāā ' : 'āāā ';
|
|
158
366
|
results.push(`${prefix}${marker}${file}`);
|
|
159
|
-
|
|
367
|
+
|
|
160
368
|
const fullPath = path.join(dir, file);
|
|
161
369
|
if (fs.statSync(fullPath).isDirectory()) {
|
|
162
370
|
const newPrefix = prefix + (isLast ? ' ' : 'ā ');
|
|
@@ -167,7 +375,295 @@ function buildAsciiTree(dir, prefix = '') {
|
|
|
167
375
|
return results;
|
|
168
376
|
}
|
|
169
377
|
|
|
170
|
-
//
|
|
378
|
+
// ============================================================
|
|
379
|
+
// IMPROVED IMPORT EXTRACTION: handles TypeScript generics,
|
|
380
|
+
// type-only imports, re-exports, and dynamic imports
|
|
381
|
+
// ============================================================
|
|
382
|
+
function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLocations) {
|
|
383
|
+
walk.simple(ast, {
|
|
384
|
+
ImportDeclaration(node) {
|
|
385
|
+
const pkg = cleanPackageName(node.source.value);
|
|
386
|
+
if (pkg && !builtinModules.includes(pkg)) {
|
|
387
|
+
fileRawDeps.add(pkg);
|
|
388
|
+
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
389
|
+
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
390
|
+
importedLocations.get(pkg).push(node.loc?.start?.line ?? 0);
|
|
391
|
+
|
|
392
|
+
node.specifiers.forEach(spec => {
|
|
393
|
+
if (spec.type === 'ImportDefaultSpecifier' || spec.type === 'ImportNamespaceSpecifier') {
|
|
394
|
+
importedIdentifiers.get(pkg).add(spec.local.name);
|
|
395
|
+
} else if (spec.type === 'ImportSpecifier') {
|
|
396
|
+
importedIdentifiers.get(pkg).add(spec.local.name);
|
|
397
|
+
// Also track the imported name (before 'as') for side-effect detection
|
|
398
|
+
if (spec.imported && spec.imported.name !== spec.local.name) {
|
|
399
|
+
importedIdentifiers.get(pkg).add(spec.imported.name);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Side-effect only import: import 'pkg' ā always considered "used"
|
|
405
|
+
if (node.specifiers.length === 0) {
|
|
406
|
+
importedIdentifiers.get(pkg).add('__SIDE_EFFECT__');
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
VariableDeclarator(node) {
|
|
411
|
+
if (node.init && node.init.type === 'CallExpression' &&
|
|
412
|
+
node.init.callee.type === 'Identifier' && node.init.callee.name === 'require') {
|
|
413
|
+
const arg = node.init.arguments[0];
|
|
414
|
+
if (arg && arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
415
|
+
const pkg = cleanPackageName(arg.value);
|
|
416
|
+
if (pkg && !builtinModules.includes(pkg)) {
|
|
417
|
+
fileRawDeps.add(pkg);
|
|
418
|
+
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
419
|
+
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
420
|
+
importedLocations.get(pkg).push(node.loc?.start?.line ?? 0);
|
|
421
|
+
|
|
422
|
+
const extractBindings = (idNode) => {
|
|
423
|
+
if (idNode.type === 'Identifier') {
|
|
424
|
+
importedIdentifiers.get(pkg).add(idNode.name);
|
|
425
|
+
} else if (idNode.type === 'ObjectPattern') {
|
|
426
|
+
idNode.properties.forEach(p => {
|
|
427
|
+
if (p.value && p.value.type === 'Identifier') importedIdentifiers.get(pkg).add(p.value.name);
|
|
428
|
+
if (p.key && p.key.type === 'Identifier') importedIdentifiers.get(pkg).add(p.key.name);
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
extractBindings(node.id);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
ImportExpression(node) {
|
|
438
|
+
if (node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
439
|
+
const pkg = cleanPackageName(node.source.value);
|
|
440
|
+
if (pkg && !builtinModules.includes(pkg)) {
|
|
441
|
+
fileRawDeps.add(pkg);
|
|
442
|
+
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
443
|
+
importedIdentifiers.get(pkg).add('__DYNAMIC__');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
ExportNamedDeclaration(node) {
|
|
448
|
+
if (node.source && node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
449
|
+
const pkg = cleanPackageName(node.source.value);
|
|
450
|
+
if (pkg && !builtinModules.includes(pkg)) {
|
|
451
|
+
fileRawDeps.add(pkg);
|
|
452
|
+
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
453
|
+
importedIdentifiers.get(pkg).add('__REEXPORT__');
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
ExportAllDeclaration(node) {
|
|
458
|
+
if (node.source && node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
459
|
+
const pkg = cleanPackageName(node.source.value);
|
|
460
|
+
if (pkg && !builtinModules.includes(pkg)) {
|
|
461
|
+
fileRawDeps.add(pkg);
|
|
462
|
+
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
463
|
+
importedIdentifiers.get(pkg).add('__REEXPORT__');
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ============================================================
|
|
471
|
+
// REGEX FALLBACK: handles TypeScript files that acorn can't parse
|
|
472
|
+
// ============================================================
|
|
473
|
+
function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, importedLocations) {
|
|
474
|
+
codeLines.forEach((line, lineIdx) => {
|
|
475
|
+
const lineNum = lineIdx + 1;
|
|
476
|
+
|
|
477
|
+
// import type { ... } from '...' ā type-only, mark as side-effect
|
|
478
|
+
const typeImportMatch = line.match(/\bimport\s+type\s+\{[^}]*\}\s+from\s+['"]([^'"]+)['"]/);
|
|
479
|
+
if (typeImportMatch) {
|
|
480
|
+
const pkg = cleanPackageName(typeImportMatch[1]);
|
|
481
|
+
if (pkg && !builtinModules.includes(pkg)) {
|
|
482
|
+
fileRawDeps.add(pkg);
|
|
483
|
+
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
484
|
+
importedIdentifiers.get(pkg).add('__TYPE_ONLY__');
|
|
485
|
+
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
486
|
+
importedLocations.get(pkg).push(lineNum);
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// import DefaultExport from '...'
|
|
492
|
+
// import * as Namespace from '...'
|
|
493
|
+
const esmDefaultMatch = line.match(/\bimport\s+(?:\*\s+as\s+)?([a-zA-Z0-9_$]+)\s+from\s+['"]([^'"]+)['"]/);
|
|
494
|
+
if (esmDefaultMatch) {
|
|
495
|
+
const id = esmDefaultMatch[1];
|
|
496
|
+
const pkg = cleanPackageName(esmDefaultMatch[2]);
|
|
497
|
+
if (pkg && !builtinModules.includes(pkg)) {
|
|
498
|
+
fileRawDeps.add(pkg);
|
|
499
|
+
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
500
|
+
importedIdentifiers.get(pkg).add(id);
|
|
501
|
+
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
502
|
+
importedLocations.get(pkg).push(lineNum);
|
|
503
|
+
}
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// import { named, exports } from '...'
|
|
508
|
+
const esmNamedMatch = line.match(/\bimport\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/);
|
|
509
|
+
if (esmNamedMatch) {
|
|
510
|
+
const pkg = cleanPackageName(esmNamedMatch[2]);
|
|
511
|
+
if (pkg && !builtinModules.includes(pkg)) {
|
|
512
|
+
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
513
|
+
fileRawDeps.add(pkg);
|
|
514
|
+
esmNamedMatch[1].split(',').forEach(part => {
|
|
515
|
+
const chunk = part.trim();
|
|
516
|
+
if (!chunk) return;
|
|
517
|
+
const id = chunk.includes(' as ') ? chunk.split(' as ')[1].trim() : chunk;
|
|
518
|
+
importedIdentifiers.get(pkg).add(id);
|
|
519
|
+
// Also add the original name
|
|
520
|
+
if (chunk.includes(' as ')) importedIdentifiers.get(pkg).add(chunk.split(' as ')[0].trim());
|
|
521
|
+
});
|
|
522
|
+
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
523
|
+
importedLocations.get(pkg).push(lineNum);
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Side-effect only: import '...'
|
|
529
|
+
const sideEffectMatch = line.match(/\bimport\s+['"]([^'"]+)['"]/);
|
|
530
|
+
if (sideEffectMatch) {
|
|
531
|
+
const pkg = cleanPackageName(sideEffectMatch[1]);
|
|
532
|
+
if (pkg && !builtinModules.includes(pkg)) {
|
|
533
|
+
fileRawDeps.add(pkg);
|
|
534
|
+
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
535
|
+
importedIdentifiers.get(pkg).add('__SIDE_EFFECT__');
|
|
536
|
+
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
537
|
+
importedLocations.get(pkg).push(lineNum);
|
|
538
|
+
}
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// const x = require('...')
|
|
543
|
+
const cjsMatch = line.match(/\b(?:const|let|var)\s+([a-zA-Z0-9_$]+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
544
|
+
if (cjsMatch) {
|
|
545
|
+
const id = cjsMatch[1];
|
|
546
|
+
const pkg = cleanPackageName(cjsMatch[2]);
|
|
547
|
+
if (pkg && !builtinModules.includes(pkg)) {
|
|
548
|
+
fileRawDeps.add(pkg);
|
|
549
|
+
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
550
|
+
importedIdentifiers.get(pkg).add(id);
|
|
551
|
+
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
552
|
+
importedLocations.get(pkg).push(lineNum);
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// const { a, b } = require('...')
|
|
558
|
+
const cjsDestructMatch = line.match(/\b(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
559
|
+
if (cjsDestructMatch) {
|
|
560
|
+
const pkg = cleanPackageName(cjsDestructMatch[2]);
|
|
561
|
+
if (pkg && !builtinModules.includes(pkg)) {
|
|
562
|
+
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
563
|
+
fileRawDeps.add(pkg);
|
|
564
|
+
cjsDestructMatch[1].split(',').forEach(part => {
|
|
565
|
+
const chunk = part.trim();
|
|
566
|
+
if (!chunk) return;
|
|
567
|
+
const id = chunk.includes(':') ? chunk.split(':')[1].trim() : chunk;
|
|
568
|
+
importedIdentifiers.get(pkg).add(id);
|
|
569
|
+
});
|
|
570
|
+
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
571
|
+
importedLocations.get(pkg).push(lineNum);
|
|
572
|
+
}
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Dynamic import: import('...')
|
|
577
|
+
const dynamicMatch = line.match(/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
578
|
+
if (dynamicMatch) {
|
|
579
|
+
const pkg = cleanPackageName(dynamicMatch[1]);
|
|
580
|
+
if (pkg && !builtinModules.includes(pkg)) {
|
|
581
|
+
fileRawDeps.add(pkg);
|
|
582
|
+
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
583
|
+
importedIdentifiers.get(pkg).add('__DYNAMIC__');
|
|
584
|
+
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
585
|
+
importedLocations.get(pkg).push(lineNum);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ============================================================
|
|
592
|
+
// USAGE ANALYSIS: determines if imported identifiers are
|
|
593
|
+
// actually referenced in the non-import code body
|
|
594
|
+
// ============================================================
|
|
595
|
+
function analyzeIdentifierUsage(pkg, identifiers, executionCode) {
|
|
596
|
+
// Always-used markers: side-effect, dynamic, re-export, type-only
|
|
597
|
+
const autoUsedMarkers = new Set(['__SIDE_EFFECT__', '__DYNAMIC__', '__REEXPORT__', '__TYPE_ONLY__']);
|
|
598
|
+
for (const id of identifiers) {
|
|
599
|
+
if (autoUsedMarkers.has(id)) return true;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Check known aliases for this package
|
|
603
|
+
const knownAliases = PACKAGE_IMPORT_ALIASES[pkg] || [];
|
|
604
|
+
|
|
605
|
+
for (const identifier of identifiers) {
|
|
606
|
+
if (!identifier || identifier.startsWith('__')) continue;
|
|
607
|
+
// Escape special regex chars in identifier
|
|
608
|
+
const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
609
|
+
const usagePattern = new RegExp(`\\b${escaped}\\b`);
|
|
610
|
+
if (usagePattern.test(executionCode)) return true;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Check if any known alias for this package appears in the code
|
|
614
|
+
for (const alias of knownAliases) {
|
|
615
|
+
const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
616
|
+
const aliasPattern = new RegExp(`\\b${escaped}\\b`);
|
|
617
|
+
if (aliasPattern.test(executionCode)) return true;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ============================================================
|
|
624
|
+
// GHOST DEPENDENCY DETECTOR
|
|
625
|
+
// Finds packages used in code but NOT declared in package.json
|
|
626
|
+
// This is the most critical error: will fail at runtime/deploy
|
|
627
|
+
// ============================================================
|
|
628
|
+
function detectGhostDependencies(allImportedPackages, declaredDeps, declaredDevDeps) {
|
|
629
|
+
const allDeclared = new Set([...declaredDeps, ...declaredDevDeps]);
|
|
630
|
+
const ghosts = new Set();
|
|
631
|
+
|
|
632
|
+
for (const pkg of allImportedPackages) {
|
|
633
|
+
if (!allDeclared.has(pkg) && !builtinModules.includes(pkg)) {
|
|
634
|
+
ghosts.add(pkg);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return ghosts;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ============================================================
|
|
641
|
+
// ORPHANED DEPENDENCY DETECTOR
|
|
642
|
+
// Finds packages in package.json that are never imported anywhere
|
|
643
|
+
// in the codebase (candidates for removal)
|
|
644
|
+
// ============================================================
|
|
645
|
+
function detectOrphanedDependencies(declaredDeps, allImportedPackages, binariesUsed, devTooling) {
|
|
646
|
+
const orphans = new Set();
|
|
647
|
+
|
|
648
|
+
for (const dep of declaredDeps) {
|
|
649
|
+
// Skip dev tooling ā they're used via CLI, not imports
|
|
650
|
+
if (devTooling.has(dep) || dep.startsWith('@types/')) continue;
|
|
651
|
+
|
|
652
|
+
// Check if it's used as a binary
|
|
653
|
+
const binaryPkg = Object.values(BINARY_TO_PACKAGE_MAP).find(p => p === dep);
|
|
654
|
+
if (binaryPkg && binariesUsed.has(dep)) continue;
|
|
655
|
+
|
|
656
|
+
// Check if it's imported anywhere
|
|
657
|
+
if (!allImportedPackages.has(dep)) {
|
|
658
|
+
orphans.add(dep);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return orphans;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ============================================================
|
|
665
|
+
// HIGH PERFORMANCE AST WORKSPACE PARSING ENGINE
|
|
666
|
+
// ============================================================
|
|
171
667
|
function scanWorkspace(dir, stats, rootNamespace) {
|
|
172
668
|
const files = fs.readdirSync(dir);
|
|
173
669
|
|
|
@@ -176,21 +672,26 @@ function scanWorkspace(dir, stats, rootNamespace) {
|
|
|
176
672
|
const stat = fs.statSync(fullPath);
|
|
177
673
|
|
|
178
674
|
if (stat.isDirectory()) {
|
|
179
|
-
if (!IGNORED_DIRS.has(file) && !file.startsWith('.'))
|
|
675
|
+
if (!IGNORED_DIRS.has(file) && !file.startsWith('.')) {
|
|
676
|
+
scanWorkspace(fullPath, stats, rootNamespace);
|
|
677
|
+
}
|
|
180
678
|
} else {
|
|
181
679
|
const ext = path.extname(file);
|
|
182
|
-
|
|
183
|
-
if (file === 'index.html' ||
|
|
680
|
+
|
|
681
|
+
if (file === 'index.html' || REGEX_PATTERNS.configFile.test(file)) stats.hasHtml = true;
|
|
184
682
|
if (REGEX_PATTERNS.testFile.test(file)) stats.hasTests = true;
|
|
185
683
|
if (ext === '.ts' || ext === '.tsx') stats.tsFiles++;
|
|
186
684
|
if (ext === '.js' || ext === '.jsx' || ext === '.mjs') stats.jsFiles++;
|
|
187
685
|
|
|
188
686
|
if (VALID_EXTENSIONS.has(ext)) {
|
|
687
|
+
stats.scannedFiles++;
|
|
189
688
|
const rawContent = readFileSyncNormalized(fullPath);
|
|
190
|
-
|
|
191
|
-
|
|
689
|
+
// Strip non-printable chars but keep Unicode letters (important for identifiers)
|
|
690
|
+
const content = rawContent.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
691
|
+
|
|
192
692
|
const codeLines = content.split(/\r?\n/);
|
|
193
|
-
const importedIdentifiers = new Map();
|
|
693
|
+
const importedIdentifiers = new Map();
|
|
694
|
+
const importedLocations = new Map();
|
|
194
695
|
const fileRawDeps = new Set();
|
|
195
696
|
|
|
196
697
|
analyzeCodeStyle(content, stats);
|
|
@@ -206,7 +707,7 @@ function scanWorkspace(dir, stats, rootNamespace) {
|
|
|
206
707
|
stats.envVars.add(envVarName);
|
|
207
708
|
}
|
|
208
709
|
|
|
209
|
-
//
|
|
710
|
+
// Global Regex Environmental Extraction Module
|
|
210
711
|
let fileHasEnv = false;
|
|
211
712
|
let envMatch;
|
|
212
713
|
REGEX_PATTERNS.env.lastIndex = 0;
|
|
@@ -214,164 +715,51 @@ function scanWorkspace(dir, stats, rootNamespace) {
|
|
|
214
715
|
stats.envVars.add(envMatch[1]);
|
|
215
716
|
fileHasEnv = true;
|
|
216
717
|
}
|
|
217
|
-
if (fileHasEnv)
|
|
218
|
-
stats.filesWithEnvVars.add(fullPath);
|
|
219
|
-
}
|
|
718
|
+
if (fileHasEnv) stats.filesWithEnvVars.add(fullPath);
|
|
220
719
|
|
|
221
720
|
if (content.includes('import ') || content.includes('export ')) stats.usesEsm = true;
|
|
222
721
|
|
|
223
|
-
// ---
|
|
722
|
+
// --- AST Parsing (preferred) ---
|
|
224
723
|
let ast = null;
|
|
225
724
|
try {
|
|
226
|
-
ast = acorn.parse(content, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true });
|
|
725
|
+
ast = acorn.parse(content, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true, locations: true });
|
|
227
726
|
} catch (e) {
|
|
228
727
|
try {
|
|
229
|
-
ast = acorn.parse(content, { ecmaVersion: 'latest', sourceType: 'script', allowHashBang: true });
|
|
728
|
+
ast = acorn.parse(content, { ecmaVersion: 'latest', sourceType: 'script', allowHashBang: true, locations: true });
|
|
230
729
|
} catch (err) {}
|
|
231
730
|
}
|
|
232
731
|
|
|
233
732
|
if (ast) {
|
|
234
|
-
|
|
235
|
-
ImportDeclaration(node) {
|
|
236
|
-
const pkg = cleanPackageName(node.source.value);
|
|
237
|
-
if (pkg && !builtinModules.includes(pkg)) {
|
|
238
|
-
fileRawDeps.add(pkg);
|
|
239
|
-
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
240
|
-
node.specifiers.forEach(spec => importedIdentifiers.get(pkg).add(spec.local.name));
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
VariableDeclarator(node) {
|
|
244
|
-
if (node.init && node.init.type === 'CallExpression' &&
|
|
245
|
-
node.init.callee.type === 'Identifier' && node.init.callee.name === 'require') {
|
|
246
|
-
const arg = node.init.arguments[0];
|
|
247
|
-
if (arg && arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
248
|
-
const pkg = cleanPackageName(arg.value);
|
|
249
|
-
if (pkg && !builtinModules.includes(pkg)) {
|
|
250
|
-
fileRawDeps.add(pkg);
|
|
251
|
-
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
252
|
-
|
|
253
|
-
const extractBindings = (idNode) => {
|
|
254
|
-
if (idNode.type === 'Identifier') {
|
|
255
|
-
importedIdentifiers.get(pkg).add(idNode.name);
|
|
256
|
-
} else if (idNode.type === 'ObjectPattern') {
|
|
257
|
-
idNode.properties.forEach(p => {
|
|
258
|
-
if (p.value && p.value.type === 'Identifier') importedIdentifiers.get(pkg).add(p.value.name);
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
extractBindings(node.id);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
},
|
|
267
|
-
ImportExpression(node) {
|
|
268
|
-
if (node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
269
|
-
const pkg = cleanPackageName(node.source.value);
|
|
270
|
-
if (pkg && !builtinModules.includes(pkg)) fileRawDeps.add(pkg);
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
ExportNamedDeclaration(node) {
|
|
274
|
-
if (node.source && node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
275
|
-
const pkg = cleanPackageName(node.source.value);
|
|
276
|
-
if (pkg && !builtinModules.includes(pkg)) fileRawDeps.add(pkg);
|
|
277
|
-
}
|
|
278
|
-
},
|
|
279
|
-
ExportAllDeclaration(node) {
|
|
280
|
-
if (node.source && node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
281
|
-
const pkg = cleanPackageName(node.source.value);
|
|
282
|
-
if (pkg && !builtinModules.includes(pkg)) fileRawDeps.add(pkg);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
});
|
|
733
|
+
extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLocations);
|
|
286
734
|
} else {
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
let esmMatch = line.match(/\bimport\s+(?:\*+\s+as\s+)?([a-zA-Z0-9_]+)\s+from\s+['"]([^'"]+)['"]/);
|
|
290
|
-
if (esmMatch) {
|
|
291
|
-
const id = esmMatch[1];
|
|
292
|
-
const pkg = cleanPackageName(esmMatch[2]);
|
|
293
|
-
if (pkg && !builtinModules.includes(pkg)) {
|
|
294
|
-
fileRawDeps.add(pkg);
|
|
295
|
-
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
296
|
-
importedIdentifiers.get(pkg).add(id);
|
|
297
|
-
}
|
|
298
|
-
continue;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
let esmNamedMatch = line.match(/\bimport\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/);
|
|
302
|
-
if (esmNamedMatch) {
|
|
303
|
-
const pkg = cleanPackageName(esmNamedMatch[2]);
|
|
304
|
-
if (pkg && !builtinModules.includes(pkg)) {
|
|
305
|
-
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
306
|
-
fileRawDeps.add(pkg);
|
|
307
|
-
esmNamedMatch[1].split(',').forEach(part => {
|
|
308
|
-
const chunk = part.trim();
|
|
309
|
-
if (!chunk) return;
|
|
310
|
-
const id = chunk.includes(' as ') ? chunk.split(' as ')[1].trim() : chunk;
|
|
311
|
-
importedIdentifiers.get(pkg).add(id);
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
let cjsMatch = line.match(/\b(?:const|let|var)\s+([a-zA-Z0-9_]+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
318
|
-
if (cjsMatch) {
|
|
319
|
-
const id = cjsMatch[1];
|
|
320
|
-
const pkg = cleanPackageName(cjsMatch[2]);
|
|
321
|
-
if (pkg && !builtinModules.includes(pkg)) {
|
|
322
|
-
fileRawDeps.add(pkg);
|
|
323
|
-
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
324
|
-
importedIdentifiers.get(pkg).add(id);
|
|
325
|
-
}
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
let cjsDestructMatch = line.match(/\b(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
330
|
-
if (cjsDestructMatch) {
|
|
331
|
-
const pkg = cleanPackageName(cjsDestructMatch[2]);
|
|
332
|
-
if (pkg && !builtinModules.includes(pkg)) {
|
|
333
|
-
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
334
|
-
fileRawDeps.add(pkg);
|
|
335
|
-
cjsDestructMatch[1].split(',').forEach(part => {
|
|
336
|
-
const chunk = part.trim();
|
|
337
|
-
if (!chunk) return;
|
|
338
|
-
const id = chunk.includes(':') ? chunk.split(':')[1].trim() : chunk;
|
|
339
|
-
importedIdentifiers.get(pkg).add(id);
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
735
|
+
// Regex fallback for TypeScript generics / decorators / etc.
|
|
736
|
+
extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, importedLocations);
|
|
345
737
|
}
|
|
346
738
|
|
|
739
|
+
// Register all deps found in this file
|
|
740
|
+
fileRawDeps.forEach(dep => stats.allImportedPackages.add(dep));
|
|
347
741
|
fileRawDeps.forEach(dep => stats.rawDeps.add(dep));
|
|
348
742
|
|
|
349
|
-
|
|
350
|
-
|
|
743
|
+
// --- Per-file usage analysis ---
|
|
744
|
+
// Strip import/require lines to get only execution code
|
|
745
|
+
const executionCode = codeLines
|
|
746
|
+
.filter(l => {
|
|
747
|
+
const t = l.trim();
|
|
748
|
+
return !t.startsWith('import ') && !/\brequire\s*\(/.test(t);
|
|
749
|
+
})
|
|
351
750
|
.join('\n');
|
|
352
751
|
|
|
353
752
|
for (const [pkg, identifiers] of importedIdentifiers.entries()) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
tokenReferenced = true;
|
|
359
|
-
break;
|
|
753
|
+
const isUsed = analyzeIdentifierUsage(pkg, identifiers, executionCode);
|
|
754
|
+
if (!isUsed && identifiers.size > 0) {
|
|
755
|
+
if (!stats.unusedImportsPerFile.has(fullPath)) {
|
|
756
|
+
stats.unusedImportsPerFile.set(fullPath, new Map());
|
|
360
757
|
}
|
|
361
|
-
|
|
362
|
-
|
|
758
|
+
const lines = importedLocations.get(pkg) || [];
|
|
759
|
+
stats.unusedImportsPerFile.get(fullPath).set(pkg, lines);
|
|
363
760
|
stats.unusedDepsInCode.add(pkg);
|
|
364
761
|
}
|
|
365
762
|
}
|
|
366
|
-
|
|
367
|
-
COMMON_EXTERNAL_TOKENS.forEach(token => {
|
|
368
|
-
const tokenPattern = new RegExp(`\\b${token}\\b`);
|
|
369
|
-
if (tokenPattern.test(functionalExecutionCodeOnly) && !importedIdentifiers.has(token)) {
|
|
370
|
-
stats.rawDeps.add(token);
|
|
371
|
-
if (!stats.phantomInjections.has(fullPath)) stats.phantomInjections.set(fullPath, new Set());
|
|
372
|
-
stats.phantomInjections.get(fullPath).add(token);
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
763
|
}
|
|
376
764
|
}
|
|
377
765
|
}
|
|
@@ -381,12 +769,21 @@ async function main() {
|
|
|
381
769
|
const targetDir = process.cwd();
|
|
382
770
|
const folderName = path.basename(targetDir);
|
|
383
771
|
const gitInfo = getGitIdentity();
|
|
384
|
-
|
|
772
|
+
|
|
385
773
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
386
|
-
|
|
774
|
+
let rlClosed = false;
|
|
775
|
+
rl.on('close', () => { rlClosed = true; });
|
|
776
|
+
const safeQuestion = async (prompt) => {
|
|
777
|
+
if (rlClosed || !process.stdin.readable) return '';
|
|
778
|
+
try { return await safeQuestion(prompt); } catch { return ''; }
|
|
779
|
+
};
|
|
780
|
+
|
|
387
781
|
const stats = {
|
|
388
782
|
tsFiles: 0, jsFiles: 0, usesEsm: false, hasHtml: false, hasTests: false,
|
|
389
|
-
|
|
783
|
+
scannedFiles: 0,
|
|
784
|
+
rawDeps: new Set(),
|
|
785
|
+
allImportedPackages: new Set(),
|
|
786
|
+
envVars: new Set(),
|
|
390
787
|
style: { semiCount: 0, noSemiCount: 0, tabCount: 0, space2Count: 0, space4Count: 0 },
|
|
391
788
|
quality: { varCount: 0, hasEval: false, syncFsCount: 0 },
|
|
392
789
|
phantomInjections: new Map(),
|
|
@@ -394,20 +791,28 @@ async function main() {
|
|
|
394
791
|
subWorkspaces: [],
|
|
395
792
|
conflictingLockfiles: [],
|
|
396
793
|
unusedDepsInCode: new Set(),
|
|
794
|
+
unusedImportsPerFile: new Map(),
|
|
397
795
|
filesWithEnvVars: new Set(),
|
|
398
796
|
injectDotenvEngine: false,
|
|
399
|
-
bootstrapEslintSuite: false
|
|
797
|
+
bootstrapEslintSuite: false,
|
|
798
|
+
// New tracking structures
|
|
799
|
+
ghostDependencies: new Set(), // used in code, missing from package.json
|
|
800
|
+
orphanedDependencies: new Set(), // in package.json, never imported
|
|
801
|
+
deprecatedPackages: new Map(), // pkg -> deprecation message
|
|
400
802
|
};
|
|
401
803
|
|
|
402
804
|
const activePkgManager = detectPackageManager(targetDir, stats);
|
|
403
805
|
const pkgPath = path.join(targetDir, 'package.json');
|
|
404
806
|
let preExistingLicense = null;
|
|
405
807
|
let preExistingDeps = [];
|
|
808
|
+
let preExistingDevDeps = [];
|
|
809
|
+
let existingPackageJson = null;
|
|
406
810
|
|
|
407
|
-
console.log(`\n
|
|
408
|
-
console.log(`š pkg-scaffold
|
|
409
|
-
console.log(
|
|
811
|
+
console.log(`\n${'ā'.repeat(67)}`);
|
|
812
|
+
console.log(`š pkg-scaffold v2.0: Advanced Dependency Intelligence Engine`);
|
|
813
|
+
console.log(`${'ā'.repeat(67)}\n`);
|
|
410
814
|
|
|
815
|
+
// --- Sub-workspace detection ---
|
|
411
816
|
const topLevelItems = fs.readdirSync(targetDir);
|
|
412
817
|
const potentialSubModules = [];
|
|
413
818
|
for (const item of topLevelItems) {
|
|
@@ -415,45 +820,54 @@ async function main() {
|
|
|
415
820
|
if (!IGNORED_DIRS.has(item) && !item.startsWith('.') && fs.statSync(fullPath).isDirectory()) {
|
|
416
821
|
let containsSourceCode = false;
|
|
417
822
|
const examineDirectory = (d) => {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
if (
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
823
|
+
try {
|
|
824
|
+
const subEntries = fs.readdirSync(d);
|
|
825
|
+
for (const entry of subEntries) {
|
|
826
|
+
const entryPath = path.join(d, entry);
|
|
827
|
+
if (fs.statSync(entryPath).isDirectory()) {
|
|
828
|
+
if (!IGNORED_DIRS.has(entry) && !entry.startsWith('.')) examineDirectory(entryPath);
|
|
829
|
+
} else if (VALID_EXTENSIONS.has(path.extname(entry))) {
|
|
830
|
+
containsSourceCode = true;
|
|
831
|
+
}
|
|
426
832
|
}
|
|
427
|
-
}
|
|
833
|
+
} catch {}
|
|
428
834
|
};
|
|
429
|
-
|
|
835
|
+
examineDirectory(fullPath);
|
|
430
836
|
if (containsSourceCode) potentialSubModules.push(item);
|
|
431
837
|
}
|
|
432
838
|
}
|
|
433
|
-
if (potentialSubModules.length > 1)
|
|
434
|
-
stats.subWorkspaces = potentialSubModules;
|
|
435
|
-
}
|
|
839
|
+
if (potentialSubModules.length > 1) stats.subWorkspaces = potentialSubModules;
|
|
436
840
|
|
|
841
|
+
// --- Existing package.json analysis ---
|
|
437
842
|
if (fs.existsSync(pkgPath)) {
|
|
438
843
|
console.log(`ā ļø An existing package.json was found in this working directory.`);
|
|
439
844
|
console.log(`š” Analyzing existing installation arrays for invalid metrics...`);
|
|
440
845
|
try {
|
|
441
|
-
|
|
442
|
-
if (
|
|
443
|
-
preExistingLicense =
|
|
846
|
+
existingPackageJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
847
|
+
if (existingPackageJson.license && typeof existingPackageJson.license === 'string' && existingPackageJson.license.toLowerCase() !== 'none') {
|
|
848
|
+
preExistingLicense = existingPackageJson.license;
|
|
444
849
|
}
|
|
445
|
-
if (
|
|
446
|
-
if (
|
|
850
|
+
if (existingPackageJson.dependencies) preExistingDeps = Object.keys(existingPackageJson.dependencies);
|
|
851
|
+
if (existingPackageJson.devDependencies) preExistingDevDeps = Object.keys(existingPackageJson.devDependencies);
|
|
447
852
|
|
|
448
|
-
const combinedDeps =
|
|
853
|
+
const combinedDeps = [...preExistingDeps, ...preExistingDevDeps];
|
|
449
854
|
let brokenEcosystem = combinedDeps.length === 0;
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
855
|
+
|
|
856
|
+
// Check for non-existent AND deprecated packages
|
|
857
|
+
if (combinedDeps.length > 0) {
|
|
858
|
+
console.log(` š Validating ${combinedDeps.length} declared package(s) against npm registry...`);
|
|
859
|
+
for (const dep of combinedDeps) {
|
|
860
|
+
const check = await inspectNpmPackage(dep);
|
|
861
|
+
if (check && check.error === 'NOT_FOUND') {
|
|
862
|
+
brokenEcosystem = true;
|
|
863
|
+
console.log(` ā Non-existent package on registry: "${dep}"`);
|
|
864
|
+
} else if (check && check.deprecated) {
|
|
865
|
+
stats.deprecatedPackages.set(dep, check.deprecated);
|
|
866
|
+
console.log(` ā ļø Deprecated package detected: "${dep}" ā ${check.deprecated}`);
|
|
867
|
+
}
|
|
455
868
|
}
|
|
456
869
|
}
|
|
870
|
+
|
|
457
871
|
if (brokenEcosystem) {
|
|
458
872
|
console.log(`\nš CRITICAL COMPLIANCE BREAK: Your current package.json is empty or contains non-existent packages.`);
|
|
459
873
|
console.log(`š Action Required: Please remove or backup the existing 'package.json' from this folder.\n`);
|
|
@@ -467,54 +881,255 @@ async function main() {
|
|
|
467
881
|
}
|
|
468
882
|
}
|
|
469
883
|
|
|
884
|
+
// --- Workspace scan ---
|
|
885
|
+
console.log(`\nš¬ Scanning workspace source files...`);
|
|
470
886
|
scanWorkspace(targetDir, stats, folderName);
|
|
887
|
+
console.log(` ā
Scanned ${stats.scannedFiles} source file(s) | TS: ${stats.tsFiles} | JS: ${stats.jsFiles}`);
|
|
888
|
+
|
|
889
|
+
// --- Binary-to-package resolution ---
|
|
890
|
+
const binariesInScripts = existingPackageJson ? getBinariesFromPackageJson(existingPackageJson) : [];
|
|
891
|
+
const resolvedBinaryPackages = new Set();
|
|
892
|
+
for (const binary of binariesInScripts) {
|
|
893
|
+
const pkgName = BINARY_TO_PACKAGE_MAP[binary] || binary;
|
|
894
|
+
resolvedBinaryPackages.add(pkgName);
|
|
895
|
+
stats.rawDeps.add(pkgName);
|
|
896
|
+
stats.allImportedPackages.add(pkgName); // treat as "used"
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// ============================================================
|
|
900
|
+
// GHOST DEPENDENCY ANALYSIS
|
|
901
|
+
// Packages imported in code but missing from package.json
|
|
902
|
+
// ============================================================
|
|
903
|
+
if (preExistingDeps.length > 0 || preExistingDevDeps.length > 0) {
|
|
904
|
+
stats.ghostDependencies = detectGhostDependencies(
|
|
905
|
+
stats.allImportedPackages,
|
|
906
|
+
preExistingDeps,
|
|
907
|
+
preExistingDevDeps
|
|
908
|
+
);
|
|
909
|
+
// Remove dev tooling from ghost list (they may be globally installed)
|
|
910
|
+
for (const dep of stats.ghostDependencies) {
|
|
911
|
+
if (DEV_TOOLING_ECOSYSTEM.has(dep) || dep.startsWith('@types/')) {
|
|
912
|
+
stats.ghostDependencies.delete(dep);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
471
916
|
|
|
472
|
-
//
|
|
473
|
-
|
|
917
|
+
// ============================================================
|
|
918
|
+
// ORPHANED DEPENDENCY ANALYSIS
|
|
919
|
+
// Packages in package.json that are never imported
|
|
920
|
+
// ============================================================
|
|
474
921
|
if (preExistingDeps.length > 0) {
|
|
475
|
-
|
|
922
|
+
stats.orphanedDependencies = detectOrphanedDependencies(
|
|
923
|
+
preExistingDeps,
|
|
924
|
+
stats.allImportedPackages,
|
|
925
|
+
resolvedBinaryPackages,
|
|
926
|
+
DEV_TOOLING_ECOSYSTEM
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// ============================================================
|
|
931
|
+
// UNUSED IMPORTS ANALYSIS (cross-file aggregation)
|
|
932
|
+
// A package is only truly "unused" if it's never used in ANY file
|
|
933
|
+
// ============================================================
|
|
934
|
+
// Build a set of packages that ARE used in at least one file
|
|
935
|
+
const usedInAtLeastOneFile = new Set();
|
|
936
|
+
for (const [, fileImports] of stats.unusedImportsPerFile.entries()) {
|
|
937
|
+
// If a package appears in unusedImportsPerFile for this file,
|
|
938
|
+
// it might still be used in another file ā check allImportedPackages
|
|
939
|
+
}
|
|
940
|
+
// Refine: unusedDepsInCode should only include packages that are
|
|
941
|
+
// imported but never referenced across the entire codebase
|
|
942
|
+
const trulyUnusedImports = new Set();
|
|
943
|
+
for (const pkg of stats.unusedDepsInCode) {
|
|
944
|
+
// If the package is used (identifier found) in ANY file, remove from unused
|
|
945
|
+
let foundUsedElsewhere = false;
|
|
946
|
+
for (const [filePath, fileUnused] of stats.unusedImportsPerFile.entries()) {
|
|
947
|
+
if (!fileUnused.has(pkg)) {
|
|
948
|
+
// This file imports pkg and DOES use it
|
|
949
|
+
if (stats.allImportedPackages.has(pkg)) {
|
|
950
|
+
foundUsedElsewhere = true;
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (!foundUsedElsewhere) trulyUnusedImports.add(pkg);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// ============================================================
|
|
959
|
+
// DISPLAY: GHOST DEPENDENCIES (critical ā will break at runtime)
|
|
960
|
+
// ============================================================
|
|
961
|
+
if (stats.ghostDependencies.size > 0) {
|
|
962
|
+
console.log(`\n${'ā'.repeat(67)}`);
|
|
963
|
+
console.log(`šØ GHOST DEPENDENCIES DETECTED (CRITICAL ā Runtime/Deploy will FAIL)`);
|
|
964
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
965
|
+
console.log(` These packages are USED in your code but NOT listed in package.json.`);
|
|
966
|
+
console.log(` They may work locally (if globally installed) but WILL FAIL in CI/CD.\n`);
|
|
967
|
+
for (const pkg of stats.ghostDependencies) {
|
|
968
|
+
console.log(` ā \x1b[31m"${pkg}"\x1b[0m ā imported in code, missing from package.json`);
|
|
969
|
+
}
|
|
970
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
971
|
+
const addGhosts = await safeQuestion(`ā Add these missing packages to package.json automatically? (Y/n): `);
|
|
972
|
+
if (addGhosts.trim().toLowerCase() !== 'n' && addGhosts.trim().toLowerCase() !== 'no') {
|
|
973
|
+
for (const pkg of stats.ghostDependencies) stats.rawDeps.add(pkg);
|
|
974
|
+
console.log(` ā
Ghost dependencies queued for package.json registration.`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// ============================================================
|
|
979
|
+
// DISPLAY: ORPHANED DEPENDENCIES (in package.json, never used)
|
|
980
|
+
// ============================================================
|
|
981
|
+
if (stats.orphanedDependencies.size > 0) {
|
|
982
|
+
console.log(`\n${'ā'.repeat(67)}`);
|
|
983
|
+
console.log(`š¦ ORPHANED DEPENDENCIES DETECTED (in package.json, never imported)`);
|
|
984
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
985
|
+
console.log(` These packages are declared in package.json but never imported`);
|
|
986
|
+
console.log(` anywhere in your source code. Safe to remove.\n`);
|
|
987
|
+
for (const pkg of stats.orphanedDependencies) {
|
|
988
|
+
console.log(` šļø \x1b[33m"${pkg}"\x1b[0m ā declared but never imported`);
|
|
989
|
+
}
|
|
990
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
991
|
+
const pruneOrphans = await safeQuestion(`ā Remove these orphaned packages from package.json? (y/N): `);
|
|
992
|
+
if (pruneOrphans.trim().toLowerCase() === 'y' || pruneOrphans.trim().toLowerCase() === 'yes') {
|
|
993
|
+
if (existingPackageJson) {
|
|
994
|
+
for (const pkg of stats.orphanedDependencies) {
|
|
995
|
+
delete existingPackageJson.dependencies?.[pkg];
|
|
996
|
+
}
|
|
997
|
+
fs.writeFileSync(pkgPath, JSON.stringify(existingPackageJson, null, 2));
|
|
998
|
+
console.log(` šļø Orphaned dependencies removed from package.json.`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
476
1001
|
}
|
|
477
1002
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
]);
|
|
1003
|
+
// ============================================================
|
|
1004
|
+
// DISPLAY: UNUSED IMPORTS (imported but never referenced in code)
|
|
1005
|
+
// ============================================================
|
|
1006
|
+
const allDiscoveredUnused = new Set([...trulyUnusedImports]);
|
|
1007
|
+
// Also add packages in package.json not found in code at all
|
|
1008
|
+
if (preExistingDeps.length > 0) {
|
|
1009
|
+
preExistingDeps.forEach(dep => {
|
|
1010
|
+
if (!stats.rawDeps.has(dep) && !DEV_TOOLING_ECOSYSTEM.has(dep) && !dep.startsWith('@types/')) {
|
|
1011
|
+
allDiscoveredUnused.add(dep);
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
// Remove dev tooling from unused list
|
|
482
1016
|
for (const dep of allDiscoveredUnused) {
|
|
483
|
-
if (
|
|
1017
|
+
if (DEV_TOOLING_ECOSYSTEM.has(dep) || dep.startsWith('@types/')) {
|
|
484
1018
|
allDiscoveredUnused.delete(dep);
|
|
485
1019
|
}
|
|
486
1020
|
}
|
|
487
1021
|
|
|
488
1022
|
if (allDiscoveredUnused.size > 0) {
|
|
489
|
-
console.log(`\n
|
|
490
|
-
console.log(
|
|
491
|
-
console.log(`
|
|
492
|
-
console.log(`
|
|
493
|
-
console.log(
|
|
494
|
-
|
|
495
|
-
|
|
1023
|
+
console.log(`\n${'ā'.repeat(67)}`);
|
|
1024
|
+
console.log(`ā ļø UNUSED IMPORTS DETECTED (imported but never referenced in code)`);
|
|
1025
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
1026
|
+
console.log(` These modules are imported but their identifiers are never used`);
|
|
1027
|
+
console.log(` in executable code paths.\n`);
|
|
1028
|
+
|
|
1029
|
+
for (const dep of allDiscoveredUnused) {
|
|
1030
|
+
// Show which files have this unused import
|
|
1031
|
+
const filesWithUnused = [];
|
|
1032
|
+
for (const [filePath, fileUnused] of stats.unusedImportsPerFile.entries()) {
|
|
1033
|
+
if (fileUnused.has(dep)) {
|
|
1034
|
+
const lines = fileUnused.get(dep);
|
|
1035
|
+
const lineStr = lines.length > 0 ? `:${lines[0]}` : '';
|
|
1036
|
+
filesWithUnused.push(`${path.relative(targetDir, filePath)}${lineStr}`);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (filesWithUnused.length > 0) {
|
|
1040
|
+
console.log(` ā” \x1b[33m"${dep}"\x1b[0m`);
|
|
1041
|
+
filesWithUnused.forEach(f => console.log(` āā ${f}`));
|
|
1042
|
+
} else {
|
|
1043
|
+
console.log(` ā” \x1b[33m"${dep}"\x1b[0m`);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
1047
|
+
|
|
1048
|
+
const pruneChoice = await safeQuestion(`ā Exclude these unused imports from your package.json setup? (y/N): `);
|
|
496
1049
|
if (pruneChoice.trim().toLowerCase() === 'y' || pruneChoice.trim().toLowerCase() === 'yes') {
|
|
497
1050
|
for (const deadDep of allDiscoveredUnused) stats.rawDeps.delete(deadDep);
|
|
498
|
-
console.log(` šļø Pruned unused
|
|
1051
|
+
console.log(` šļø Pruned unused imports from configuration blueprint.`);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// ============================================================
|
|
1056
|
+
// DISPLAY: DEPRECATED PACKAGES
|
|
1057
|
+
// ============================================================
|
|
1058
|
+
if (stats.deprecatedPackages.size > 0) {
|
|
1059
|
+
console.log(`\n${'ā'.repeat(67)}`);
|
|
1060
|
+
console.log(`ā ļø DEPRECATED PACKAGES DETECTED`);
|
|
1061
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
1062
|
+
for (const [pkg, msg] of stats.deprecatedPackages.entries()) {
|
|
1063
|
+
console.log(` š \x1b[33m"${pkg}"\x1b[0m ā ${msg}`);
|
|
1064
|
+
}
|
|
1065
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// ============================================================
|
|
1069
|
+
// PHANTOM INJECTION DETECTION
|
|
1070
|
+
// Packages used in code (by identifier) but never imported
|
|
1071
|
+
// ============================================================
|
|
1072
|
+
// Build phantom detection from ALL declared packages
|
|
1073
|
+
const allDeclaredForPhantom = new Set([...preExistingDeps, ...preExistingDevDeps]);
|
|
1074
|
+
for (const [filePath] of stats.unusedImportsPerFile.entries()) {
|
|
1075
|
+
// Already handled above
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Scan for identifiers used without import (using declared package names as hints)
|
|
1079
|
+
const phantomScanContent = new Map();
|
|
1080
|
+
function collectExecutionContent(dir) {
|
|
1081
|
+
try {
|
|
1082
|
+
for (const file of fs.readdirSync(dir)) {
|
|
1083
|
+
const fullPath = path.join(dir, file);
|
|
1084
|
+
const stat = fs.statSync(fullPath);
|
|
1085
|
+
if (stat.isDirectory() && !IGNORED_DIRS.has(file) && !file.startsWith('.')) {
|
|
1086
|
+
collectExecutionContent(fullPath);
|
|
1087
|
+
} else if (VALID_EXTENSIONS.has(path.extname(file))) {
|
|
1088
|
+
try {
|
|
1089
|
+
const content = readFileSyncNormalized(fullPath);
|
|
1090
|
+
const execCode = content.split(/\r?\n/)
|
|
1091
|
+
.filter(l => {
|
|
1092
|
+
const t = l.trim();
|
|
1093
|
+
return !t.startsWith('import ') && !/\brequire\s*\(/.test(t);
|
|
1094
|
+
})
|
|
1095
|
+
.join('\n');
|
|
1096
|
+
phantomScanContent.set(fullPath, execCode);
|
|
1097
|
+
} catch {}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
} catch {}
|
|
1101
|
+
}
|
|
1102
|
+
collectExecutionContent(targetDir);
|
|
1103
|
+
|
|
1104
|
+
for (const [filePath, execCode] of phantomScanContent.entries()) {
|
|
1105
|
+
for (const token of allDeclaredForPhantom) {
|
|
1106
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1107
|
+
const tokenPattern = new RegExp(`\\b${escaped}\\b`);
|
|
1108
|
+
if (tokenPattern.test(execCode) && !stats.allImportedPackages.has(token)) {
|
|
1109
|
+
stats.rawDeps.add(token);
|
|
1110
|
+
if (!stats.phantomInjections.has(filePath)) stats.phantomInjections.set(filePath, new Set());
|
|
1111
|
+
stats.phantomInjections.get(filePath).add(token);
|
|
1112
|
+
}
|
|
499
1113
|
}
|
|
500
1114
|
}
|
|
501
1115
|
|
|
502
1116
|
const isTypeScript = stats.tsFiles > stats.jsFiles;
|
|
503
|
-
const isFrontendWeb = stats.hasHtml || stats.rawDeps.has('react') || stats.rawDeps.has('vue') || stats.rawDeps.has('vite');
|
|
1117
|
+
const isFrontendWeb = stats.hasHtml || stats.rawDeps.has('react') || stats.rawDeps.has('vue') || stats.rawDeps.has('vite') || stats.rawDeps.has('svelte') || stats.rawDeps.has('next') || stats.rawDeps.has('nuxt');
|
|
504
1118
|
|
|
1119
|
+
// --- dotenv suggestion ---
|
|
505
1120
|
if (stats.envVars.size > 0 && !stats.rawDeps.has('dotenv') && !isFrontendWeb) {
|
|
506
1121
|
console.log(`\nš” CONFIGURATION COMPLIANCE GAP: UNMANAGED ENVIRONMENT VARIABLES`);
|
|
507
|
-
console.log(
|
|
1122
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
508
1123
|
console.log(` Workspace utilizes 'process.env' variables but 'dotenv' is missing.`);
|
|
509
|
-
console.log(
|
|
510
|
-
const choiceEnv = await
|
|
511
|
-
|
|
1124
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
1125
|
+
const choiceEnv = await safeQuestion(`ā Add 'dotenv' and automatically wire initialization hooks into your files? (Y/n): `);
|
|
512
1126
|
if (choiceEnv.trim().toLowerCase() !== 'n' && choiceEnv.trim().toLowerCase() !== 'no') {
|
|
513
1127
|
stats.rawDeps.add('dotenv');
|
|
514
1128
|
stats.injectDotenvEngine = true;
|
|
515
1129
|
}
|
|
516
1130
|
}
|
|
517
1131
|
|
|
1132
|
+
// --- Build package.json ---
|
|
518
1133
|
const packageJson = {
|
|
519
1134
|
name: folderName.toLowerCase().replace(/[^a-z0-9-_]/g, '-'),
|
|
520
1135
|
version: '1.0.0',
|
|
@@ -527,16 +1142,16 @@ async function main() {
|
|
|
527
1142
|
devDependencies: {}
|
|
528
1143
|
};
|
|
529
1144
|
|
|
1145
|
+
// --- ESLint suggestion ---
|
|
530
1146
|
const eslintConfigFile = path.join(targetDir, 'eslint.config.js');
|
|
531
1147
|
const linterPresent = fs.existsSync(eslintConfigFile) || fs.existsSync(path.join(targetDir, '.eslintrc.json')) || fs.existsSync(path.join(targetDir, '.eslintrc.js'));
|
|
532
|
-
|
|
1148
|
+
|
|
533
1149
|
if (!linterPresent && (stats.quality.varCount > 0 || stats.quality.hasEval || stats.phantomInjections.size > 0)) {
|
|
534
1150
|
console.log(`\nšØ QUALITY LAYER AUDITOR: SYNTAX VALIDATION SYSTEM REQUIRED`);
|
|
535
|
-
console.log(
|
|
536
|
-
console.log(` Code anomalies (legacy 'var'
|
|
537
|
-
console.log(
|
|
538
|
-
const choiceLintSetup = await
|
|
539
|
-
|
|
1151
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
1152
|
+
console.log(` Code anomalies (legacy 'var' or 'eval()') require static linter guards.`);
|
|
1153
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
1154
|
+
const choiceLintSetup = await safeQuestion(`ā Bootstrap standard ESLint flat verification rules into workspace? (Y/n): `);
|
|
540
1155
|
if (choiceLintSetup.trim().toLowerCase() !== 'n' && choiceLintSetup.trim().toLowerCase() !== 'no') {
|
|
541
1156
|
stats.bootstrapEslintSuite = true;
|
|
542
1157
|
stats.rawDeps.add('eslint');
|
|
@@ -566,6 +1181,7 @@ async function main() {
|
|
|
566
1181
|
if (!isFrontendWeb) packageJson.devDependencies['@types/node'] = '^20.11.0';
|
|
567
1182
|
}
|
|
568
1183
|
|
|
1184
|
+
// --- Resolve package versions from npm ---
|
|
569
1185
|
if (stats.rawDeps.size > 0) {
|
|
570
1186
|
console.log(`\nš” Resolving baseline package registry definitions...`);
|
|
571
1187
|
for (const pkg of stats.rawDeps) {
|
|
@@ -574,110 +1190,111 @@ async function main() {
|
|
|
574
1190
|
const check = await inspectNpmPackage(cleaned);
|
|
575
1191
|
if (check && check.error !== 'NOT_FOUND') {
|
|
576
1192
|
const version = check.version || 'latest';
|
|
577
|
-
|
|
1193
|
+
|
|
578
1194
|
const isDevDep = [
|
|
579
|
-
'vite', 'vitest', 'typescript', 'eslint', 'typescript-eslint',
|
|
580
|
-
'@eslint/js', 'prettier', 'jest', 'nodemon', 'ts-node', 'tsup'
|
|
1195
|
+
'vite', 'vitest', 'typescript', 'eslint', 'typescript-eslint',
|
|
1196
|
+
'@eslint/js', 'prettier', 'jest', 'nodemon', 'ts-node', 'tsup',
|
|
1197
|
+
'esbuild', '@swc/cli', 'tsx', 'rimraf', 'copyfiles', 'mkdirp',
|
|
1198
|
+
'husky', 'lint-staged', '@commitlint/cli', 'typedoc', 'c8', 'nyc',
|
|
1199
|
+
'mocha', 'ava', 'tap', 'jasmine', 'storybook', 'turbo', 'nx',
|
|
1200
|
+
'biome', '@biomejs/biome', 'oxlint', 'xo', 'standard',
|
|
581
1201
|
].includes(cleaned) || cleaned.startsWith('@types/');
|
|
582
1202
|
|
|
583
1203
|
if (isDevDep) packageJson.devDependencies[cleaned] = `^${version}`;
|
|
584
1204
|
else packageJson.dependencies[cleaned] = `^${version}`;
|
|
585
|
-
console.log(`
|
|
1205
|
+
console.log(` ā Synced: ${cleaned}@^${version}${check.deprecated ? ' \x1b[33m[DEPRECATED]\x1b[0m' : ''}`);
|
|
586
1206
|
}
|
|
587
1207
|
}
|
|
588
1208
|
}
|
|
589
1209
|
}
|
|
590
1210
|
|
|
1211
|
+
// --- Phantom injection report ---
|
|
591
1212
|
if (stats.phantomInjections.size > 0) {
|
|
592
|
-
console.log(`\n
|
|
593
|
-
console.log(
|
|
1213
|
+
console.log(`\n${'ā'.repeat(67)}`);
|
|
1214
|
+
console.log(`š» PHANTOM STRUCTURE ALERT: UNIMPORTED EXECUTIONS DETECTED`);
|
|
1215
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
594
1216
|
for (const [filePath, missingModules] of stats.phantomInjections.entries()) {
|
|
595
1217
|
console.log(`š File: ${path.relative(targetDir, filePath)}`);
|
|
596
1218
|
console.log(` ā Used but never imported: ${Array.from(missingModules).map(m => `"${m}"`).join(', ')}`);
|
|
597
1219
|
}
|
|
598
|
-
console.log(
|
|
1220
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
599
1221
|
}
|
|
600
1222
|
|
|
1223
|
+
// --- Code quality warnings ---
|
|
601
1224
|
if (stats.quality.varCount > 0 || stats.quality.hasEval || stats.quality.syncFsCount > 0) {
|
|
602
1225
|
console.log(`\nā ļø CODE ARCHITECTURE & MODERNIZATION COMPLIANCE WARNINGS:`);
|
|
603
|
-
console.log(
|
|
604
|
-
if (stats.quality.varCount > 0) console.log(` ā” Found ${stats.quality.varCount} instances of legacy 'var'
|
|
605
|
-
if (stats.quality.hasEval) console.log(` š„ DANGER: 'eval()'
|
|
606
|
-
if (stats.quality.syncFsCount > 0) console.log(` š Performance
|
|
607
|
-
console.log(
|
|
1226
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
1227
|
+
if (stats.quality.varCount > 0) console.log(` ā” Found ${stats.quality.varCount} instances of legacy 'var'. Transition to 'let' / 'const'.`);
|
|
1228
|
+
if (stats.quality.hasEval) console.log(` š„ DANGER: 'eval()' detected! Refactor to mitigate remote code execution vectors.`);
|
|
1229
|
+
if (stats.quality.syncFsCount > 0) console.log(` š Performance: Found ${stats.quality.syncFsCount} synchronous fs calls. Transition to 'fs/promises'.`);
|
|
1230
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
608
1231
|
}
|
|
609
1232
|
|
|
1233
|
+
// --- Security: hardcoded secrets ---
|
|
610
1234
|
if (stats.discoveredSecrets.length > 0) {
|
|
611
1235
|
console.log(`\nšØ CRITICAL SECURITY COMPLIANCE ALERT: HARDCODED CREDENTIALS DETECTED`);
|
|
612
|
-
console.log(
|
|
1236
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
613
1237
|
for (const secretMeta of stats.discoveredSecrets) {
|
|
614
1238
|
console.log(`š File: ${path.relative(targetDir, secretMeta.filePath)}`);
|
|
615
|
-
console.log(` ā ļø
|
|
1239
|
+
console.log(` ā ļø Hardcoded credential found: [${secretMeta.keyName}]`);
|
|
616
1240
|
}
|
|
617
|
-
console.log(
|
|
618
|
-
|
|
619
|
-
const fixSecrets = await rl.question(`ā Automatically extract credentials into environment mappings safely? (y/N): `);
|
|
1241
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
620
1242
|
|
|
1243
|
+
const fixSecrets = await safeQuestion(`ā Automatically extract credentials into environment mappings safely? (y/N): `);
|
|
621
1244
|
if (fixSecrets.trim().toLowerCase() === 'y' || fixSecrets.trim().toLowerCase() === 'yes') {
|
|
622
1245
|
const envPath = path.join(targetDir, '.env');
|
|
623
1246
|
let envBuffer = fs.existsSync(envPath) ? readFileSyncNormalized(envPath) : '';
|
|
624
|
-
|
|
1247
|
+
|
|
625
1248
|
for (const secretMeta of stats.discoveredSecrets) {
|
|
626
1249
|
let currentCodeContent = readFileSyncNormalized(secretMeta.filePath);
|
|
627
1250
|
const envAccessor = isFrontendWeb ? `import.meta.env.${secretMeta.envVarName}` : `process.env.${secretMeta.envVarName}`;
|
|
628
|
-
|
|
629
1251
|
const exactLiteralPattern = new RegExp(`\\b${secretMeta.keyName}\\s*=\\s*['"\`]${secretMeta.secretValue.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}['"\`]`, 'g');
|
|
630
1252
|
currentCodeContent = currentCodeContent.replace(exactLiteralPattern, `${secretMeta.keyName} = ${envAccessor}`);
|
|
631
1253
|
fs.writeFileSync(secretMeta.filePath, currentCodeContent);
|
|
632
|
-
|
|
633
1254
|
if (!envBuffer.includes(`${secretMeta.envVarName}=`)) envBuffer += `${secretMeta.envVarName}=${secretMeta.secretValue}\n`;
|
|
634
|
-
console.log(` š
|
|
1255
|
+
console.log(` š Isolated: ${secretMeta.keyName} ā ${envAccessor}`);
|
|
635
1256
|
}
|
|
636
1257
|
fs.writeFileSync(envPath, envBuffer);
|
|
637
1258
|
}
|
|
638
1259
|
}
|
|
639
1260
|
|
|
1261
|
+
// --- Monorepo detection ---
|
|
640
1262
|
if (stats.subWorkspaces && stats.subWorkspaces.length > 1) {
|
|
641
1263
|
console.log(`\nš MULTI-WORKSPACE SEGMENTATION DETECTED`);
|
|
642
|
-
console.log(` Identified
|
|
643
|
-
|
|
644
|
-
const setupWorkspace = await rl.question(`ā Setup layout architecture as a multi-package Monorepo Workspace layout? (y/N): `);
|
|
645
|
-
|
|
1264
|
+
console.log(` Identified sub-module paths: ${stats.subWorkspaces.map(w => `/${w}`).join(', ')}`);
|
|
1265
|
+
const setupWorkspace = await safeQuestion(`ā Setup as a multi-package Monorepo Workspace layout? (y/N): `);
|
|
646
1266
|
if (setupWorkspace.trim().toLowerCase() === 'y' || setupWorkspace.trim().toLowerCase() === 'yes') {
|
|
647
1267
|
if (activePkgManager === 'pnpm') {
|
|
648
1268
|
const workspaceYamlPath = path.join(targetDir, 'pnpm-workspace.yaml');
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
console.log(` šļø Generated monorepo configuration layer: pnpm-workspace.yaml`);
|
|
1269
|
+
fs.writeFileSync(workspaceYamlPath, `packages:\n${stats.subWorkspaces.map(w => ` - '${w}'`).join('\n')}\n`);
|
|
1270
|
+
console.log(` šļø Generated: pnpm-workspace.yaml`);
|
|
652
1271
|
} else {
|
|
653
|
-
packageJson.workspaces = stats.subWorkspaces
|
|
654
|
-
console.log(` šļø Injected 'workspaces'
|
|
1272
|
+
packageJson.workspaces = stats.subWorkspaces;
|
|
1273
|
+
console.log(` šļø Injected 'workspaces' into root package.json.`);
|
|
655
1274
|
}
|
|
656
1275
|
}
|
|
657
1276
|
}
|
|
658
1277
|
|
|
1278
|
+
// --- License ---
|
|
659
1279
|
const licensePath = path.join(targetDir, 'LICENSE');
|
|
660
1280
|
let chosenLicenseType = preExistingLicense || 'None';
|
|
661
|
-
|
|
1281
|
+
|
|
662
1282
|
if (!fs.existsSync(licensePath) && !preExistingLicense) {
|
|
663
1283
|
console.log(`\nāļø Legal Compliance Auditor: No LICENSE file located.`);
|
|
664
|
-
const licInput = await
|
|
665
|
-
|
|
1284
|
+
const licInput = await safeQuestion(`ā Enter Open Source License (e.g. MIT, Apache-2.0, ISC, BSD-3-Clause, skip): `);
|
|
666
1285
|
const cleanedInput = licInput.trim();
|
|
667
1286
|
if (cleanedInput.toLowerCase() !== 'skip' && cleanedInput.toLowerCase() !== 'none' && cleanedInput !== '') {
|
|
668
|
-
console.log(` š” Querying GitHub Legal Databases for "${cleanedInput.toUpperCase()}"
|
|
1287
|
+
console.log(` š” Querying GitHub Legal Databases for "${cleanedInput.toUpperCase()}"...`);
|
|
669
1288
|
const rawTemplate = await fetchRemoteLicense(cleanedInput);
|
|
670
|
-
|
|
671
1289
|
if (rawTemplate) {
|
|
672
1290
|
const parsedText = rawTemplate
|
|
673
|
-
.replace(/\[year\]|<year>/gi,
|
|
1291
|
+
.replace(/\[year\]|<year>/gi, new Date().getFullYear().toString())
|
|
674
1292
|
.replace(/\[fullname\]|\[name of copyright owner\]|<copyright holders>|<name of author>/gi, gitInfo.name);
|
|
675
|
-
|
|
676
1293
|
fs.writeFileSync(licensePath, parsedText);
|
|
677
1294
|
chosenLicenseType = cleanedInput.toUpperCase();
|
|
678
|
-
console.log(` āļø
|
|
1295
|
+
console.log(` āļø Provisioned: LICENSE`);
|
|
679
1296
|
} else {
|
|
680
|
-
console.log(` ā ļø License
|
|
1297
|
+
console.log(` ā ļø License "${cleanedInput}" not found. Saving custom label.`);
|
|
681
1298
|
chosenLicenseType = cleanedInput;
|
|
682
1299
|
}
|
|
683
1300
|
packageJson.license = chosenLicenseType;
|
|
@@ -689,7 +1306,7 @@ async function main() {
|
|
|
689
1306
|
const rawTemplate = await fetchRemoteLicense(preExistingLicense);
|
|
690
1307
|
if (rawTemplate) {
|
|
691
1308
|
const parsedText = rawTemplate
|
|
692
|
-
.replace(/\[year\]|<year>/gi,
|
|
1309
|
+
.replace(/\[year\]|<year>/gi, new Date().getFullYear().toString())
|
|
693
1310
|
.replace(/\[fullname\]|\[name of copyright owner\]|<copyright holders>|<name of author>/gi, gitInfo.name);
|
|
694
1311
|
fs.writeFileSync(licensePath, parsedText);
|
|
695
1312
|
}
|
|
@@ -700,33 +1317,31 @@ async function main() {
|
|
|
700
1317
|
if (currentLicenseContent.includes('MIT')) chosenLicenseType = 'MIT';
|
|
701
1318
|
else if (currentLicenseContent.includes('Apache')) chosenLicenseType = 'Apache-2.0';
|
|
702
1319
|
else chosenLicenseType = 'Custom';
|
|
703
|
-
} catch(e) {}
|
|
1320
|
+
} catch (e) {}
|
|
704
1321
|
}
|
|
705
1322
|
packageJson.license = chosenLicenseType;
|
|
706
1323
|
}
|
|
707
1324
|
|
|
1325
|
+
// --- Test scaffolding ---
|
|
708
1326
|
if (!stats.hasTests) {
|
|
709
|
-
const bootstrapTest = await
|
|
710
|
-
|
|
1327
|
+
const bootstrapTest = await safeQuestion(`\nā No test files detected. Scaffold a zero-bloat testing harness via Node native test runner? (y/N): `);
|
|
711
1328
|
if (bootstrapTest.trim().toLowerCase() === 'y' || bootstrapTest.trim().toLowerCase() === 'yes') {
|
|
712
1329
|
const isEsm = packageJson.type === 'module';
|
|
713
1330
|
const testExt = isTypeScript ? '.test.ts' : '.test.js';
|
|
714
|
-
const
|
|
715
|
-
const testFilePath = path.join(targetDir, targetTestFile);
|
|
716
|
-
|
|
1331
|
+
const testFilePath = path.join(targetDir, `index${testExt}`);
|
|
717
1332
|
const testTemplate = isEsm
|
|
718
1333
|
? `import { test, describe } from 'node:test';\nimport assert from 'node:assert';\n\ndescribe('Core Architecture Testing Suite', () => {\n test('should verify systemic environmental execution health', () => {\n assert.strictEqual(1, 1);\n });\n});\n`
|
|
719
1334
|
: `const { test, describe } = require('node:test');\nconst assert = require('node:assert');\n\ndescribe('Core Architecture Testing Suite', () => {\n test('should verify systemic environmental execution health', () => {\n assert.strictEqual(1, 1);\n });\n});\n`;
|
|
720
|
-
|
|
721
1335
|
fs.writeFileSync(testFilePath, testTemplate);
|
|
722
1336
|
packageJson.scripts.test = 'node --test';
|
|
723
1337
|
stats.hasTests = true;
|
|
724
|
-
console.log(` š§Ŗ Generated
|
|
1338
|
+
console.log(` š§Ŗ Generated: index${testExt}`);
|
|
725
1339
|
}
|
|
726
1340
|
}
|
|
727
1341
|
|
|
728
1342
|
console.log(`\nāļø Writing ecosystem configuration artifacts...`);
|
|
729
1343
|
|
|
1344
|
+
// --- ESLint config ---
|
|
730
1345
|
if (stats.bootstrapEslintSuite) {
|
|
731
1346
|
packageJson.scripts.lint = 'eslint .';
|
|
732
1347
|
let eslintConfigContent = '';
|
|
@@ -740,33 +1355,38 @@ async function main() {
|
|
|
740
1355
|
}
|
|
741
1356
|
}
|
|
742
1357
|
fs.writeFileSync(eslintConfigFile, eslintConfigContent);
|
|
743
|
-
console.log(` šØ Provisioned
|
|
1358
|
+
console.log(` šØ Provisioned: eslint.config.js`);
|
|
744
1359
|
}
|
|
745
1360
|
|
|
1361
|
+
// --- Write / merge package.json ---
|
|
746
1362
|
if (fs.existsSync(pkgPath)) {
|
|
747
1363
|
try {
|
|
748
1364
|
const currentPackageJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
749
1365
|
currentPackageJson.dependencies = { ...packageJson.dependencies, ...currentPackageJson.dependencies };
|
|
750
1366
|
currentPackageJson.devDependencies = { ...packageJson.devDependencies, ...currentPackageJson.devDependencies };
|
|
751
|
-
if (packageJson.scripts.lint && !currentPackageJson.scripts
|
|
752
|
-
|
|
1367
|
+
if (packageJson.scripts.lint && !currentPackageJson.scripts?.lint) {
|
|
1368
|
+
currentPackageJson.scripts = currentPackageJson.scripts || {};
|
|
1369
|
+
currentPackageJson.scripts.lint = packageJson.scripts.lint;
|
|
1370
|
+
}
|
|
753
1371
|
fs.writeFileSync(pkgPath, JSON.stringify(currentPackageJson, null, 2));
|
|
754
1372
|
console.log(` š Safely merged discovered dependencies into existing package.json`);
|
|
755
1373
|
} catch (e) {}
|
|
756
1374
|
} else {
|
|
757
|
-
fs.writeFileSync(pkgPath, JSON.stringify(packageJson, null, 2));
|
|
758
|
-
console.log(` š
|
|
1375
|
+
fs.writeFileSync(pkgPath, JSON.stringify(packageJson, null, 2));
|
|
1376
|
+
console.log(` š Generated: package.json`);
|
|
759
1377
|
}
|
|
760
1378
|
|
|
1379
|
+
// --- Prettier config ---
|
|
761
1380
|
const prettierPath = path.join(targetDir, '.prettierrc');
|
|
762
1381
|
if (!fs.existsSync(prettierPath)) {
|
|
763
1382
|
const useTabs = stats.style.tabCount > (stats.style.space2Count + stats.style.space4Count);
|
|
764
1383
|
const useSemi = stats.style.semiCount >= stats.style.noSemiCount;
|
|
765
1384
|
const tabWidth = stats.style.space4Count > stats.style.space2Count ? 4 : 2;
|
|
766
|
-
fs.writeFileSync(prettierPath, JSON.stringify({ semi: useSemi, useTabs
|
|
1385
|
+
fs.writeFileSync(prettierPath, JSON.stringify({ semi: useSemi, useTabs, tabWidth, singleQuote: true, trailingComma: "es5" }, null, 2));
|
|
767
1386
|
console.log(` šØ Code formatting mirror locked: .prettierrc`);
|
|
768
1387
|
}
|
|
769
1388
|
|
|
1389
|
+
// --- .env.example ---
|
|
770
1390
|
if (stats.envVars.size > 0) {
|
|
771
1391
|
const envExamplePath = path.join(targetDir, '.env.example');
|
|
772
1392
|
if (!fs.existsSync(envExamplePath)) {
|
|
@@ -775,20 +1395,26 @@ async function main() {
|
|
|
775
1395
|
}
|
|
776
1396
|
}
|
|
777
1397
|
|
|
1398
|
+
// --- .gitignore ---
|
|
778
1399
|
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
779
|
-
if (!fs.existsSync(gitignorePath)) {
|
|
780
|
-
fs.writeFileSync(gitignorePath, `node_modules/\ndist/\nbuild/\n.env\n.env.local\n.DS_Store\n`);
|
|
781
|
-
console.log(` āļø
|
|
1400
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
1401
|
+
fs.writeFileSync(gitignorePath, `node_modules/\ndist/\nbuild/\n.env\n.env.local\n.DS_Store\n*.log\n`);
|
|
1402
|
+
console.log(` āļø Generated: .gitignore`);
|
|
782
1403
|
}
|
|
783
1404
|
|
|
1405
|
+
// --- tsconfig.json ---
|
|
784
1406
|
if (isTypeScript) {
|
|
785
1407
|
const tsconfigPath = path.join(targetDir, 'tsconfig.json');
|
|
786
1408
|
if (!fs.existsSync(tsconfigPath)) {
|
|
787
|
-
fs.writeFileSync(tsconfigPath, JSON.stringify({
|
|
788
|
-
|
|
1409
|
+
fs.writeFileSync(tsconfigPath, JSON.stringify({
|
|
1410
|
+
compilerOptions: { target: "ES2022", module: "NodeNext", moduleResolution: "NodeNext", esModuleInterop: true, strict: true, skipLibCheck: true, outDir: "./dist" },
|
|
1411
|
+
include: ["src/**/*", "**/*.ts"]
|
|
1412
|
+
}, null, 2));
|
|
1413
|
+
console.log(` āļø Generated: tsconfig.json`);
|
|
789
1414
|
}
|
|
790
1415
|
}
|
|
791
1416
|
|
|
1417
|
+
// --- README ---
|
|
792
1418
|
const readmePath = path.join(targetDir, 'README.md');
|
|
793
1419
|
if (!fs.existsSync(readmePath)) {
|
|
794
1420
|
const pName = packageJson.name;
|
|
@@ -797,7 +1423,7 @@ async function main() {
|
|
|
797
1423
|
const displayDevDeps = Object.keys(packageJson.devDependencies).map(d => `* \`${d}\``).join('\n') || '* None extracted';
|
|
798
1424
|
const licenseBadgeParam = encodeURIComponent(chosenLicenseType.replace(/-/g, '_'));
|
|
799
1425
|
|
|
800
|
-
const documentationTemplate =
|
|
1426
|
+
const documentationTemplate =
|
|
801
1427
|
`# ${pName}
|
|
802
1428
|
|
|
803
1429
|

|
|
@@ -814,10 +1440,6 @@ ${displayDeps}
|
|
|
814
1440
|
### System Tooling Engines (\`devDependencies\`)
|
|
815
1441
|
${displayDevDeps}
|
|
816
1442
|
|
|
817
|
-
### Underlying Tooling Architecture
|
|
818
|
-
This project environment layout maps out core metadata elements dynamically using:
|
|
819
|
-
* \`npm-deprecated-check\` (Bundled internal core validation system for dependency deprecation checking routines)
|
|
820
|
-
|
|
821
1443
|
---
|
|
822
1444
|
|
|
823
1445
|
## Project Architecture Layout
|
|
@@ -825,24 +1447,24 @@ This project environment layout maps out core metadata elements dynamically usin
|
|
|
825
1447
|
${layoutTree}
|
|
826
1448
|
\`\`\`
|
|
827
1449
|
|
|
828
|
-
## Installation
|
|
829
|
-
Initialize the workspace tracking structures via your active system package engine:
|
|
1450
|
+
## Installation
|
|
830
1451
|
|
|
831
1452
|
\`\`\`bash
|
|
832
1453
|
${activePkgManager} install
|
|
833
1454
|
\`\`\`
|
|
834
1455
|
`;
|
|
835
1456
|
fs.writeFileSync(readmePath, documentationTemplate);
|
|
836
|
-
console.log(` š
|
|
1457
|
+
console.log(` š Generated: README.md`);
|
|
837
1458
|
}
|
|
838
1459
|
|
|
1460
|
+
// --- Phantom injection fix ---
|
|
839
1461
|
if (stats.phantomInjections.size > 0 || (stats.injectDotenvEngine && stats.filesWithEnvVars.size > 0)) {
|
|
840
1462
|
console.log(`\nš” Source Code Modification Subsystem:`);
|
|
841
|
-
const injectChoice = await
|
|
1463
|
+
const injectChoice = await safeQuestion(`ā Found phantom modules or unmanaged env components. Mutate file headers cleanly now? (y/N): `);
|
|
842
1464
|
|
|
843
1465
|
if (injectChoice.trim().toLowerCase() === 'y' || injectChoice.trim().toLowerCase() === 'yes') {
|
|
844
1466
|
const allTargets = new Set([...stats.phantomInjections.keys(), ...stats.filesWithEnvVars]);
|
|
845
|
-
|
|
1467
|
+
|
|
846
1468
|
for (const filePath of allTargets) {
|
|
847
1469
|
const originalCode = readFileSyncNormalized(filePath);
|
|
848
1470
|
let declarationBlock = '';
|
|
@@ -859,62 +1481,85 @@ ${activePkgManager} install
|
|
|
859
1481
|
if (packageJson.type === 'module') declarationBlock += `import 'dotenv/config';\n`;
|
|
860
1482
|
else declarationBlock += `require('dotenv').config();\n`;
|
|
861
1483
|
}
|
|
862
|
-
|
|
1484
|
+
|
|
863
1485
|
if (declarationBlock !== '') {
|
|
864
1486
|
fs.writeFileSync(filePath, smartPrepend(originalCode, declarationBlock));
|
|
865
|
-
console.log(` ā” Injected
|
|
1487
|
+
console.log(` ā” Injected headers: ${path.relative(targetDir, filePath)}`);
|
|
866
1488
|
}
|
|
867
1489
|
}
|
|
868
1490
|
}
|
|
869
1491
|
}
|
|
870
1492
|
|
|
1493
|
+
// --- Deprecation scan via npm-deprecated-check ---
|
|
871
1494
|
console.log(`\nš INITIALIZING LIVE ECOSYSTEM DEPRECATION SECURITY SCAN...`);
|
|
872
|
-
console.log(` Running integrated npm-deprecated-check validation
|
|
1495
|
+
console.log(` Running integrated npm-deprecated-check validation:\n`);
|
|
873
1496
|
try {
|
|
874
1497
|
const localRequire = createRequire(import.meta.url);
|
|
875
1498
|
const dependencyPkgJsonPath = localRequire.resolve('npm-deprecated-check/package.json');
|
|
876
1499
|
const dependencyPkgJson = JSON.parse(fs.readFileSync(dependencyPkgJsonPath, 'utf8'));
|
|
877
|
-
const binRelativeMapping = typeof dependencyPkgJson.bin === 'string'
|
|
1500
|
+
const binRelativeMapping = typeof dependencyPkgJson.bin === 'string'
|
|
1501
|
+
? dependencyPkgJson.bin
|
|
1502
|
+
: (dependencyPkgJson.bin['npm-deprecated-check'] || dependencyPkgJson.bin['ndc']);
|
|
878
1503
|
const absoluteExecutablePath = path.join(path.dirname(dependencyPkgJsonPath), binRelativeMapping);
|
|
879
1504
|
execSync(`node "${absoluteExecutablePath}" current`, { stdio: 'inherit', cwd: targetDir });
|
|
880
1505
|
} catch (err) {}
|
|
881
1506
|
|
|
1507
|
+
// --- Conflicting lockfiles ---
|
|
882
1508
|
if (stats.conflictingLockfiles.length > 1) {
|
|
883
|
-
console.log(`\nā ļø CONFLICTING
|
|
884
|
-
const cleanLocks = await
|
|
885
|
-
|
|
1509
|
+
console.log(`\nā ļø CONFLICTING LOCKFILES DETECTED: [${stats.conflictingLockfiles.join(', ')}]`);
|
|
1510
|
+
const cleanLocks = await safeQuestion(`ā Purge legacy/mismatched lockfiles to protect package integrity? (y/N): `);
|
|
886
1511
|
if (cleanLocks.trim().toLowerCase() === 'y' || cleanLocks.trim().toLowerCase() === 'yes') {
|
|
887
|
-
const packageEngineLockmap = { npm: 'package-lock.json', pnpm: 'pnpm-lock.yaml', yarn: 'yarn.lock' };
|
|
1512
|
+
const packageEngineLockmap = { npm: 'package-lock.json', pnpm: 'pnpm-lock.yaml', yarn: 'yarn.lock', bun: 'bun.lockb' };
|
|
888
1513
|
const operationalLockfile = packageEngineLockmap[activePkgManager];
|
|
889
1514
|
for (const lockfile of stats.conflictingLockfiles) {
|
|
890
1515
|
if (lockfile !== operationalLockfile) {
|
|
891
1516
|
try {
|
|
892
1517
|
fs.unlinkSync(path.join(targetDir, lockfile));
|
|
893
|
-
console.log(` šļø Cleaned
|
|
1518
|
+
console.log(` šļø Cleaned: ${lockfile}`);
|
|
894
1519
|
} catch (e) {}
|
|
895
1520
|
}
|
|
896
1521
|
}
|
|
897
1522
|
}
|
|
898
1523
|
}
|
|
899
1524
|
|
|
1525
|
+
// --- Final install prompt ---
|
|
900
1526
|
console.log(`\nš¦ Auto-scaffolding pipeline complete!`);
|
|
901
|
-
const userPromptChoice = await rl.question(`ā Detected system default manager: "${activePkgManager}". Run "${activePkgManager} install" automatically now? (y/N): `);
|
|
902
1527
|
|
|
1528
|
+
// Summary report
|
|
1529
|
+
console.log(`\n${'ā'.repeat(67)}`);
|
|
1530
|
+
console.log(`š DEPENDENCY INTELLIGENCE SUMMARY`);
|
|
1531
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
1532
|
+
console.log(` š Files scanned: ${stats.scannedFiles}`);
|
|
1533
|
+
console.log(` š¦ Packages imported: ${stats.allImportedPackages.size}`);
|
|
1534
|
+
if (stats.ghostDependencies.size > 0)
|
|
1535
|
+
console.log(` šØ Ghost deps (missing): ${stats.ghostDependencies.size} ā \x1b[31mCRITICAL\x1b[0m`);
|
|
1536
|
+
if (stats.orphanedDependencies.size > 0)
|
|
1537
|
+
console.log(` šļø Orphaned deps (unused): ${stats.orphanedDependencies.size}`);
|
|
1538
|
+
if (allDiscoveredUnused.size > 0)
|
|
1539
|
+
console.log(` ā” Unused imports: ${allDiscoveredUnused.size}`);
|
|
1540
|
+
if (stats.deprecatedPackages.size > 0)
|
|
1541
|
+
console.log(` š Deprecated packages: ${stats.deprecatedPackages.size}`);
|
|
1542
|
+
if (stats.phantomInjections.size > 0)
|
|
1543
|
+
console.log(` š» Phantom injections: ${stats.phantomInjections.size} file(s)`);
|
|
1544
|
+
if (stats.discoveredSecrets.length > 0)
|
|
1545
|
+
console.log(` š Hardcoded secrets: ${stats.discoveredSecrets.length} ā \x1b[31mSECURITY RISK\x1b[0m`);
|
|
1546
|
+
console.log(`${'ā'.repeat(67)}`);
|
|
1547
|
+
|
|
1548
|
+
const userPromptChoice = await safeQuestion(`ā Detected package manager: "${activePkgManager}". Run "${activePkgManager} install" now? (y/N): `);
|
|
903
1549
|
rl.close();
|
|
904
1550
|
|
|
905
1551
|
const normalizedAnswer = userPromptChoice.trim().toLowerCase();
|
|
906
1552
|
if (normalizedAnswer === 'y' || normalizedAnswer === 'yes') {
|
|
907
|
-
console.log(`\nā³ Executing automated asset installations
|
|
1553
|
+
console.log(`\nā³ Executing automated asset installations...`);
|
|
908
1554
|
try {
|
|
909
|
-
console.log(` Running: "${activePkgManager} install" inside current folder...`);
|
|
910
1555
|
execSync(`${activePkgManager} install`, { stdio: 'inherit', cwd: targetDir });
|
|
911
|
-
console.log(`\nš Project fully mapped,
|
|
1556
|
+
console.log(`\nš Project fully mapped, configured, and installed successfully!`);
|
|
912
1557
|
} catch (err) {
|
|
913
|
-
console.error(`\nā
|
|
1558
|
+
console.error(`\nā Installation returned an issue. Please run "${activePkgManager} install" manually.`);
|
|
914
1559
|
}
|
|
915
1560
|
} else {
|
|
916
|
-
console.log(`\nā¶ļø Skipping
|
|
1561
|
+
console.log(`\nā¶ļø Skipping install. Run "${activePkgManager} install" manually when ready.`);
|
|
917
1562
|
}
|
|
918
1563
|
}
|
|
919
1564
|
|
|
920
|
-
main();
|
|
1565
|
+
main();
|