pkg-scaffold 2.0.0 → 2.1.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/README.md +4 -4
- package/index.js +914 -27
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
`pkg-scaffold` is a high-performance CLI tool designed to audit, clean, and initialize your JavaScript/TypeScript workspaces. It goes far beyond simple scaffolding by analyzing your source code's Abstract Syntax Tree (AST) to find critical issues like undeclared dependencies, orphaned packages, and security leaks.
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
[](https://www.npmjs.com/package/pkg-scaffold)
|
|
9
|
+
[](https://www.npmjs.com/package/pkg-scaffold)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://github.com/DreamLongYT/pkg-scaffold/stargazers)
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
package/index.js
CHANGED
|
@@ -25,7 +25,80 @@ const REGEX_PATTERNS = {
|
|
|
25
25
|
syncFsCalls: /\.readFileSync|\.writeFileSync|\.mkdirSync|\.existsSync/g,
|
|
26
26
|
|
|
27
27
|
// Cryptographic Risk & Hardcoded Keyholes
|
|
28
|
-
secretKeys: /\b(secret|passwd|password|token|api_?key|private_?key)\s*=\s*['"`]([a-zA-Z0-9_\-\.]{8,})['"`]/gi
|
|
28
|
+
secretKeys: /\b(secret|passwd|password|token|api_?key|private_?key)\s*=\s*['"`]([a-zA-Z0-9_\-\.]{8,})['"`]/gi,
|
|
29
|
+
awsKeys: /AKIA[0-9A-Z]{16}/g,
|
|
30
|
+
googleCloudKeys: /AIza[0-9A-Za-z\-_]{35}/g,
|
|
31
|
+
stripeKeys: /sk_live_[0-9a-zA-Z]{24}/g,
|
|
32
|
+
slackKeys: /xox[baprs]-[0-9a-zA-Z]{10,48}/g,
|
|
33
|
+
githubTokens: /gh[pousr]_[a-zA-Z0-9]{36}/g,
|
|
34
|
+
rsaPrivateKeys: /-----BEGIN RSA PRIVATE KEY-----/g,
|
|
35
|
+
sshPrivateKeys: /-----BEGIN OPENSSH PRIVATE KEY-----/g,
|
|
36
|
+
pgpPrivateKeys: /-----BEGIN PGP PRIVATE KEY BLOCK-----/g,
|
|
37
|
+
|
|
38
|
+
// Insecure Patterns
|
|
39
|
+
insecureInnerHTML: /\.innerHTML\s*=/g,
|
|
40
|
+
insecureDocumentWrite: /document\.write\s*\(/g,
|
|
41
|
+
insecureDangerouslySet: /dangerouslySetInnerHTML/g,
|
|
42
|
+
insecureRegex: /\/\.\*\//g, // Common catastrophic backtracking
|
|
43
|
+
|
|
44
|
+
// New: Advanced Security Patterns
|
|
45
|
+
insecureCrypto: /crypto\.(?:createCipher|createDecipher|pbkdf2Sync)/g, // Deprecated/insecure crypto usage
|
|
46
|
+
sqlInjection: /(?:SELECT|INSERT|UPDATE|DELETE)\s+.*\s+FROM\s+.*\s+WHERE\s+.*\s*=\s*[""]?.*[""]?/i, // Basic SQL injection pattern
|
|
47
|
+
xssVulnerability: /<script\b[^>]*>[\s\S]*?<\/script>/i, // Basic XSS script tag detection
|
|
48
|
+
|
|
49
|
+
// New: Performance Patterns
|
|
50
|
+
largeImageImport: /import\s+.*\s+from\s+[""](?:.*\.(?:png|jpg|jpeg|gif|svg))[""]/g, // Direct import of large images
|
|
51
|
+
unoptimizedLoop: /for\s*\(let\s+i\s*=\s*0;\s*i\s*<\s*\w+\.length;\s*i\s*\+\+\)/g, // Simple for loop, can be optimized with for-of or forEach
|
|
52
|
+
|
|
53
|
+
// New: Framework-specific patterns (examples)
|
|
54
|
+
nextjsImageComponent: /<Image\s+[^>]*>/g, // Next.js Image component usage
|
|
55
|
+
nextjsFontOptimization: /next\/font/g, // Next.js Font optimization usage
|
|
56
|
+
nuxtAutoImport: /use(?:State|Fetch|AsyncData)/g, // Nuxt 3 auto-imports
|
|
57
|
+
sveltekitLoadFunction: /export\s+const\s+load\s*=/g, // SvelteKit load function
|
|
58
|
+
reactUseEffectNoDeps: /useEffect\s*\(\s*\([^)]*\)\s*=>\s*{[^}]*},\s*\[\s*\]\s*\)/g, // useEffect with empty dependency array (potential for stale closures)
|
|
59
|
+
|
|
60
|
+
// New: Advanced Security Patterns
|
|
61
|
+
insecureCrypto: /crypto\.(?:createCipher|createDecipher|pbkdf2Sync)/g, // Deprecated/insecure crypto usage
|
|
62
|
+
sqlInjection: /(?:SELECT|INSERT|UPDATE|DELETE)\s+.*\s+FROM\s+.*\s+WHERE\s+.*\s*=\s*[""]?.*[""]?/i, // Basic SQL injection pattern
|
|
63
|
+
xssVulnerability: /<script\b[^>]*>[\s\S]*?<\/script>/i, // Basic XSS script tag detection
|
|
64
|
+
|
|
65
|
+
// New: Performance Patterns
|
|
66
|
+
largeImageImport: /import\s+.*\s+from\s+[""](?:.*\.(?:png|jpg|jpeg|gif|svg))[""]/g, // Direct import of large images
|
|
67
|
+
unoptimizedLoop: /for\s*\(let\s+i\s*=\s*0;\s*i\s*<\s*\w+\.length;\s*i\s*\+\+\)/g, // Simple for loop, can be optimized with for-of or forEach
|
|
68
|
+
|
|
69
|
+
// New: Framework-specific patterns (examples)
|
|
70
|
+
nextjsImageComponent: /<Image\s+[^>]*>/g, // Next.js Image component usage
|
|
71
|
+
nextjsFontOptimization: /next\/font/g, // Next.js Font optimization usage
|
|
72
|
+
nuxtAutoImport: /use(?:State|Fetch|AsyncData)/g, // Nuxt 3 auto-imports
|
|
73
|
+
sveltekitLoadFunction: /export\s+const\s+load\s*=/g, // SvelteKit load function
|
|
74
|
+
reactUseEffectNoDeps: /useEffect\s*\(\s*\([^)]*\)\s*=>\s*{[^}]*},\s*\[\s*\]\s*\)/g, // useEffect with empty dependency array (potential for stale closures)
|
|
75
|
+
|
|
76
|
+
// New: Advanced Security Patterns
|
|
77
|
+
insecureCrypto: /crypto\.(?:createCipher|createDecipher|pbkdf2Sync)/g, // Deprecated/insecure crypto usage
|
|
78
|
+
sqlInjection: /(?:SELECT|INSERT|UPDATE|DELETE)\s+.*\s+FROM\s+.*\s+WHERE\s+.*\s*=\s*[""]?.*[""]?/i, // Basic SQL injection pattern
|
|
79
|
+
xssVulnerability: /<script\b[^>]*>[\s\S]*?<\/script>/i, // Basic XSS script tag detection
|
|
80
|
+
|
|
81
|
+
// New: Performance Patterns
|
|
82
|
+
largeImageImport: /import\s+.*\s+from\s+[""](?:.*\.(?:png|jpg|jpeg|gif|svg))[""]/g, // Direct import of large images
|
|
83
|
+
unoptimizedLoop: /for\s*\(let\s+i\s*=\s*0;\s*i\s*<\s*\w+\.length;\s*i\s*\+\+\)/g, // Simple for loop, can be optimized with for-of or forEach
|
|
84
|
+
|
|
85
|
+
// New: Framework-specific patterns (examples)
|
|
86
|
+
nextjsImageComponent: /<Image\s+[^>]*>/g, // Next.js Image component usage
|
|
87
|
+
nextjsFontOptimization: /next\/font/g, // Next.js Font optimization usage
|
|
88
|
+
nuxtAutoImport: /use(?:State|Fetch|AsyncData)/g, // Nuxt 3 auto-imports
|
|
89
|
+
sveltekitLoadFunction: /export\s+const\s+load\s*=/g, // SvelteKit load function
|
|
90
|
+
reactUseEffectNoDeps: /useEffect\s*\(\s*\([^)]*\)\s*=>\s*{[^}]*},\s*\[\s*\]\s*\)/g, // useEffect with empty dependency array (potential for stale closures)
|
|
91
|
+
|
|
92
|
+
// Framework-specific patterns for deeper analysis
|
|
93
|
+
nextjsPage: /pages\/[^\/]+\.(js|jsx|ts|tsx)$/i,
|
|
94
|
+
nextjsApi: /pages\/api\/[^\/]+\.(js|jsx|ts|tsx)$/i,
|
|
95
|
+
nextjsComponent: /components\/[^\/]+\.(js|jsx|ts|tsx)$/i,
|
|
96
|
+
nuxtPage: /pages\/[^\/]+\.(vue|js|ts)$/i,
|
|
97
|
+
nuxtComponent: /components\/[^\/]+\.(vue|js|ts)$/i,
|
|
98
|
+
sveltekitPage: /src\/routes\/[^\/]+\/\+page\.(svelte|js|ts)$/i,
|
|
99
|
+
sveltekitComponent: /src\/lib\/[^\/]+\.(svelte|js|ts)$/i,
|
|
100
|
+
reactHook: /hooks\/[^\/]+\.(js|jsx|ts|tsx)$/i,
|
|
101
|
+
vueComposable: /composables\/[^\/]+\.(js|ts)$/i
|
|
29
102
|
};
|
|
30
103
|
|
|
31
104
|
// ============================================================
|
|
@@ -354,6 +427,212 @@ function readFileSyncNormalized(fullPath) {
|
|
|
354
427
|
return buffer.toString('utf8');
|
|
355
428
|
}
|
|
356
429
|
|
|
430
|
+
// ============================================================
|
|
431
|
+
// 🏗️ FRAMEWORK-SPECIFIC DEEP SCAN LOGIC
|
|
432
|
+
// ============================================================
|
|
433
|
+
class FrameworkAnalyzer {
|
|
434
|
+
static analyzeNextjsFile(filePath, content, stats) {
|
|
435
|
+
// Data Fetching Patterns (getServerSideProps, getStaticProps, getStaticPaths, Route Handlers)
|
|
436
|
+
if (filePath.includes("pages/") && content.includes("getServerSideProps")) {
|
|
437
|
+
stats.frameworkFiles.nextjs.dataFetching.set(filePath, "getServerSideProps");
|
|
438
|
+
stats.frameworkOptimizations.push(`Next.js: Consider using 'getStaticProps' or client-side fetching for '${path.relative(process.cwd(), filePath)}' if data is not highly dynamic.`);
|
|
439
|
+
}
|
|
440
|
+
if (filePath.includes("pages/") && content.includes("getStaticProps")) {
|
|
441
|
+
stats.frameworkFiles.nextjs.dataFetching.set(filePath, "getStaticProps");
|
|
442
|
+
}
|
|
443
|
+
if (filePath.includes("pages/") && content.includes("getStaticPaths")) {
|
|
444
|
+
stats.frameworkFiles.nextjs.dataFetching.set(filePath, "getStaticPaths");
|
|
445
|
+
}
|
|
446
|
+
if (filePath.includes("app/") && content.includes("export async function GET")) {
|
|
447
|
+
stats.frameworkFiles.nextjs.dataFetching.set(filePath, "Route Handler (GET)");
|
|
448
|
+
}
|
|
449
|
+
// More Next.js specific checks: Image optimization, Font optimization, Script optimization
|
|
450
|
+
if (content.includes("<img") && !content.includes("<Image")) {
|
|
451
|
+
stats.frameworkOptimizations.push(`Next.js: Use next/image for '${path.relative(process.cwd(), filePath)}' to optimize images.`);
|
|
452
|
+
}
|
|
453
|
+
if (content.includes("<link") && content.includes("googlefonts") && !content.includes("next/font")) {
|
|
454
|
+
stats.frameworkOptimizations.push(`Next.js: Use next/font for '${path.relative(process.cwd(), filePath)}' to optimize fonts.`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
static analyzeNuxtFile(filePath, content, stats) {
|
|
459
|
+
// Data Fetching Patterns (useAsyncData, useFetch)
|
|
460
|
+
if (content.includes("useAsyncData")) {
|
|
461
|
+
stats.frameworkFiles.nuxt.dataFetching.set(filePath, "useAsyncData");
|
|
462
|
+
}
|
|
463
|
+
if (content.includes("useFetch")) {
|
|
464
|
+
stats.frameworkFiles.nuxt.dataFetching.set(filePath, "useFetch");
|
|
465
|
+
}
|
|
466
|
+
// Nuxt specific checks: Auto-imports, module usage
|
|
467
|
+
if (filePath.includes("components/") && !content.includes("defineComponent")) {
|
|
468
|
+
stats.frameworkOptimizations.push(`Nuxt: Ensure components in '${path.relative(process.cwd(), filePath)}' are properly defined for auto-import or explicitly imported.`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
static analyzeSvelteKitFile(filePath, content, stats) {
|
|
473
|
+
// Data Fetching Patterns (load functions)
|
|
474
|
+
if (content.includes("export async function load")) {
|
|
475
|
+
stats.frameworkFiles.sveltekit.loadFunctions.set(filePath, "load");
|
|
476
|
+
}
|
|
477
|
+
// SvelteKit specific checks: endpoint usage, form actions
|
|
478
|
+
if (filePath.includes("src/routes/") && content.includes("export const actions")) {
|
|
479
|
+
stats.frameworkFiles.sveltekit.endpoints.add(filePath);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
static analyzeReactFile(filePath, content, stats) {
|
|
484
|
+
// React specific checks: useEffect dependencies, custom hooks
|
|
485
|
+
if (content.includes("useEffect(") && !content.includes("[]")) {
|
|
486
|
+
stats.frameworkOptimizations.push(`React: Check useEffect dependencies in '${path.relative(process.cwd(), filePath)}' to prevent unnecessary re-renders.`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
static analyzeVueFile(filePath, content, stats) {
|
|
491
|
+
// Vue specific checks: reactivity, component registration
|
|
492
|
+
if (content.includes("Vue.component")) {
|
|
493
|
+
stats.frameworkOptimizations.push(`Vue: Consider using single-file components or local registration for '${path.relative(process.cwd(), filePath)}' for better modularity.`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
static analyzeFile(filePath, content, stats, detectedFrameworks) {
|
|
498
|
+
if (detectedFrameworks.includes("next")) {
|
|
499
|
+
FrameworkAnalyzer.analyzeNextjsFile(filePath, content, stats);
|
|
500
|
+
}
|
|
501
|
+
if (detectedFrameworks.includes("nuxt")) {
|
|
502
|
+
FrameworkAnalyzer.analyzeNuxtFile(filePath, content, stats);
|
|
503
|
+
}
|
|
504
|
+
if (detectedFrameworks.includes("svelte")) {
|
|
505
|
+
FrameworkAnalyzer.analyzeSvelteKitFile(filePath, content, stats);
|
|
506
|
+
}
|
|
507
|
+
if (detectedFrameworks.includes("react")) {
|
|
508
|
+
FrameworkAnalyzer.analyzeReactFile(filePath, content, stats);
|
|
509
|
+
}
|
|
510
|
+
if (detectedFrameworks.includes("vue")) {
|
|
511
|
+
FrameworkAnalyzer.analyzeVueFile(filePath, content, stats);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ============================================================
|
|
517
|
+
// ⚙️ FRAMEWORK DETECTION ENGINE
|
|
518
|
+
// ============================================================
|
|
519
|
+
class FrameworkEngine {
|
|
520
|
+
static detect(targetDir, packageJson) {
|
|
521
|
+
const detected = new Set();
|
|
522
|
+
|
|
523
|
+
// Check package.json dependencies
|
|
524
|
+
const allDependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
525
|
+
if (allDependencies.next) detected.add("next");
|
|
526
|
+
if (allDependencies.nuxt) detected.add("nuxt");
|
|
527
|
+
if (allDependencies.sveltekit) detected.add("svelte"); // SvelteKit implies Svelte
|
|
528
|
+
if (allDependencies.react) detected.add("react");
|
|
529
|
+
if (allDependencies.vue) detected.add("vue");
|
|
530
|
+
|
|
531
|
+
// Check config files
|
|
532
|
+
if (fs.existsSync(path.join(targetDir, "next.config.js")) || fs.existsSync(path.join(targetDir, "next.config.mjs"))) detected.add("next");
|
|
533
|
+
if (fs.existsSync(path.join(targetDir, "nuxt.config.js")) || fs.existsSync(path.join(targetDir, "nuxt.config.ts"))) detected.add("nuxt");
|
|
534
|
+
if (fs.existsSync(path.join(targetDir, "svelte.config.js"))) detected.add("svelte");
|
|
535
|
+
if (fs.existsSync(path.join(targetDir, "vite.config.js")) || fs.existsSync(path.join(targetDir, "vite.config.ts"))) {
|
|
536
|
+
// Vite can be used with multiple frameworks, try to be more specific
|
|
537
|
+
if (allDependencies["@vitejs/plugin-react"]) detected.add("react");
|
|
538
|
+
if (allDependencies["@vitejs/plugin-vue"]) detected.add("vue");
|
|
539
|
+
if (allDependencies["@sveltejs/vite-plugin-svelte"]) detected.add("svelte");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return Array.from(detected);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ============================================================
|
|
547
|
+
// 🧩 TEMPLATE ENGINE (Hygen-level Customization)
|
|
548
|
+
// ============================================================
|
|
549
|
+
class TemplateEngine {
|
|
550
|
+
constructor(targetDir, safeQuestion) {
|
|
551
|
+
this.targetDir = targetDir;
|
|
552
|
+
this.templatesDir = path.join(targetDir, ".templates");
|
|
553
|
+
this.safeQuestion = safeQuestion;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async listTemplates() {
|
|
557
|
+
if (!fs.existsSync(this.templatesDir)) {
|
|
558
|
+
return [];
|
|
559
|
+
}
|
|
560
|
+
const templateFolders = fs.readdirSync(this.templatesDir, { withFileTypes: true })
|
|
561
|
+
.filter(dirent => dirent.isDirectory())
|
|
562
|
+
.map(dirent => dirent.name);
|
|
563
|
+
return templateFolders;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async generate(templateName, variables = {}) {
|
|
567
|
+
const templatePath = path.join(this.templatesDir, templateName);
|
|
568
|
+
if (!fs.existsSync(templatePath)) {
|
|
569
|
+
console.log(` ⚠️ Template '${templateName}' not found in ${this.templatesDir}`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
console.log(` 🚀 Generating from template '${templateName}'...`);
|
|
574
|
+
|
|
575
|
+
const templateFiles = this._getTemplateFiles(templatePath);
|
|
576
|
+
|
|
577
|
+
for (const file of templateFiles) {
|
|
578
|
+
const relativePath = path.relative(templatePath, file);
|
|
579
|
+
let targetFilePath = path.join(this.targetDir, this._renderString(relativePath, variables));
|
|
580
|
+
|
|
581
|
+
// Handle dynamic file names (e.g., _name_.js)
|
|
582
|
+
targetFilePath = targetFilePath.replace(/_([a-zA-Z0-9_]+)_/g, (match, p1) => {
|
|
583
|
+
return variables[p1] || match; // Replace with variable or keep original if not found
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
587
|
+
const renderedContent = this._renderString(content, variables);
|
|
588
|
+
|
|
589
|
+
fs.mkdirSync(path.dirname(targetFilePath), { recursive: true });
|
|
590
|
+
fs.writeFileSync(targetFilePath, renderedContent);
|
|
591
|
+
console.log(` ✅ Created: ${path.relative(this.targetDir, targetFilePath)}`);
|
|
592
|
+
}
|
|
593
|
+
console.log(` ✨ Template generation complete.`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
_getTemplateFiles(dir) {
|
|
597
|
+
let files = [];
|
|
598
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
599
|
+
for (const item of items) {
|
|
600
|
+
const fullPath = path.join(dir, item.name);
|
|
601
|
+
if (item.isDirectory()) {
|
|
602
|
+
files = files.concat(this._getTemplateFiles(fullPath));
|
|
603
|
+
} else {
|
|
604
|
+
files.push(fullPath);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return files;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
_renderString(templateString, variables) {
|
|
611
|
+
let result = templateString;
|
|
612
|
+
for (const key in variables) {
|
|
613
|
+
result = result.replace(new RegExp(`{{\s*${key}\s*}}`, 'g'), variables[key]);
|
|
614
|
+
}
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async promptForVariables(templateName) {
|
|
619
|
+
const templatePath = path.join(this.templatesDir, templateName);
|
|
620
|
+
const configPath = path.join(templatePath, "config.json");
|
|
621
|
+
const variables = {};
|
|
622
|
+
|
|
623
|
+
if (fs.existsSync(configPath)) {
|
|
624
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
625
|
+
if (config.prompts && Array.isArray(config.prompts)) {
|
|
626
|
+
for (const prompt of config.prompts) {
|
|
627
|
+
const answer = await this.safeQuestion(` ❓ ${prompt.message} (${prompt.name}): `);
|
|
628
|
+
variables[prompt.name] = answer || prompt.default || '';
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return variables;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
357
636
|
function buildAsciiTree(dir, prefix = '') {
|
|
358
637
|
const results = [];
|
|
359
638
|
try {
|
|
@@ -379,11 +658,14 @@ function buildAsciiTree(dir, prefix = '') {
|
|
|
379
658
|
// IMPROVED IMPORT EXTRACTION: handles TypeScript generics,
|
|
380
659
|
// type-only imports, re-exports, and dynamic imports
|
|
381
660
|
// ============================================================
|
|
382
|
-
function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLocations) {
|
|
661
|
+
function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLocations, exportedSymbols, stats, currentFilePath) {
|
|
383
662
|
walk.simple(ast, {
|
|
384
663
|
ImportDeclaration(node) {
|
|
385
|
-
const
|
|
664
|
+
const importSource = node.source.value;
|
|
665
|
+
const pkg = cleanPackageName(importSource);
|
|
666
|
+
|
|
386
667
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
668
|
+
// External package import
|
|
387
669
|
fileRawDeps.add(pkg);
|
|
388
670
|
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
389
671
|
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
@@ -405,6 +687,26 @@ function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLo
|
|
|
405
687
|
if (node.specifiers.length === 0) {
|
|
406
688
|
importedIdentifiers.get(pkg).add('__SIDE_EFFECT__');
|
|
407
689
|
}
|
|
690
|
+
} else if (importSource.startsWith('.') || importSource.startsWith('/')) {
|
|
691
|
+
// Local file import
|
|
692
|
+
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
693
|
+
const normalizedPath = path.normalize(resolvedPath);
|
|
694
|
+
|
|
695
|
+
if (!stats.localFileImports) stats.localFileImports = new Map();
|
|
696
|
+
if (!stats.localFileImports.has(normalizedPath)) {
|
|
697
|
+
stats.localFileImports.set(normalizedPath, new Set());
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
node.specifiers.forEach(spec => {
|
|
701
|
+
if (spec.type === 'ImportDefaultSpecifier' || spec.type === 'ImportNamespaceSpecifier') {
|
|
702
|
+
stats.localFileImports.get(normalizedPath).add(spec.local.name);
|
|
703
|
+
} else if (spec.type === 'ImportSpecifier') {
|
|
704
|
+
stats.localFileImports.get(normalizedPath).add(spec.local.name);
|
|
705
|
+
if (spec.imported && spec.imported.name !== spec.local.name) {
|
|
706
|
+
stats.localFileImports.get(normalizedPath).add(spec.imported.name);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
});
|
|
408
710
|
}
|
|
409
711
|
},
|
|
410
712
|
VariableDeclarator(node) {
|
|
@@ -445,6 +747,29 @@ function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLo
|
|
|
445
747
|
}
|
|
446
748
|
},
|
|
447
749
|
ExportNamedDeclaration(node) {
|
|
750
|
+
if (node.declaration) {
|
|
751
|
+
if (node.declaration.type === 'VariableDeclaration') {
|
|
752
|
+
node.declaration.declarations.forEach(decl => {
|
|
753
|
+
if (decl.id.type === 'Identifier') {
|
|
754
|
+
exportedSymbols.set(decl.id.name, { type: 'variable', loc: decl.id.loc.start });
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
} else if (node.declaration.type === 'FunctionDeclaration') {
|
|
758
|
+
if (node.declaration.id) {
|
|
759
|
+
exportedSymbols.set(node.declaration.id.name, { type: 'function', loc: node.declaration.id.loc.start });
|
|
760
|
+
}
|
|
761
|
+
} else if (node.declaration.type === 'ClassDeclaration') {
|
|
762
|
+
if (node.declaration.id) {
|
|
763
|
+
exportedSymbols.set(node.declaration.id.name, { type: 'class', loc: node.declaration.id.loc.start });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
} else if (node.specifiers) {
|
|
767
|
+
node.specifiers.forEach(spec => {
|
|
768
|
+
if (spec.exported.type === 'Identifier') {
|
|
769
|
+
exportedSymbols.set(spec.exported.name, { type: 'namedExport', loc: spec.exported.loc.start });
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
448
773
|
if (node.source && node.source.type === 'Literal' && typeof node.source.value === 'string') {
|
|
449
774
|
const pkg = cleanPackageName(node.source.value);
|
|
450
775
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
@@ -470,20 +795,29 @@ function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLo
|
|
|
470
795
|
// ============================================================
|
|
471
796
|
// REGEX FALLBACK: handles TypeScript files that acorn can't parse
|
|
472
797
|
// ============================================================
|
|
473
|
-
function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, importedLocations) {
|
|
798
|
+
function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, importedLocations, stats, currentFilePath) {
|
|
474
799
|
codeLines.forEach((line, lineIdx) => {
|
|
475
800
|
const lineNum = lineIdx + 1;
|
|
476
801
|
|
|
477
802
|
// import type { ... } from '...' — type-only, mark as side-effect
|
|
478
803
|
const typeImportMatch = line.match(/\bimport\s+type\s+\{[^}]*\}\s+from\s+['"]([^'"]+)['"]/);
|
|
479
804
|
if (typeImportMatch) {
|
|
480
|
-
const
|
|
805
|
+
const importSource = typeImportMatch[1];
|
|
806
|
+
const pkg = cleanPackageName(importSource);
|
|
481
807
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
482
808
|
fileRawDeps.add(pkg);
|
|
483
809
|
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
484
810
|
importedIdentifiers.get(pkg).add('__TYPE_ONLY__');
|
|
485
811
|
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
486
812
|
importedLocations.get(pkg).push(lineNum);
|
|
813
|
+
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
814
|
+
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
815
|
+
const normalizedPath = path.normalize(resolvedPath);
|
|
816
|
+
if (!stats.localFileImports) stats.localFileImports = new Map();
|
|
817
|
+
if (!stats.localFileImports.has(normalizedPath)) {
|
|
818
|
+
stats.localFileImports.set(normalizedPath, new Set());
|
|
819
|
+
}
|
|
820
|
+
stats.localFileImports.get(normalizedPath).add('__TYPE_ONLY__'); // Mark as type-only imported
|
|
487
821
|
}
|
|
488
822
|
return;
|
|
489
823
|
}
|
|
@@ -493,13 +827,22 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
493
827
|
const esmDefaultMatch = line.match(/\bimport\s+(?:\*\s+as\s+)?([a-zA-Z0-9_$]+)\s+from\s+['"]([^'"]+)['"]/);
|
|
494
828
|
if (esmDefaultMatch) {
|
|
495
829
|
const id = esmDefaultMatch[1];
|
|
496
|
-
const
|
|
830
|
+
const importSource = esmDefaultMatch[2];
|
|
831
|
+
const pkg = cleanPackageName(importSource);
|
|
497
832
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
498
833
|
fileRawDeps.add(pkg);
|
|
499
834
|
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
500
835
|
importedIdentifiers.get(pkg).add(id);
|
|
501
836
|
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
502
837
|
importedLocations.get(pkg).push(lineNum);
|
|
838
|
+
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
839
|
+
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
840
|
+
const normalizedPath = path.normalize(resolvedPath);
|
|
841
|
+
if (!stats.localFileImports) stats.localFileImports = new Map();
|
|
842
|
+
if (!stats.localFileImports.has(normalizedPath)) {
|
|
843
|
+
stats.localFileImports.set(normalizedPath, new Set());
|
|
844
|
+
}
|
|
845
|
+
stats.localFileImports.get(normalizedPath).add(id);
|
|
503
846
|
}
|
|
504
847
|
return;
|
|
505
848
|
}
|
|
@@ -507,7 +850,8 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
507
850
|
// import { named, exports } from '...'
|
|
508
851
|
const esmNamedMatch = line.match(/\bimport\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/);
|
|
509
852
|
if (esmNamedMatch) {
|
|
510
|
-
const
|
|
853
|
+
const importSource = esmNamedMatch[2];
|
|
854
|
+
const pkg = cleanPackageName(importSource);
|
|
511
855
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
512
856
|
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
513
857
|
fileRawDeps.add(pkg);
|
|
@@ -521,6 +865,20 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
521
865
|
});
|
|
522
866
|
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
523
867
|
importedLocations.get(pkg).push(lineNum);
|
|
868
|
+
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
869
|
+
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
870
|
+
const normalizedPath = path.normalize(resolvedPath);
|
|
871
|
+
if (!stats.localFileImports) stats.localFileImports = new Map();
|
|
872
|
+
if (!stats.localFileImports.has(normalizedPath)) {
|
|
873
|
+
stats.localFileImports.set(normalizedPath, new Set());
|
|
874
|
+
}
|
|
875
|
+
esmNamedMatch[1].split(',').forEach(part => {
|
|
876
|
+
const chunk = part.trim();
|
|
877
|
+
if (!chunk) return;
|
|
878
|
+
const id = chunk.includes(' as ') ? chunk.split(' as ')[1].trim() : chunk;
|
|
879
|
+
stats.localFileImports.get(normalizedPath).add(id);
|
|
880
|
+
if (chunk.includes(' as ')) stats.localFileImports.get(normalizedPath).add(chunk.split(' as ')[0].trim());
|
|
881
|
+
});
|
|
524
882
|
}
|
|
525
883
|
return;
|
|
526
884
|
}
|
|
@@ -528,13 +886,22 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
528
886
|
// Side-effect only: import '...'
|
|
529
887
|
const sideEffectMatch = line.match(/\bimport\s+['"]([^'"]+)['"]/);
|
|
530
888
|
if (sideEffectMatch) {
|
|
531
|
-
const
|
|
889
|
+
const importSource = sideEffectMatch[1];
|
|
890
|
+
const pkg = cleanPackageName(importSource);
|
|
532
891
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
533
892
|
fileRawDeps.add(pkg);
|
|
534
893
|
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
535
894
|
importedIdentifiers.get(pkg).add('__SIDE_EFFECT__');
|
|
536
895
|
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
537
896
|
importedLocations.get(pkg).push(lineNum);
|
|
897
|
+
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
898
|
+
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
899
|
+
const normalizedPath = path.normalize(resolvedPath);
|
|
900
|
+
if (!stats.localFileImports) stats.localFileImports = new Map();
|
|
901
|
+
if (!stats.localFileImports.has(normalizedPath)) {
|
|
902
|
+
stats.localFileImports.set(normalizedPath, new Set());
|
|
903
|
+
}
|
|
904
|
+
stats.localFileImports.get(normalizedPath).add('__SIDE_EFFECT__');
|
|
538
905
|
}
|
|
539
906
|
return;
|
|
540
907
|
}
|
|
@@ -543,13 +910,22 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
543
910
|
const cjsMatch = line.match(/\b(?:const|let|var)\s+([a-zA-Z0-9_$]+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
544
911
|
if (cjsMatch) {
|
|
545
912
|
const id = cjsMatch[1];
|
|
546
|
-
const
|
|
913
|
+
const importSource = cjsMatch[2];
|
|
914
|
+
const pkg = cleanPackageName(importSource);
|
|
547
915
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
548
916
|
fileRawDeps.add(pkg);
|
|
549
917
|
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
550
918
|
importedIdentifiers.get(pkg).add(id);
|
|
551
919
|
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
552
920
|
importedLocations.get(pkg).push(lineNum);
|
|
921
|
+
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
922
|
+
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
923
|
+
const normalizedPath = path.normalize(resolvedPath);
|
|
924
|
+
if (!stats.localFileImports) stats.localFileImports = new Map();
|
|
925
|
+
if (!stats.localFileImports.has(normalizedPath)) {
|
|
926
|
+
stats.localFileImports.set(normalizedPath, new Set());
|
|
927
|
+
}
|
|
928
|
+
stats.localFileImports.get(normalizedPath).add(id);
|
|
553
929
|
}
|
|
554
930
|
return;
|
|
555
931
|
}
|
|
@@ -557,7 +933,8 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
557
933
|
// const { a, b } = require('...')
|
|
558
934
|
const cjsDestructMatch = line.match(/\b(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
559
935
|
if (cjsDestructMatch) {
|
|
560
|
-
const
|
|
936
|
+
const importSource = cjsDestructMatch[2];
|
|
937
|
+
const pkg = cleanPackageName(importSource);
|
|
561
938
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
562
939
|
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
563
940
|
fileRawDeps.add(pkg);
|
|
@@ -569,6 +946,19 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
569
946
|
});
|
|
570
947
|
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
571
948
|
importedLocations.get(pkg).push(lineNum);
|
|
949
|
+
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
950
|
+
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
951
|
+
const normalizedPath = path.normalize(resolvedPath);
|
|
952
|
+
if (!stats.localFileImports) stats.localFileImports = new Map();
|
|
953
|
+
if (!stats.localFileImports.has(normalizedPath)) {
|
|
954
|
+
stats.localFileImports.set(normalizedPath, new Set());
|
|
955
|
+
}
|
|
956
|
+
cjsDestructMatch[1].split(',').forEach(part => {
|
|
957
|
+
const chunk = part.trim();
|
|
958
|
+
if (!chunk) return;
|
|
959
|
+
const id = chunk.includes(':') ? chunk.split(':')[1].trim() : chunk;
|
|
960
|
+
stats.localFileImports.get(normalizedPath).add(id);
|
|
961
|
+
});
|
|
572
962
|
}
|
|
573
963
|
return;
|
|
574
964
|
}
|
|
@@ -576,13 +966,22 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
576
966
|
// Dynamic import: import('...')
|
|
577
967
|
const dynamicMatch = line.match(/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
578
968
|
if (dynamicMatch) {
|
|
579
|
-
const
|
|
969
|
+
const importSource = dynamicMatch[1];
|
|
970
|
+
const pkg = cleanPackageName(importSource);
|
|
580
971
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
581
972
|
fileRawDeps.add(pkg);
|
|
582
973
|
if (!importedIdentifiers.has(pkg)) importedIdentifiers.set(pkg, new Set());
|
|
583
974
|
importedIdentifiers.get(pkg).add('__DYNAMIC__');
|
|
584
975
|
if (!importedLocations.has(pkg)) importedLocations.set(pkg, []);
|
|
585
976
|
importedLocations.get(pkg).push(lineNum);
|
|
977
|
+
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
978
|
+
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
979
|
+
const normalizedPath = path.normalize(resolvedPath);
|
|
980
|
+
if (!stats.localFileImports) stats.localFileImports = new Map();
|
|
981
|
+
if (!stats.localFileImports.has(normalizedPath)) {
|
|
982
|
+
stats.localFileImports.set(normalizedPath, new Set());
|
|
983
|
+
}
|
|
984
|
+
stats.localFileImports.get(normalizedPath).add('__DYNAMIC__');
|
|
586
985
|
}
|
|
587
986
|
}
|
|
588
987
|
});
|
|
@@ -664,7 +1063,7 @@ function detectOrphanedDependencies(declaredDeps, allImportedPackages, binariesU
|
|
|
664
1063
|
// ============================================================
|
|
665
1064
|
// HIGH PERFORMANCE AST WORKSPACE PARSING ENGINE
|
|
666
1065
|
// ============================================================
|
|
667
|
-
function scanWorkspace(dir, stats, rootNamespace) {
|
|
1066
|
+
function scanWorkspace(dir, stats, rootNamespace, detectedFrameworks) {
|
|
668
1067
|
const files = fs.readdirSync(dir);
|
|
669
1068
|
|
|
670
1069
|
for (const file of files) {
|
|
@@ -673,7 +1072,7 @@ function scanWorkspace(dir, stats, rootNamespace) {
|
|
|
673
1072
|
|
|
674
1073
|
if (stat.isDirectory()) {
|
|
675
1074
|
if (!IGNORED_DIRS.has(file) && !file.startsWith('.')) {
|
|
676
|
-
scanWorkspace(fullPath, stats, rootNamespace);
|
|
1075
|
+
scanWorkspace(fullPath, stats, rootNamespace, detectedFrameworks);
|
|
677
1076
|
}
|
|
678
1077
|
} else {
|
|
679
1078
|
const ext = path.extname(file);
|
|
@@ -683,6 +1082,17 @@ function scanWorkspace(dir, stats, rootNamespace) {
|
|
|
683
1082
|
if (ext === '.ts' || ext === '.tsx') stats.tsFiles++;
|
|
684
1083
|
if (ext === '.js' || ext === '.jsx' || ext === '.mjs') stats.jsFiles++;
|
|
685
1084
|
|
|
1085
|
+
// Framework-specific file type detection
|
|
1086
|
+
if (REGEX_PATTERNS.nextjsPage.test(fullPath)) stats.frameworkFiles.nextjs.pages.add(fullPath);
|
|
1087
|
+
if (REGEX_PATTERNS.nextjsApi.test(fullPath)) stats.frameworkFiles.nextjs.apiRoutes.add(fullPath);
|
|
1088
|
+
if (REGEX_PATTERNS.nextjsComponent.test(fullPath)) stats.frameworkFiles.nextjs.components.add(fullPath);
|
|
1089
|
+
if (REGEX_PATTERNS.nuxtPage.test(fullPath)) stats.frameworkFiles.nuxt.pages.add(fullPath);
|
|
1090
|
+
if (REGEX_PATTERNS.nuxtComponent.test(fullPath)) stats.frameworkFiles.nuxt.components.add(fullPath);
|
|
1091
|
+
if (REGEX_PATTERNS.sveltekitPage.test(fullPath)) stats.frameworkFiles.sveltekit.pages.add(fullPath);
|
|
1092
|
+
if (REGEX_PATTERNS.sveltekitComponent.test(fullPath)) stats.frameworkFiles.sveltekit.components.add(fullPath);
|
|
1093
|
+
if (REGEX_PATTERNS.reactHook.test(fullPath)) stats.frameworkFiles.react.hooks.add(fullPath);
|
|
1094
|
+
if (REGEX_PATTERNS.vueComposable.test(fullPath)) stats.frameworkFiles.vue.composables.add(fullPath);
|
|
1095
|
+
|
|
686
1096
|
if (VALID_EXTENSIONS.has(ext)) {
|
|
687
1097
|
stats.scannedFiles++;
|
|
688
1098
|
const rawContent = readFileSyncNormalized(fullPath);
|
|
@@ -696,15 +1106,55 @@ function scanWorkspace(dir, stats, rootNamespace) {
|
|
|
696
1106
|
|
|
697
1107
|
analyzeCodeStyle(content, stats);
|
|
698
1108
|
|
|
699
|
-
// Universal Cryptographic Leak Interception
|
|
700
|
-
REGEX_PATTERNS
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1109
|
+
// Universal Cryptographic Leak Interception (Expanded)
|
|
1110
|
+
for (const [patternName, patternRegex] of Object.entries(REGEX_PATTERNS)) {
|
|
1111
|
+
if (patternName.startsWith("secretKeys") || patternName.endsWith("Keys") || patternName.endsWith("Tokens")) {
|
|
1112
|
+
patternRegex.lastIndex = 0;
|
|
1113
|
+
let match;
|
|
1114
|
+
while ((match = patternRegex.exec(content)) !== null) {
|
|
1115
|
+
const keyName = match[1] || patternName; // Use patternName if no specific key name is captured
|
|
1116
|
+
const secretValue = match[2] || match[0]; // Use full match if no specific value is captured
|
|
1117
|
+
const envVarName = `${rootNamespace.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_${keyName.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
|
|
1118
|
+
stats.discoveredSecrets.push({ filePath: fullPath, keyName, secretValue, envVarName, type: patternName });
|
|
1119
|
+
stats.envVars.add(envVarName);
|
|
1120
|
+
}
|
|
1121
|
+
} else if (patternName.startsWith("insecure")) {
|
|
1122
|
+
patternRegex.lastIndex = 0;
|
|
1123
|
+
let match;
|
|
1124
|
+
while ((match = patternRegex.exec(content)) !== null) {
|
|
1125
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
1126
|
+
if (patternName === "insecureCrypto") {
|
|
1127
|
+
stats.quality.insecureCryptoUsage.push({ filePath: fullPath, type: patternName, line, code: match[0] });
|
|
1128
|
+
} else if (patternName === "sqlInjection") {
|
|
1129
|
+
stats.quality.sqlInjectionVulnerabilities.push({ filePath: fullPath, type: patternName, line, code: match[0] });
|
|
1130
|
+
} else if (patternName === "xssVulnerability") {
|
|
1131
|
+
stats.quality.xssVulnerabilities.push({ filePath: fullPath, type: patternName, line, code: match[0] });
|
|
1132
|
+
} else {
|
|
1133
|
+
stats.quality.insecurePatterns.push({ filePath: fullPath, type: patternName, line, code: match[0] });
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
} else if (patternName.startsWith("largeImageImport")) {
|
|
1137
|
+
patternRegex.lastIndex = 0;
|
|
1138
|
+
let match;
|
|
1139
|
+
while ((match = patternRegex.exec(content)) !== null) {
|
|
1140
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
1141
|
+
stats.quality.largeImageImports.push({ filePath: fullPath, type: patternName, line, code: match[0] });
|
|
1142
|
+
}
|
|
1143
|
+
} else if (patternName.startsWith("unoptimizedLoop")) {
|
|
1144
|
+
patternRegex.lastIndex = 0;
|
|
1145
|
+
let match;
|
|
1146
|
+
while ((match = patternRegex.exec(content)) !== null) {
|
|
1147
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
1148
|
+
stats.quality.unoptimizedLoops.push({ filePath: fullPath, type: patternName, line, code: match[0] });
|
|
1149
|
+
}
|
|
1150
|
+
} else if (patternName.startsWith("nextjs") || patternName.startsWith("nuxt") || patternName.startsWith("sveltekit") || patternName.startsWith("react") || patternName.startsWith("vue")) {
|
|
1151
|
+
patternRegex.lastIndex = 0;
|
|
1152
|
+
let match;
|
|
1153
|
+
while ((match = patternRegex.exec(content)) !== null) {
|
|
1154
|
+
const line = content.substring(0, match.index).split("\n").length;
|
|
1155
|
+
stats.quality.frameworkSpecificIssues.push({ filePath: fullPath, type: patternName, line, code: match[0] });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
708
1158
|
}
|
|
709
1159
|
|
|
710
1160
|
// Global Regex Environmental Extraction Module
|
|
@@ -719,6 +1169,9 @@ function scanWorkspace(dir, stats, rootNamespace) {
|
|
|
719
1169
|
|
|
720
1170
|
if (content.includes('import ') || content.includes('export ')) stats.usesEsm = true;
|
|
721
1171
|
|
|
1172
|
+
// Perform framework-specific analysis
|
|
1173
|
+
FrameworkAnalyzer.analyzeFile(fullPath, content, stats, detectedFrameworks);
|
|
1174
|
+
|
|
722
1175
|
// --- AST Parsing (preferred) ---
|
|
723
1176
|
let ast = null;
|
|
724
1177
|
try {
|
|
@@ -730,10 +1183,14 @@ function scanWorkspace(dir, stats, rootNamespace) {
|
|
|
730
1183
|
}
|
|
731
1184
|
|
|
732
1185
|
if (ast) {
|
|
733
|
-
|
|
1186
|
+
const currentFileExportedSymbols = new Map();
|
|
1187
|
+
extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLocations, currentFileExportedSymbols, stats, fullPath);
|
|
1188
|
+
if (currentFileExportedSymbols.size > 0) {
|
|
1189
|
+
stats.exportedSymbols.set(fullPath, currentFileExportedSymbols);
|
|
1190
|
+
}
|
|
734
1191
|
} else {
|
|
735
1192
|
// Regex fallback for TypeScript generics / decorators / etc.
|
|
736
|
-
extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, importedLocations);
|
|
1193
|
+
extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, importedLocations, stats, fullPath);
|
|
737
1194
|
}
|
|
738
1195
|
|
|
739
1196
|
// Register all deps found in this file
|
|
@@ -766,6 +1223,11 @@ function scanWorkspace(dir, stats, rootNamespace) {
|
|
|
766
1223
|
}
|
|
767
1224
|
|
|
768
1225
|
async function main() {
|
|
1226
|
+
if (process.env.INIT_CWD && !process.env.NPX_CLI_JS) {
|
|
1227
|
+
console.log("\x1b[31m%s\x1b[0m", "🛑 Wait! Do not install this package locally.");
|
|
1228
|
+
console.log("Please run it directly using: \x1b[36mnpx pkg-scaffold\x1b[0m\n");
|
|
1229
|
+
process.exit(1);
|
|
1230
|
+
}
|
|
769
1231
|
const targetDir = process.cwd();
|
|
770
1232
|
const folderName = path.basename(targetDir);
|
|
771
1233
|
const gitInfo = getGitIdentity();
|
|
@@ -775,7 +1237,7 @@ async function main() {
|
|
|
775
1237
|
rl.on('close', () => { rlClosed = true; });
|
|
776
1238
|
const safeQuestion = async (prompt) => {
|
|
777
1239
|
if (rlClosed || !process.stdin.readable) return '';
|
|
778
|
-
try { return await
|
|
1240
|
+
try { return await rl.question(prompt); } catch { return ''; }
|
|
779
1241
|
};
|
|
780
1242
|
|
|
781
1243
|
const stats = {
|
|
@@ -785,11 +1247,29 @@ async function main() {
|
|
|
785
1247
|
allImportedPackages: new Set(),
|
|
786
1248
|
envVars: new Set(),
|
|
787
1249
|
style: { semiCount: 0, noSemiCount: 0, tabCount: 0, space2Count: 0, space4Count: 0 },
|
|
788
|
-
quality: {
|
|
1250
|
+
quality: {
|
|
1251
|
+
varCount: 0,
|
|
1252
|
+
hasEval: false,
|
|
1253
|
+
syncFsCount: 0,
|
|
1254
|
+
insecurePatterns: [],
|
|
1255
|
+
complexRegexes: [],
|
|
1256
|
+
insecureCryptoUsage: [],
|
|
1257
|
+
sqlInjectionVulnerabilities: [],
|
|
1258
|
+
xssVulnerabilities: [],
|
|
1259
|
+
largeImageImports: [],
|
|
1260
|
+
unoptimizedLoops: [],
|
|
1261
|
+
frameworkSpecificIssues: []
|
|
1262
|
+
},
|
|
789
1263
|
phantomInjections: new Map(),
|
|
790
1264
|
discoveredSecrets: [],
|
|
1265
|
+
insecureCodePatterns: [], // New: detailed insecure code patterns
|
|
791
1266
|
subWorkspaces: [],
|
|
792
1267
|
conflictingLockfiles: [],
|
|
1268
|
+
exportedSymbols: new Map(), // filePath -> Map<symbolName, { type: 'function'|'variable'|'class', loc: {line, col} }>
|
|
1269
|
+
usedExports: new Map(), // filePath -> Set<symbolName> (exports from this file that are used elsewhere)
|
|
1270
|
+
unusedFiles: new Set(), // Files that are never imported/referenced
|
|
1271
|
+
unusedExportsPerFile: new Map(), // filePath -> Set<symbolName> (exports from this file that are not used anywhere)
|
|
1272
|
+
localFileImports: new Map(), // filePath -> Set<importedSymbol> (local imports from this file)
|
|
793
1273
|
unusedDepsInCode: new Set(),
|
|
794
1274
|
unusedImportsPerFile: new Map(),
|
|
795
1275
|
filesWithEnvVars: new Set(),
|
|
@@ -799,6 +1279,14 @@ async function main() {
|
|
|
799
1279
|
ghostDependencies: new Set(), // used in code, missing from package.json
|
|
800
1280
|
orphanedDependencies: new Set(), // in package.json, never imported
|
|
801
1281
|
deprecatedPackages: new Map(), // pkg -> deprecation message
|
|
1282
|
+
frameworkFiles: {
|
|
1283
|
+
nextjs: { pages: new Set(), apiRoutes: new Set(), components: new Set(), dataFetching: new Map(), optimizations: [] },
|
|
1284
|
+
nuxt: { pages: new Set(), components: new Set(), modules: new Set(), dataFetching: new Map(), optimizations: [] },
|
|
1285
|
+
sveltekit: { pages: new Set(), components: new Set(), endpoints: new Set(), loadFunctions: new Map(), optimizations: [] },
|
|
1286
|
+
react: { hooks: new Set(), components: new Set(), optimizations: [] },
|
|
1287
|
+
vue: { composables: new Set(), components: new Set(), optimizations: [] },
|
|
1288
|
+
},
|
|
1289
|
+
frameworkOptimizations: [], // General framework-agnostic optimizations
|
|
802
1290
|
};
|
|
803
1291
|
|
|
804
1292
|
const activePkgManager = detectPackageManager(targetDir, stats);
|
|
@@ -808,6 +1296,10 @@ async function main() {
|
|
|
808
1296
|
let preExistingDevDeps = [];
|
|
809
1297
|
let existingPackageJson = null;
|
|
810
1298
|
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
|
|
1302
|
+
|
|
811
1303
|
console.log(`\n${'═'.repeat(67)}`);
|
|
812
1304
|
console.log(`🚀 pkg-scaffold v2.0: Advanced Dependency Intelligence Engine`);
|
|
813
1305
|
console.log(`${'═'.repeat(67)}\n`);
|
|
@@ -850,6 +1342,10 @@ async function main() {
|
|
|
850
1342
|
if (existingPackageJson.dependencies) preExistingDeps = Object.keys(existingPackageJson.dependencies);
|
|
851
1343
|
if (existingPackageJson.devDependencies) preExistingDevDeps = Object.keys(existingPackageJson.devDependencies);
|
|
852
1344
|
|
|
1345
|
+
// Detect frameworks after packageJson is loaded
|
|
1346
|
+
const detectedFrameworks = FrameworkEngine.detect(targetDir, existingPackageJson);
|
|
1347
|
+
stats.detectedFrameworks = detectedFrameworks;
|
|
1348
|
+
|
|
853
1349
|
const combinedDeps = [...preExistingDeps, ...preExistingDevDeps];
|
|
854
1350
|
let brokenEcosystem = combinedDeps.length === 0;
|
|
855
1351
|
|
|
@@ -883,9 +1379,12 @@ async function main() {
|
|
|
883
1379
|
|
|
884
1380
|
// --- Workspace scan ---
|
|
885
1381
|
console.log(`\n🔬 Scanning workspace source files...`);
|
|
886
|
-
scanWorkspace(targetDir, stats, folderName);
|
|
1382
|
+
scanWorkspace(targetDir, stats, folderName, detectedFrameworks);
|
|
887
1383
|
console.log(` ✅ Scanned ${stats.scannedFiles} source file(s) | TS: ${stats.tsFiles} | JS: ${stats.jsFiles}`);
|
|
888
1384
|
|
|
1385
|
+
// Build dependency graph for advanced analysis
|
|
1386
|
+
const dependencyGraph = new DependencyGraph(stats);
|
|
1387
|
+
|
|
889
1388
|
// --- Binary-to-package resolution ---
|
|
890
1389
|
const binariesInScripts = existingPackageJson ? getBinariesFromPackageJson(existingPackageJson) : [];
|
|
891
1390
|
const resolvedBinaryPackages = new Set();
|
|
@@ -1526,6 +2025,7 @@ ${activePkgManager} install
|
|
|
1526
2025
|
console.log(`\n📦 Auto-scaffolding pipeline complete!`);
|
|
1527
2026
|
|
|
1528
2027
|
// Summary report
|
|
2028
|
+
postProcessAnalysis(stats, dependencyGraph);
|
|
1529
2029
|
console.log(`\n${'═'.repeat(67)}`);
|
|
1530
2030
|
console.log(`📊 DEPENDENCY INTELLIGENCE SUMMARY`);
|
|
1531
2031
|
console.log(`${'═'.repeat(67)}`);
|
|
@@ -1537,14 +2037,51 @@ ${activePkgManager} install
|
|
|
1537
2037
|
console.log(` 🗑️ Orphaned deps (unused): ${stats.orphanedDependencies.size}`);
|
|
1538
2038
|
if (allDiscoveredUnused.size > 0)
|
|
1539
2039
|
console.log(` ⚡ Unused imports: ${allDiscoveredUnused.size}`);
|
|
2040
|
+
if (stats.unusedExportsPerFile.size > 0) {
|
|
2041
|
+
console.log(` 📤 Unused exports: ${Array.from(stats.unusedExportsPerFile.values()).reduce((acc, val) => acc + val.size, 0)} in ${stats.unusedExportsPerFile.size} files`);
|
|
2042
|
+
}
|
|
2043
|
+
if (stats.unusedFiles.size > 0) {
|
|
2044
|
+
console.log(` 🗑️ Unused files: ${stats.unusedFiles.size}`);
|
|
2045
|
+
}
|
|
1540
2046
|
if (stats.deprecatedPackages.size > 0)
|
|
1541
2047
|
console.log(` 📛 Deprecated packages: ${stats.deprecatedPackages.size}`);
|
|
1542
2048
|
if (stats.phantomInjections.size > 0)
|
|
1543
2049
|
console.log(` 👻 Phantom injections: ${stats.phantomInjections.size} file(s)`);
|
|
1544
2050
|
if (stats.discoveredSecrets.length > 0)
|
|
1545
2051
|
console.log(` 🔐 Hardcoded secrets: ${stats.discoveredSecrets.length} — \x1b[31mSECURITY RISK\x1b[0m`);
|
|
2052
|
+
if (stats.quality.insecureCryptoUsage.length > 0)
|
|
2053
|
+
console.log(` 🚫 Insecure Crypto: ${stats.quality.insecureCryptoUsage.length} — \x1b[31mSECURITY RISK\x1b[0m`);
|
|
2054
|
+
if (stats.quality.sqlInjectionVulnerabilities.length > 0)
|
|
2055
|
+
console.log(` 💉 SQL Injection: ${stats.quality.sqlInjectionVulnerabilities.length} — \x1b[31mSECURITY RISK\x1b[0m`);
|
|
2056
|
+
if (stats.quality.xssVulnerabilities.length > 0)
|
|
2057
|
+
console.log(` 🌐 XSS Vulnerabilities: ${stats.quality.xssVulnerabilities.length} — \x1b[31mSECURITY RISK\x1b[0m`);
|
|
2058
|
+
if (stats.quality.largeImageImports.length > 0)
|
|
2059
|
+
console.log(` 🖼️ Large Image Imports: ${stats.quality.largeImageImports.length} — \x1b[33mPERFORMANCE WARNING\x1b[0m`);
|
|
2060
|
+
if (stats.quality.unoptimizedLoops.length > 0)
|
|
2061
|
+
console.log(` 🐌 Unoptimized Loops: ${stats.quality.unoptimizedLoops.length} — \x1b[33mPERFORMANCE WARNING\x1b[0m`);
|
|
2062
|
+
if (stats.quality.frameworkSpecificIssues.length > 0)
|
|
2063
|
+
console.log(` 🧩 Framework Issues: ${stats.quality.frameworkSpecificIssues.length} — \x1b[33mFRAMEWORK OPTIMIZATION\x1b[0m`);
|
|
1546
2064
|
console.log(`${'═'.repeat(67)}`);
|
|
1547
2065
|
|
|
2066
|
+
// 6. Hygen-like Templating and Scaffolding
|
|
2067
|
+
const templateManager = new TemplateManager(targetDir, safeQuestion);
|
|
2068
|
+
const availableTemplates = await templateManager.listAvailableTemplates();
|
|
2069
|
+
|
|
2070
|
+
if (availableTemplates.length > 0) {
|
|
2071
|
+
console.log(`\n🧩 \x1b[1mCustom Templating Engine Detected:\x1b[0m`);
|
|
2072
|
+
console.log(` Available templates: ${availableTemplates.join(", ")}`);
|
|
2073
|
+
const useTemplate = await safeQuestion(`❓ Do you want to generate code from a template? (y/N): `);
|
|
2074
|
+
if (useTemplate.toLowerCase() === 'y') {
|
|
2075
|
+
const chosenTemplate = await safeQuestion(`❓ Enter template name: `);
|
|
2076
|
+
if (availableTemplates.includes(chosenTemplate)) {
|
|
2077
|
+
const templateVars = await templateManager.promptForVariables(chosenTemplate);
|
|
2078
|
+
await templateManager.generate(chosenTemplate, templateVars);
|
|
2079
|
+
} else {
|
|
2080
|
+
console.log(` ⚠️ Template '${chosenTemplate}' not found.`);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
1548
2085
|
const userPromptChoice = await safeQuestion(`❓ Detected package manager: "${activePkgManager}". Run "${activePkgManager} install" now? (y/N): `);
|
|
1549
2086
|
rl.close();
|
|
1550
2087
|
|
|
@@ -1563,3 +2100,353 @@ ${activePkgManager} install
|
|
|
1563
2100
|
}
|
|
1564
2101
|
|
|
1565
2102
|
main();
|
|
2103
|
+
|
|
2104
|
+
// ============================================================
|
|
2105
|
+
// 📊 POST-PROCESSING ANALYSIS: Unused Exports, Unused Files
|
|
2106
|
+
// ============================================================
|
|
2107
|
+
function postProcessAnalysis(stats, dependencyGraph) {
|
|
2108
|
+
// Initialize all scanned files as potentially unused
|
|
2109
|
+
const allScannedFiles = new Set(Array.from(stats.exportedSymbols.keys()));
|
|
2110
|
+
stats.unusedFiles = new Set(allScannedFiles);
|
|
2111
|
+
|
|
2112
|
+
// Determine used exports and identify used files
|
|
2113
|
+
for (const [importerFilePath, importedSymbols] of stats.localFileImports.entries()) {
|
|
2114
|
+
// Remove importerFilePath from unusedFiles if it imports something
|
|
2115
|
+
if (importedSymbols.size > 0) {
|
|
2116
|
+
stats.unusedFiles.delete(importerFilePath);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
for (const [exportedFilePath, exportedSymbolsMap] of stats.exportedSymbols.entries()) {
|
|
2120
|
+
// If importerFilePath imports from exportedFilePath
|
|
2121
|
+
if (importerFilePath === exportedFilePath) {
|
|
2122
|
+
// This is a self-import or internal reference, not a cross-file import for export usage
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// Check if any symbol from exportedFilePath is imported by importerFilePath
|
|
2127
|
+
for (const importedSymbol of importedSymbols) {
|
|
2128
|
+
if (exportedSymbolsMap.has(importedSymbol)) {
|
|
2129
|
+
if (!stats.usedExports.has(exportedFilePath)) {
|
|
2130
|
+
stats.usedExports.set(exportedFilePath, new Set());
|
|
2131
|
+
}
|
|
2132
|
+
stats.usedExports.get(exportedFilePath).add(importedSymbol);
|
|
2133
|
+
stats.unusedFiles.delete(exportedFilePath); // Mark as used
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
// Identify unused exports per file
|
|
2140
|
+
for (const [filePath, exportedSymbolsMap] of stats.exportedSymbols.entries()) {
|
|
2141
|
+
const used = stats.usedExports.get(filePath) || new Set();
|
|
2142
|
+
const unused = new Set();
|
|
2143
|
+
for (const [symbolName, symbolInfo] of exportedSymbolsMap.entries()) {
|
|
2144
|
+
if (!used.has(symbolName)) {
|
|
2145
|
+
unused.add(symbolName);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
if (unused.size > 0) {
|
|
2149
|
+
stats.unusedExportsPerFile.set(filePath, unused);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// Identify truly unused files: those that are never imported by any other file.
|
|
2154
|
+
const allScannedFiles = new Set(stats.scannedFiles); // All files that were processed
|
|
2155
|
+
const entryPoints = new Set(); // Files that are likely entry points (e.g., main, framework-specific entry points)
|
|
2156
|
+
|
|
2157
|
+
// Add main entry point if package.json has one
|
|
2158
|
+
if (stats.packageJson && stats.packageJson.main) {
|
|
2159
|
+
entryPoints.add(path.resolve(stats.targetDir, stats.packageJson.main));
|
|
2160
|
+
}
|
|
2161
|
+
if (stats.packageJson && stats.packageJson.module) {
|
|
2162
|
+
entryPoints.add(path.resolve(stats.targetDir, stats.packageJson.module));
|
|
2163
|
+
}
|
|
2164
|
+
if (stats.packageJson && stats.packageJson.type === 'module' && fs.existsSync(path.join(stats.targetDir, 'index.js'))) {
|
|
2165
|
+
entryPoints.add(path.resolve(stats.targetDir, 'index.js'));
|
|
2166
|
+
}
|
|
2167
|
+
if (stats.packageJson && stats.packageJson.type !== 'module' && fs.existsSync(path.join(stats.targetDir, 'index.cjs'))) {
|
|
2168
|
+
entryPoints.add(path.resolve(stats.targetDir, 'index.cjs'));
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// Add framework-specific entry points or files that are implicitly used
|
|
2172
|
+
if (stats.detectedFrameworks.includes('next')) {
|
|
2173
|
+
stats.frameworkFiles.nextjs.pages.forEach(file => entryPoints.add(file));
|
|
2174
|
+
stats.frameworkFiles.nextjs.apiRoutes.forEach(file => entryPoints.add(file));
|
|
2175
|
+
stats.frameworkFiles.nextjs.components.forEach(file => entryPoints.add(file));
|
|
2176
|
+
}
|
|
2177
|
+
if (stats.detectedFrameworks.includes('nuxt')) {
|
|
2178
|
+
stats.frameworkFiles.nuxt.pages.forEach(file => entryPoints.add(file));
|
|
2179
|
+
stats.frameworkFiles.nuxt.components.forEach(file => entryPoints.add(file));
|
|
2180
|
+
}
|
|
2181
|
+
if (stats.detectedFrameworks.includes('svelte')) {
|
|
2182
|
+
stats.frameworkFiles.sveltekit.pages.forEach(file => entryPoints.add(file));
|
|
2183
|
+
stats.frameworkFiles.sveltekit.endpoints.forEach(file => entryPoints.add(file));
|
|
2184
|
+
stats.frameworkFiles.sveltekit.components.forEach(file => entryPoints.add(file));
|
|
2185
|
+
}
|
|
2186
|
+
if (stats.detectedFrameworks.includes('react')) {
|
|
2187
|
+
stats.frameworkFiles.react.components.forEach(file => entryPoints.add(file));
|
|
2188
|
+
stats.frameworkFiles.react.hooks.forEach(file => entryPoints.add(file));
|
|
2189
|
+
}
|
|
2190
|
+
if (stats.detectedFrameworks.includes('vue')) {
|
|
2191
|
+
stats.frameworkFiles.vue.components.forEach(file => entryPoints.add(file));
|
|
2192
|
+
stats.frameworkFiles.vue.composables.forEach(file => entryPoints.add(file));
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// Use the DependencyGraph to find all reachable files from the entry points
|
|
2196
|
+
const reachableFiles = dependencyGraph.getReachableFiles(Array.from(entryPoints));
|
|
2197
|
+
|
|
2198
|
+
// A file is considered unused if it was scanned but not reachable from any entry point
|
|
2199
|
+
stats.unusedFiles = new Set(Array.from(allScannedFiles).filter(file => !reachableFiles.has(file)));
|
|
2200
|
+
|
|
2201
|
+
// Further refinement: check for files referenced in common configuration files
|
|
2202
|
+
// This is a more advanced step and would require parsing specific config file formats.
|
|
2203
|
+
// Example: Tailwind CSS `tailwind.config.js` `content` array.
|
|
2204
|
+
// For now, this is a conceptual placeholder.
|
|
2205
|
+
if (stats.detectedFrameworks.includes('tailwind')) {
|
|
2206
|
+
// Look for tailwind.config.js
|
|
2207
|
+
const tailwindConfigPath = path.join(stats.targetDir, 'tailwind.config.js');
|
|
2208
|
+
if (fs.existsSync(tailwindConfigPath)) {
|
|
2209
|
+
try {
|
|
2210
|
+
// This would require a more robust JS file parser to extract the 'content' array
|
|
2211
|
+
// For demonstration, we'll assume a simple regex or AST analysis could find patterns like:
|
|
2212
|
+
// content: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html']
|
|
2213
|
+
const tailwindContent = fs.readFileSync(tailwindConfigPath, 'utf8');
|
|
2214
|
+
const contentArrayMatch = tailwindContent.match(/content:\s*\[([^\]]+)\]/s);
|
|
2215
|
+
if (contentArrayMatch && contentArrayMatch[1]) {
|
|
2216
|
+
const globPatterns = contentArrayMatch[1].split(',').map(s => s.trim().replace(/["']/g, ''));
|
|
2217
|
+
for (const pattern of globPatterns) {
|
|
2218
|
+
// Resolve glob patterns to actual files and mark them as used
|
|
2219
|
+
// This would require a glob library (e.g., 'glob' npm package)
|
|
2220
|
+
// For now, we'll just log the intent.
|
|
2221
|
+
// console.log(` 💡 Tailwind config references files via glob: ${pattern}`);
|
|
2222
|
+
// A real implementation would iterate through glob results and remove from unusedFiles
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
} catch (e) {
|
|
2226
|
+
console.error(` ❌ Error parsing tailwind.config.js: ${e.message}`);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// ============================================================
|
|
2234
|
+
// 🧩 ADVANCED TEMPLATE MANAGEMENT SYSTEM (Hygen-level)
|
|
2235
|
+
// ============================================================
|
|
2236
|
+
class TemplateManager {
|
|
2237
|
+
constructor(baseDir, safeQuestion) {
|
|
2238
|
+
this.baseDir = baseDir;
|
|
2239
|
+
this.safeQuestion = safeQuestion;
|
|
2240
|
+
this.templateSources = [
|
|
2241
|
+
{ name: 'local', path: path.join(this.baseDir, '.templates') },
|
|
2242
|
+
// Future: Add remote Git repositories, e.g., { name: 'remote-official', url: 'https://github.com/my-org/templates.git' }
|
|
2243
|
+
];
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
async listAvailableTemplates() {
|
|
2247
|
+
const allTemplates = new Set();
|
|
2248
|
+
for (const source of this.templateSources) {
|
|
2249
|
+
if (source.name === 'local') {
|
|
2250
|
+
const localTemplatesPath = source.path;
|
|
2251
|
+
if (fs.existsSync(localTemplatesPath)) {
|
|
2252
|
+
const templates = fs.readdirSync(localTemplatesPath, { withFileTypes: true })
|
|
2253
|
+
.filter(dirent => dirent.isDirectory())
|
|
2254
|
+
.map(dirent => dirent.name);
|
|
2255
|
+
templates.forEach(t => allTemplates.add(t));
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
// Future: Handle remote template sources
|
|
2259
|
+
}
|
|
2260
|
+
return Array.from(allTemplates);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
async getTemplatePath(templateName) {
|
|
2264
|
+
for (const source of this.templateSources) {
|
|
2265
|
+
if (source.name === 'local') {
|
|
2266
|
+
const templatePath = path.join(source.path, templateName);
|
|
2267
|
+
if (fs.existsSync(templatePath)) {
|
|
2268
|
+
return templatePath;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
return null;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
async promptForVariables(templateName) {
|
|
2276
|
+
const templatePath = await this.getTemplatePath(templateName);
|
|
2277
|
+
if (!templatePath) {
|
|
2278
|
+
console.log(` ⚠️ Template '${templateName}' not found.`);
|
|
2279
|
+
return {};
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
const configPath = path.join(templatePath, '_config.json');
|
|
2283
|
+
if (fs.existsSync(configPath)) {
|
|
2284
|
+
try {
|
|
2285
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
2286
|
+
const variables = {};
|
|
2287
|
+
for (const key in config.prompts) {
|
|
2288
|
+
const prompt = config.prompts[key];
|
|
2289
|
+
let answer = await this.safeQuestion(`❓ ${prompt.message || key}: `);
|
|
2290
|
+
if (prompt.type === 'boolean') {
|
|
2291
|
+
answer = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
2292
|
+
} else if (prompt.type === 'number') {
|
|
2293
|
+
answer = parseFloat(answer);
|
|
2294
|
+
}
|
|
2295
|
+
variables[key] = answer;
|
|
2296
|
+
}
|
|
2297
|
+
return variables;
|
|
2298
|
+
} catch (e) {
|
|
2299
|
+
console.error(` ❌ Error reading template config for '${templateName}': ${e.message}`);
|
|
2300
|
+
return {};
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
return {};
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
async generate(templateName, variables) {
|
|
2307
|
+
const templatePath = await this.getTemplatePath(templateName);
|
|
2308
|
+
if (!templatePath) return;
|
|
2309
|
+
|
|
2310
|
+
console.log(` 🚀 Generating '${templateName}' template...`);
|
|
2311
|
+
|
|
2312
|
+
const renderFile = async (srcPath, destPath, vars) => {
|
|
2313
|
+
const content = fs.readFileSync(srcPath, 'utf8');
|
|
2314
|
+
// Simple templating: replace {{varName}} with variable value
|
|
2315
|
+
let renderedContent = content;
|
|
2316
|
+
for (const key in vars) {
|
|
2317
|
+
renderedContent = renderedContent.replace(new RegExp(`{{\s*${key}\s*}}`, 'g'), vars[key]);
|
|
2318
|
+
}
|
|
2319
|
+
fs.writeFileSync(destPath, renderedContent);
|
|
2320
|
+
};
|
|
2321
|
+
|
|
2322
|
+
const processDirectory = async (currentSrcDir, currentDestDir, vars) => {
|
|
2323
|
+
fs.mkdirSync(currentDestDir, { recursive: true });
|
|
2324
|
+
const items = fs.readdirSync(currentSrcDir, { withFileTypes: true });
|
|
2325
|
+
|
|
2326
|
+
for (const item of items) {
|
|
2327
|
+
const srcItemPath = path.join(currentSrcDir, item.name);
|
|
2328
|
+
const destItemPath = path.join(currentDestDir, item.name);
|
|
2329
|
+
|
|
2330
|
+
if (item.isDirectory()) {
|
|
2331
|
+
if (item.name !== '_config.json') { // Skip config file
|
|
2332
|
+
await processDirectory(srcItemPath, destItemPath, vars);
|
|
2333
|
+
}
|
|
2334
|
+
} else {
|
|
2335
|
+
await renderFile(srcItemPath, destItemPath, vars);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
};
|
|
2339
|
+
|
|
2340
|
+
await processDirectory(templatePath, this.baseDir, variables);
|
|
2341
|
+
console.log(` ✅ Template '${templateName}' generated successfully.`);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// ============================================================
|
|
2346
|
+
// 🌳 ADVANCED DEPENDENCY GRAPH ENGINE (Knip-level)
|
|
2347
|
+
// ============================================================
|
|
2348
|
+
class DependencyGraph {
|
|
2349
|
+
constructor(stats) {
|
|
2350
|
+
this.stats = stats;
|
|
2351
|
+
this.graph = new Map(); // Map<filePath, { imports: Set<filePath>, exports: Set<symbolName> }>
|
|
2352
|
+
this.symbolToFilePath = new Map(); // Map<symbolName, filePath> for global exports
|
|
2353
|
+
this.buildGraph();
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
buildGraph() {
|
|
2357
|
+
// Initialize graph nodes for all scanned files
|
|
2358
|
+
for (const filePath of this.stats.scannedFiles) {
|
|
2359
|
+
this.graph.set(filePath, { imports: new Set(), exports: new Set() });
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
// Populate exports
|
|
2363
|
+
for (const [filePath, exportedSymbolsMap] of this.stats.exportedSymbols.entries()) {
|
|
2364
|
+
const node = this.graph.get(filePath);
|
|
2365
|
+
if (node) {
|
|
2366
|
+
for (const [symbolName, symbolInfo] of exportedSymbolsMap.entries()) {
|
|
2367
|
+
node.exports.add(symbolName);
|
|
2368
|
+
// For simplicity, assuming unique global symbol names for now, or handling conflicts
|
|
2369
|
+
// A more robust solution would handle namespaces or re-exports more carefully
|
|
2370
|
+
this.symbolToFilePath.set(symbolName, filePath);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// Populate imports
|
|
2376
|
+
for (const [importerFilePath, importedSymbols] of this.stats.localFileImports.entries()) {
|
|
2377
|
+
const importerNode = this.graph.get(importerFilePath);
|
|
2378
|
+
if (importerNode) {
|
|
2379
|
+
for (const importedSymbol of importedSymbols) {
|
|
2380
|
+
// If it's a direct path import, add to imports
|
|
2381
|
+
if (importedSymbol.startsWith(".") || importedSymbol.startsWith("/")) {
|
|
2382
|
+
const resolvedPath = path.normalize(path.resolve(path.dirname(importerFilePath), importedSymbol));
|
|
2383
|
+
if (this.graph.has(resolvedPath)) {
|
|
2384
|
+
importerNode.imports.add(resolvedPath);
|
|
2385
|
+
}
|
|
2386
|
+
} else {
|
|
2387
|
+
// If it's a named import, find the file that exports it
|
|
2388
|
+
const exporterFilePath = this.symbolToFilePath.get(importedSymbol);
|
|
2389
|
+
if (exporterFilePath && this.graph.has(exporterFilePath)) {
|
|
2390
|
+
importerNode.imports.add(exporterFilePath);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
getDependents(filePath) {
|
|
2399
|
+
const dependents = new Set();
|
|
2400
|
+
for (const [importer, node] of this.graph.entries()) {
|
|
2401
|
+
if (node.imports.has(filePath)) {
|
|
2402
|
+
dependents.add(importer);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
return dependents;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
getDependencies(filePath) {
|
|
2409
|
+
const node = this.graph.get(filePath);
|
|
2410
|
+
return node ? node.imports : new Set();
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// Perform a reachability analysis to find all files reachable from entry points
|
|
2414
|
+
getReachableFiles(entryPoints) {
|
|
2415
|
+
const reachable = new Set();
|
|
2416
|
+
const queue = [...entryPoints];
|
|
2417
|
+
|
|
2418
|
+
while (queue.length > 0) {
|
|
2419
|
+
const currentFile = queue.shift();
|
|
2420
|
+
if (reachable.has(currentFile)) continue;
|
|
2421
|
+
|
|
2422
|
+
reachable.add(currentFile);
|
|
2423
|
+
const node = this.graph.get(currentFile);
|
|
2424
|
+
if (node) {
|
|
2425
|
+
for (const importedFile of node.imports) {
|
|
2426
|
+
if (!reachable.has(importedFile)) {
|
|
2427
|
+
queue.push(importedFile);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
return reachable;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// Generate a DOT graph string for visualization
|
|
2436
|
+
toDotGraph() {
|
|
2437
|
+
let dot = `digraph G {\n`;
|
|
2438
|
+
dot += ` rankdir=LR;\n`;
|
|
2439
|
+
dot += ` node [shape=box];\n`;
|
|
2440
|
+
|
|
2441
|
+
for (const [filePath, node] of this.graph.entries()) {
|
|
2442
|
+
const fileName = path.basename(filePath);
|
|
2443
|
+
dot += ` "${filePath}" [label="${fileName}"];\n`;
|
|
2444
|
+
|
|
2445
|
+
for (const importedFile of node.imports) {
|
|
2446
|
+
dot += ` "${filePath}" -> "${importedFile}";\n`;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
dot += `}\n`;
|
|
2450
|
+
return dot;
|
|
2451
|
+
}
|
|
2452
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pkg-scaffold",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Zero-config workspace initializer with advanced dependency intelligence: detects ghost dependencies (used but undeclared), orphaned packages (declared but unused), unused imports with file locations, deprecated packages, hardcoded secrets, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"pkg-scaffold": "./index.js"
|
|
9
9
|
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"postpublish": "git add . && git commit -m \"chore: auto-sync version bump and workspace configuration layers\" && git pull origin main --rebase && git push origin main"
|
|
12
|
-
},
|
|
13
10
|
"keywords": [
|
|
14
11
|
"scaffold",
|
|
15
12
|
"init",
|
|
@@ -29,6 +26,14 @@
|
|
|
29
26
|
],
|
|
30
27
|
"author": "DreamLongYT",
|
|
31
28
|
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/DreamLongYT/pkg-scaffold.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/DreamLongYT/pkg-scaffold/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/DreamLongYT/pkg-scaffold#readme",
|
|
32
37
|
"dependencies": {
|
|
33
38
|
"acorn": "^8.17.0",
|
|
34
39
|
"acorn-walk": "^8.3.5",
|