pkg-scaffold 2.0.1 → 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.
Files changed (2) hide show
  1. package/index.js +914 -27
  2. package/package.json +1 -1
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 pkg = cleanPackageName(node.source.value);
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 pkg = cleanPackageName(typeImportMatch[1]);
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 pkg = cleanPackageName(esmDefaultMatch[2]);
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 pkg = cleanPackageName(esmNamedMatch[2]);
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 pkg = cleanPackageName(sideEffectMatch[1]);
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 pkg = cleanPackageName(cjsMatch[2]);
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 pkg = cleanPackageName(cjsDestructMatch[2]);
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 pkg = cleanPackageName(dynamicMatch[1]);
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.secretKeys.lastIndex = 0;
701
- let secretMatch;
702
- while ((secretMatch = REGEX_PATTERNS.secretKeys.exec(content)) !== null) {
703
- const keyName = secretMatch[1];
704
- const secretValue = secretMatch[2];
705
- const envVarName = `${rootNamespace.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_${keyName.toUpperCase()}`;
706
- stats.discoveredSecrets.push({ filePath: fullPath, keyName, secretValue, envVarName });
707
- stats.envVars.add(envVarName);
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
- extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLocations);
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 safeQuestion(prompt); } catch { return ''; }
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: { varCount: 0, hasEval: false, syncFsCount: 0 },
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,6 +1,6 @@
1
1
  {
2
2
  "name": "pkg-scaffold",
3
- "version": "2.0.1",
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",