pkg-scaffold 2.1.0 → 2.2.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/index.js +1234 -1142
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* 📦 pkg-scaffold v2.2.0: Enterprise Dependency Intelligence & Scaffolding Engine
|
|
6
|
+
* ============================================================================
|
|
7
|
+
* * Eine hochgradig integrierte Code-Analyse- und Projektbootstrapping-Engine.
|
|
8
|
+
* Kombiniert rekursive Erreichbarkeitsanalysen (Reachability Graphs) auf
|
|
9
|
+
* Knip-Niveau mit proaktiven Code-Qualitäts-Audits, Sicherheitsprüfungen,
|
|
10
|
+
* statischem Schwachstellen-Scanning und interaktivem Scaffolding.
|
|
11
|
+
*
|
|
12
|
+
* Diese Datei ist als vollständig entfalteter, langformbasierter Monolith
|
|
13
|
+
* strukturiert, um maximale Wartbarkeit, lückenlose Fehlerabdeckung und
|
|
14
|
+
* absolute Ausführungssicherheit im Produktivbetrieb zu garantieren.
|
|
15
|
+
* * Eigenschaften:
|
|
16
|
+
* - AST-Verifikation via Acorn (ECMAScript Latest)
|
|
17
|
+
* - Textbasierte Fallback-Regex-Parsing-Infrastruktur
|
|
18
|
+
* - Token- und Symbol-Erreichbarkeitsmatrix (Dead-Code-Eliminierung)
|
|
19
|
+
* - Hardcoded Credentials Extraction & .env-Isolation
|
|
20
|
+
*/
|
|
21
|
+
|
|
3
22
|
import fs from 'fs';
|
|
4
23
|
import path from 'path';
|
|
5
24
|
import { builtinModules, createRequire } from 'module';
|
|
@@ -10,86 +29,196 @@ import readline from 'readline/promises';
|
|
|
10
29
|
import * as acorn from 'acorn';
|
|
11
30
|
import * as walk from 'acorn-walk';
|
|
12
31
|
|
|
13
|
-
|
|
14
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Globale Konfigurations-Sets für Verzeichnis- und Dateifilterung.
|
|
34
|
+
* Verhindert das Eindringen von System-, Cache- und Build-Dateien in die Analyse.
|
|
35
|
+
* @type {Set<string>}
|
|
36
|
+
*/
|
|
37
|
+
const IGNORED_DIRS = new Set([
|
|
38
|
+
'node_modules',
|
|
39
|
+
'.git',
|
|
40
|
+
'dist',
|
|
41
|
+
'build',
|
|
42
|
+
'.turbo',
|
|
43
|
+
'coverage',
|
|
44
|
+
'out',
|
|
45
|
+
'.next',
|
|
46
|
+
'.nuxt',
|
|
47
|
+
'.svelte-kit',
|
|
48
|
+
'storybook-static',
|
|
49
|
+
'.cache'
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Unterstützte Dateiendungen für die statische Codeanalyse und das Parsing.
|
|
54
|
+
* @type {Set<string>}
|
|
55
|
+
*/
|
|
56
|
+
const VALID_EXTENSIONS = new Set([
|
|
57
|
+
'.js',
|
|
58
|
+
'.jsx',
|
|
59
|
+
'.ts',
|
|
60
|
+
'.tsx',
|
|
61
|
+
'.mjs',
|
|
62
|
+
'.cjs',
|
|
63
|
+
'.vue',
|
|
64
|
+
'.svelte'
|
|
65
|
+
]);
|
|
15
66
|
|
|
16
|
-
|
|
67
|
+
/**
|
|
68
|
+
* --- Refined Target Signature Dictionaries ---
|
|
69
|
+
* Umfassendes Verzeichnis regulärer Ausdrücke für Code-Smells, kryptografische Risiken,
|
|
70
|
+
* Framework-Routing und Umgebungsvariablen.
|
|
71
|
+
* @type {Object}
|
|
72
|
+
*/
|
|
17
73
|
const REGEX_PATTERNS = {
|
|
74
|
+
/**
|
|
75
|
+
* Zugriff auf Umgebungsvariablen im Node- oder Vite-Kontext.
|
|
76
|
+
*/
|
|
18
77
|
env: /(?:process\.env|import\.meta\.env)\.([A-Z_][A-Z0-9_]*)/g,
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Erkennung von Testdateien für diverse Testrunner.
|
|
81
|
+
*/
|
|
19
82
|
testFile: /\.(test|spec)\.(js|ts|jsx|tsx|mjs|cjs)$/i,
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Erkennung gängiger Konfigurationsdateien im JavaScript-Ökosystem.
|
|
86
|
+
*/
|
|
20
87
|
configFile: /^(vite|webpack|rollup|babel|jest|vitest|tailwind|postcss|next|nuxt|svelte|astro)\.config\./i,
|
|
21
88
|
|
|
22
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Veraltete Variablendeklarationen (Code-Smell).
|
|
91
|
+
*/
|
|
23
92
|
legacyVar: /\bvar\s+[a-zA-Z_]/g,
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Gefährliche dynamische Code-Ausführung.
|
|
96
|
+
*/
|
|
24
97
|
dangerousEval: /\beval\s*\(/g,
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Blockierende synchrone Dateisystemaufrufe.
|
|
101
|
+
*/
|
|
25
102
|
syncFsCalls: /\.readFileSync|\.writeFileSync|\.mkdirSync|\.existsSync/g,
|
|
26
103
|
|
|
27
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Allgemeine Zuweisung harter Anmeldedaten.
|
|
106
|
+
*/
|
|
28
107
|
secretKeys: /\b(secret|passwd|password|token|api_?key|private_?key)\s*=\s*['"`]([a-zA-Z0-9_\-\.]{8,})['"`]/gi,
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Spezifische AWS Zugriffsschlüssel.
|
|
111
|
+
*/
|
|
29
112
|
awsKeys: /AKIA[0-9A-Z]{16}/g,
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Spezifische Google Cloud API Schlüssel.
|
|
116
|
+
*/
|
|
30
117
|
googleCloudKeys: /AIza[0-9A-Za-z\-_]{35}/g,
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Live-Schlüssel für den Stripe-Zahlungsdienstleister.
|
|
121
|
+
*/
|
|
31
122
|
stripeKeys: /sk_live_[0-9a-zA-Z]{24}/g,
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Slack Bot- und Benutzer-Tokens.
|
|
126
|
+
*/
|
|
32
127
|
slackKeys: /xox[baprs]-[0-9a-zA-Z]{10,48}/g,
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* GitHub Personal Access Tokens.
|
|
131
|
+
*/
|
|
33
132
|
githubTokens: /gh[pousr]_[a-zA-Z0-9]{36}/g,
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Private RSA-Schlüsselblöcke.
|
|
136
|
+
*/
|
|
34
137
|
rsaPrivateKeys: /-----BEGIN RSA PRIVATE KEY-----/g,
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Private OpenSSH-Schlüsselblöcke.
|
|
141
|
+
*/
|
|
35
142
|
sshPrivateKeys: /-----BEGIN OPENSSH PRIVATE KEY-----/g,
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Private PGP-Schlüsselblöcke.
|
|
146
|
+
*/
|
|
36
147
|
pgpPrivateKeys: /-----BEGIN PGP PRIVATE KEY BLOCK-----/g,
|
|
37
148
|
|
|
38
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Unsicheres Einfügen von unbereinigtem HTML (XSS-Risiko).
|
|
151
|
+
*/
|
|
39
152
|
insecureInnerHTML: /\.innerHTML\s*=/g,
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Direktes Schreiben in das Dokumentenobjekt im Browser.
|
|
156
|
+
*/
|
|
40
157
|
insecureDocumentWrite: /document\.write\s*\(/g,
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* React-spezifisches Äquivalent zu innerHTML.
|
|
161
|
+
*/
|
|
41
162
|
insecureDangerouslySet: /dangerouslySetInnerHTML/g,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
largeImageImport: /import\s+.*\s+from\s+[""](?:.*\.(?:png|jpg|jpeg|gif|svg))[""]/g,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Riskante reguläre Ausdrücke mit Potenzial für Catastrophic Backtracking.
|
|
166
|
+
*/
|
|
167
|
+
insecureRegex: /\/\.\*\//g,
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Veraltete oder unsichere Krypto-Verfahren in Node.js.
|
|
171
|
+
*/
|
|
172
|
+
insecureCrypto: /crypto\.(?:createCipher|createDecipher|pbkdf2Sync)/g,
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Basis-Muster zur Erkennung potenzieller SQL-Injections in String-Literalen.
|
|
176
|
+
*/
|
|
177
|
+
sqlInjection: /(?:SELECT|INSERT|UPDATE|DELETE)\s+.*\s+FROM\s+.*\s+WHERE\s+.*\s*=\s*[""]?.*[""]?/i,
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Erkennung von harten Script-Tags im Quellcode (XSS-Indikator).
|
|
181
|
+
*/
|
|
182
|
+
xssVulnerability: /<script\b[^>]*>[\s\S]*?<\/script>/i,
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Performance-Smell: Direkter Import von großen Bilddateien im Quellcode.
|
|
186
|
+
*/
|
|
187
|
+
largeImageImport: /import\s+.*\s+from\s+[""](?:.*\.(?:png|jpg|jpeg|gif|svg))[""]/g,
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Performance-Smell: Unoptimierte for-Schleife über Array-Längen.
|
|
191
|
+
*/
|
|
192
|
+
unoptimizedLoop: /for\s*\(let\s+i\s*=\s*0;\s*i\s*<\s*\w+\.length;\s*i\s*\+\+\)/g,
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Framework-Muster: Next.js Bildkomponente.
|
|
196
|
+
*/
|
|
197
|
+
nextjsImageComponent: /<Image\s+[^>]*>/g,
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Framework-Muster: Next.js Schriftoptimierung.
|
|
201
|
+
*/
|
|
202
|
+
nextjsFontOptimization: /next\/font/g,
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Framework-Muster: Nuxt 3 Auto-Imports für Datenabrufe.
|
|
206
|
+
*/
|
|
207
|
+
nuxtAutoImport: /use(?:State|Fetch|AsyncData)/g,
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Framework-Muster: SvelteKit Datenladefunktion.
|
|
211
|
+
*/
|
|
212
|
+
sveltekitLoadFunction: /export\s+const\s+load\s*=/g,
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Erkennt unvollständige React useEffect Hooks ohne jegliches Abhängigkeitsarray.
|
|
216
|
+
*/
|
|
217
|
+
reactUseEffectNoDeps: /useEffect\s*\(\s*(?:\([^)]*\)|[^=]+)\s*=>\s*\{[\s\S]*?\}\s*\)/g,
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Framework-Dateipfade für tiefe Routing-Analysen.
|
|
221
|
+
*/
|
|
93
222
|
nextjsPage: /pages\/[^\/]+\.(js|jsx|ts|tsx)$/i,
|
|
94
223
|
nextjsApi: /pages\/api\/[^\/]+\.(js|jsx|ts|tsx)$/i,
|
|
95
224
|
nextjsComponent: /components\/[^\/]+\.(js|jsx|ts|tsx)$/i,
|
|
@@ -101,20 +230,17 @@ const REGEX_PATTERNS = {
|
|
|
101
230
|
vueComposable: /composables\/[^\/]+\.(js|ts)$/i
|
|
102
231
|
};
|
|
103
232
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
233
|
+
/**
|
|
234
|
+
* Maps CLI-Binärnamen auf tatsächliche npm-Paketnamen (Knip-Stil).
|
|
235
|
+
* @type {Object}
|
|
236
|
+
*/
|
|
108
237
|
const BINARY_TO_PACKAGE_MAP = {
|
|
109
|
-
// TypeScript / JavaScript compilers & runtimes
|
|
110
238
|
'tsc': 'typescript',
|
|
111
239
|
'ts-node': 'ts-node',
|
|
112
240
|
'tsx': 'tsx',
|
|
113
241
|
'tsup': 'tsup',
|
|
114
242
|
'esbuild': 'esbuild',
|
|
115
243
|
'swc': '@swc/cli',
|
|
116
|
-
|
|
117
|
-
// Test runners
|
|
118
244
|
'jest': 'jest',
|
|
119
245
|
'vitest': 'vitest',
|
|
120
246
|
'mocha': 'mocha',
|
|
@@ -123,8 +249,6 @@ const BINARY_TO_PACKAGE_MAP = {
|
|
|
123
249
|
'tap': 'tap',
|
|
124
250
|
'c8': 'c8',
|
|
125
251
|
'nyc': 'nyc',
|
|
126
|
-
|
|
127
|
-
// Linters & formatters
|
|
128
252
|
'eslint': 'eslint',
|
|
129
253
|
'prettier': 'prettier',
|
|
130
254
|
'biome': '@biomejs/biome',
|
|
@@ -132,24 +256,18 @@ const BINARY_TO_PACKAGE_MAP = {
|
|
|
132
256
|
'tslint': 'tslint',
|
|
133
257
|
'xo': 'xo',
|
|
134
258
|
'standard': 'standard',
|
|
135
|
-
|
|
136
|
-
// Bundlers & dev servers
|
|
137
259
|
'vite': 'vite',
|
|
138
260
|
'webpack': 'webpack',
|
|
139
261
|
'rollup': 'rollup',
|
|
140
262
|
'parcel': 'parcel',
|
|
141
263
|
'turbo': 'turbo',
|
|
142
264
|
'nx': 'nx',
|
|
143
|
-
|
|
144
|
-
// Process managers & watchers
|
|
145
265
|
'nodemon': 'nodemon',
|
|
146
266
|
'pm2': 'pm2',
|
|
147
267
|
'concurrently': 'concurrently',
|
|
148
268
|
'cross-env': 'cross-env',
|
|
149
269
|
'dotenv-cli': 'dotenv-cli',
|
|
150
270
|
'env-cmd': 'env-cmd',
|
|
151
|
-
|
|
152
|
-
// Code generation & scaffolding
|
|
153
271
|
'hygen': 'hygen',
|
|
154
272
|
'plop': 'plop',
|
|
155
273
|
'prisma': 'prisma',
|
|
@@ -158,8 +276,6 @@ const BINARY_TO_PACKAGE_MAP = {
|
|
|
158
276
|
'sequelize': 'sequelize-cli',
|
|
159
277
|
'knex': 'knex',
|
|
160
278
|
'mikro-orm': '@mikro-orm/cli',
|
|
161
|
-
|
|
162
|
-
// Build & deployment tools
|
|
163
279
|
'rimraf': 'rimraf',
|
|
164
280
|
'copyfiles': 'copyfiles',
|
|
165
281
|
'mkdirp': 'mkdirp',
|
|
@@ -169,14 +285,10 @@ const BINARY_TO_PACKAGE_MAP = {
|
|
|
169
285
|
'npm-run-all': 'npm-run-all',
|
|
170
286
|
'run-s': 'npm-run-all',
|
|
171
287
|
'run-p': 'npm-run-all',
|
|
172
|
-
|
|
173
|
-
// Documentation
|
|
174
288
|
'typedoc': 'typedoc',
|
|
175
289
|
'jsdoc': 'jsdoc',
|
|
176
290
|
'storybook': 'storybook',
|
|
177
291
|
'sb': 'storybook',
|
|
178
|
-
|
|
179
|
-
// Misc
|
|
180
292
|
'husky': 'husky',
|
|
181
293
|
'lint-staged': 'lint-staged',
|
|
182
294
|
'commitlint': '@commitlint/cli',
|
|
@@ -185,40 +297,82 @@ const BINARY_TO_PACKAGE_MAP = {
|
|
|
185
297
|
'changeset': '@changesets/cli',
|
|
186
298
|
'changesets': '@changesets/cli',
|
|
187
299
|
'np': 'np',
|
|
188
|
-
'bumpp': 'bumpp'
|
|
300
|
+
'bumpp': 'bumpp'
|
|
189
301
|
};
|
|
190
302
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
303
|
+
/**
|
|
304
|
+
* Menge von Entwicklungswerkzeugen, die nicht als verwaist eingestuft werden sollen.
|
|
305
|
+
* @type {Set<string>}
|
|
306
|
+
*/
|
|
194
307
|
const DEV_TOOLING_ECOSYSTEM = new Set([
|
|
195
|
-
|
|
196
|
-
'
|
|
197
|
-
|
|
198
|
-
'
|
|
199
|
-
|
|
200
|
-
'
|
|
201
|
-
|
|
202
|
-
'
|
|
203
|
-
|
|
204
|
-
'
|
|
205
|
-
|
|
206
|
-
'
|
|
207
|
-
|
|
208
|
-
'
|
|
209
|
-
|
|
210
|
-
'
|
|
211
|
-
|
|
212
|
-
'
|
|
213
|
-
|
|
214
|
-
'
|
|
308
|
+
'eslint',
|
|
309
|
+
'prettier',
|
|
310
|
+
'biome',
|
|
311
|
+
'@biomejs/biome',
|
|
312
|
+
'oxlint',
|
|
313
|
+
'tslint',
|
|
314
|
+
'xo',
|
|
315
|
+
'standard',
|
|
316
|
+
'typescript',
|
|
317
|
+
'typescript-eslint',
|
|
318
|
+
'@eslint/js',
|
|
319
|
+
'ts-node',
|
|
320
|
+
'tsx',
|
|
321
|
+
'tsup',
|
|
322
|
+
'esbuild',
|
|
323
|
+
'@swc/cli',
|
|
324
|
+
'jest',
|
|
325
|
+
'vitest',
|
|
326
|
+
'mocha',
|
|
327
|
+
'jasmine',
|
|
328
|
+
'ava',
|
|
329
|
+
'tap',
|
|
330
|
+
'c8',
|
|
331
|
+
'nyc',
|
|
332
|
+
'vite',
|
|
333
|
+
'webpack',
|
|
334
|
+
'rollup',
|
|
335
|
+
'parcel',
|
|
336
|
+
'turbo',
|
|
337
|
+
'nx',
|
|
338
|
+
'nodemon',
|
|
339
|
+
'pm2',
|
|
340
|
+
'concurrently',
|
|
341
|
+
'cross-env',
|
|
342
|
+
'dotenv-cli',
|
|
343
|
+
'env-cmd',
|
|
344
|
+
'rimraf',
|
|
345
|
+
'copyfiles',
|
|
346
|
+
'mkdirp',
|
|
347
|
+
'shx',
|
|
348
|
+
'ncp',
|
|
349
|
+
'cpx',
|
|
350
|
+
'npm-run-all',
|
|
351
|
+
'typedoc',
|
|
352
|
+
'jsdoc',
|
|
353
|
+
'storybook',
|
|
354
|
+
'husky',
|
|
355
|
+
'lint-staged',
|
|
356
|
+
'@commitlint/cli',
|
|
357
|
+
'release-it',
|
|
358
|
+
'semantic-release',
|
|
359
|
+
'@changesets/cli',
|
|
360
|
+
'np',
|
|
361
|
+
'bumpp',
|
|
362
|
+
'prisma',
|
|
363
|
+
'drizzle-kit',
|
|
364
|
+
'typeorm',
|
|
365
|
+
'sequelize-cli',
|
|
366
|
+
'knex',
|
|
367
|
+
'@mikro-orm/cli',
|
|
368
|
+
'hygen',
|
|
369
|
+
'plop'
|
|
215
370
|
]);
|
|
216
371
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
// ============================================================
|
|
372
|
+
/**
|
|
373
|
+
* Bekannte Namensraum-Importbezeichner gängiger Bibliotheken.
|
|
374
|
+
* @type {Object}
|
|
375
|
+
*/
|
|
222
376
|
const PACKAGE_IMPORT_ALIASES = {
|
|
223
377
|
'lodash': ['_', 'lodash'],
|
|
224
378
|
'lodash-es': ['_', 'lodash'],
|
|
@@ -285,71 +439,154 @@ const PACKAGE_IMPORT_ALIASES = {
|
|
|
285
439
|
'svelte': ['svelte'],
|
|
286
440
|
'@angular/core': ['Component', 'NgModule'],
|
|
287
441
|
'next': ['next'],
|
|
288
|
-
'nuxt': ['nuxt']
|
|
442
|
+
'nuxt': ['nuxt']
|
|
289
443
|
};
|
|
290
444
|
|
|
445
|
+
// ============================================================
|
|
446
|
+
// BASE HOISTED HELPER INFRASTRUCTURE
|
|
447
|
+
// ============================================================
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Extrahiert die Git-Konfigurationsparameter des lokalen Benutzers.
|
|
451
|
+
* @returns {Object} Git-Identity Datenstruktur.
|
|
452
|
+
*/
|
|
291
453
|
function getGitIdentity() {
|
|
292
|
-
const identity = {
|
|
454
|
+
const identity = {
|
|
455
|
+
name: "Developer",
|
|
456
|
+
author: "Developer",
|
|
457
|
+
repository: ""
|
|
458
|
+
};
|
|
459
|
+
|
|
293
460
|
try {
|
|
294
461
|
const name = execSync('git config user.name', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
295
462
|
const email = execSync('git config user.email', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
463
|
+
|
|
296
464
|
if (name) {
|
|
297
465
|
identity.name = name;
|
|
298
|
-
|
|
466
|
+
if (email) {
|
|
467
|
+
identity.author = `${name} <${email}>`;
|
|
468
|
+
} else {
|
|
469
|
+
identity.author = name;
|
|
470
|
+
}
|
|
299
471
|
}
|
|
472
|
+
|
|
300
473
|
try {
|
|
301
474
|
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
302
475
|
identity.repository = remoteUrl.replace(/\.git$/, '');
|
|
303
|
-
} catch (
|
|
304
|
-
|
|
476
|
+
} catch (gitRemoteError) {
|
|
477
|
+
// Fehlendes Remote wird abgefangen
|
|
478
|
+
}
|
|
479
|
+
} catch (gitConfigError) {
|
|
480
|
+
// Fehlende Git-Umgebung wird abgefangen
|
|
481
|
+
}
|
|
482
|
+
|
|
305
483
|
return identity;
|
|
306
484
|
}
|
|
307
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Prüft das Vorhandensein von Lockfiles im Zielverzeichnis zur Bestimmung des Paketmanagers.
|
|
488
|
+
* @param {string} targetDir Das zu scannende Projektverzeichnis.
|
|
489
|
+
* @param {Object} stats Das globale Analyse-Objekt.
|
|
490
|
+
* @returns {string} Der ermittelte Paketmanager.
|
|
491
|
+
*/
|
|
308
492
|
function detectPackageManager(targetDir, stats = null) {
|
|
309
493
|
const detectedLockfiles = [];
|
|
310
|
-
|
|
311
|
-
if (fs.existsSync(path.join(targetDir, '
|
|
312
|
-
|
|
313
|
-
|
|
494
|
+
|
|
495
|
+
if (fs.existsSync(path.join(targetDir, 'pnpm-lock.yaml'))) {
|
|
496
|
+
detectedLockfiles.push('pnpm-lock.yaml');
|
|
497
|
+
}
|
|
498
|
+
if (fs.existsSync(path.join(targetDir, 'yarn.lock'))) {
|
|
499
|
+
detectedLockfiles.push('yarn.lock');
|
|
500
|
+
}
|
|
501
|
+
if (fs.existsSync(path.join(targetDir, 'package-lock.json'))) {
|
|
502
|
+
detectedLockfiles.push('package-lock.json');
|
|
503
|
+
}
|
|
504
|
+
if (fs.existsSync(path.join(targetDir, 'bun.lockb')) || fs.existsSync(path.join(targetDir, 'bun.lock'))) {
|
|
505
|
+
detectedLockfiles.push('bun.lock');
|
|
506
|
+
}
|
|
314
507
|
|
|
315
|
-
if (detectedLockfiles.length > 1 && stats) {
|
|
508
|
+
if (detectedLockfiles.length > 1 && stats !== null) {
|
|
316
509
|
stats.conflictingLockfiles = detectedLockfiles;
|
|
317
510
|
}
|
|
318
511
|
|
|
319
|
-
if (detectedLockfiles.some(l => l.startsWith('bun')))
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (detectedLockfiles.includes('
|
|
512
|
+
if (detectedLockfiles.some(l => { return l.startsWith('bun'); })) {
|
|
513
|
+
return 'bun';
|
|
514
|
+
}
|
|
515
|
+
if (detectedLockfiles.includes('pnpm-lock.yaml')) {
|
|
516
|
+
return 'pnpm';
|
|
517
|
+
}
|
|
518
|
+
if (detectedLockfiles.includes('yarn.lock')) {
|
|
519
|
+
return 'yarn';
|
|
520
|
+
}
|
|
521
|
+
if (detectedLockfiles.includes('package-lock.json')) {
|
|
522
|
+
return 'npm';
|
|
523
|
+
}
|
|
323
524
|
|
|
324
|
-
try {
|
|
325
|
-
|
|
525
|
+
try {
|
|
526
|
+
execSync('pnpm --version', { stdio: 'ignore' });
|
|
527
|
+
return 'pnpm';
|
|
528
|
+
} catch (pnpmVersionError) {}
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
execSync('yarn --version', { stdio: 'ignore' });
|
|
532
|
+
return 'yarn';
|
|
533
|
+
} catch (yarnVersionError) {}
|
|
534
|
+
|
|
326
535
|
return 'npm';
|
|
327
536
|
}
|
|
328
537
|
|
|
538
|
+
/**
|
|
539
|
+
* Führt Code-Style Metriken-Suchen auf dem rohen Text-Inhalt einer Datei aus.
|
|
540
|
+
* @param {string} content Dateiinhalt.
|
|
541
|
+
* @param {Object} stats Globales Statistikobjekt.
|
|
542
|
+
*/
|
|
329
543
|
function analyzeCodeStyle(content, stats) {
|
|
330
544
|
const lines = content.split('\n');
|
|
545
|
+
|
|
331
546
|
for (const line of lines) {
|
|
332
547
|
const trimmed = line.trim();
|
|
333
|
-
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*'))
|
|
548
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
334
551
|
|
|
335
|
-
if (trimmed.endsWith(';'))
|
|
336
|
-
|
|
552
|
+
if (trimmed.endsWith(';')) {
|
|
553
|
+
stats.style.semiCount++;
|
|
554
|
+
} else if (!/[{}:,\[\]]/.test(trimmed.slice(-1))) {
|
|
555
|
+
stats.style.noSemiCount++;
|
|
556
|
+
}
|
|
337
557
|
|
|
338
|
-
if (line.startsWith('\t'))
|
|
339
|
-
|
|
558
|
+
if (line.startsWith('\t')) {
|
|
559
|
+
stats.style.tabCount++;
|
|
560
|
+
} else if (line.startsWith(' ')) {
|
|
340
561
|
const spaces = line.match(/^(\s+)/)?.[1]?.length || 0;
|
|
341
|
-
if (spaces === 2)
|
|
342
|
-
|
|
562
|
+
if (spaces === 2) {
|
|
563
|
+
stats.style.space2Count++;
|
|
564
|
+
}
|
|
565
|
+
if (spaces === 4) {
|
|
566
|
+
stats.style.space4Count++;
|
|
567
|
+
}
|
|
343
568
|
}
|
|
344
569
|
}
|
|
345
570
|
|
|
346
|
-
if (REGEX_PATTERNS.legacyVar.test(content))
|
|
347
|
-
|
|
348
|
-
|
|
571
|
+
if (REGEX_PATTERNS.legacyVar.test(content)) {
|
|
572
|
+
stats.quality.varCount += (content.match(REGEX_PATTERNS.legacyVar) || []).length;
|
|
573
|
+
}
|
|
574
|
+
if (REGEX_PATTERNS.dangerousEval.test(content)) {
|
|
575
|
+
stats.quality.hasEval = true;
|
|
576
|
+
}
|
|
577
|
+
if (REGEX_PATTERNS.syncFsCalls.test(content)) {
|
|
578
|
+
stats.quality.syncFsCount += (content.match(REGEX_PATTERNS.syncFsCalls) || []).length;
|
|
579
|
+
}
|
|
349
580
|
}
|
|
350
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Durchsucht npm scripts nach ausführbaren Binärbefehlen dritter Anbieter.
|
|
584
|
+
* @param {Object} packageJsonContent Parste package.json Struktur.
|
|
585
|
+
* @returns {Array<string>} Liste gefundener CLI-Befehlsaufrufe.
|
|
586
|
+
*/
|
|
351
587
|
function getBinariesFromPackageJson(packageJsonContent) {
|
|
352
588
|
const binaries = new Set();
|
|
589
|
+
|
|
353
590
|
if (packageJsonContent && packageJsonContent.scripts) {
|
|
354
591
|
for (const script of Object.values(packageJsonContent.scripts)) {
|
|
355
592
|
const commands = String(script).split(/\s*&&\s*|\s*;\s*|\s*\|\|\s*/);
|
|
@@ -361,15 +598,28 @@ function getBinariesFromPackageJson(packageJsonContent) {
|
|
|
361
598
|
}
|
|
362
599
|
}
|
|
363
600
|
}
|
|
601
|
+
|
|
364
602
|
return Array.from(binaries);
|
|
365
603
|
}
|
|
366
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Isoliert den reinen npm Paketnamen unter Ausschluss interner Datei-Imports.
|
|
607
|
+
* @param {string} importString Import-Quell-Identifikator.
|
|
608
|
+
* @returns {string|null} Bereinigter Paketname oder null.
|
|
609
|
+
*/
|
|
367
610
|
function cleanPackageName(importString) {
|
|
368
|
-
if (!importString || /^[./~\\]/.test(importString))
|
|
369
|
-
|
|
611
|
+
if (!importString || /^[./~\\]/.test(importString)) {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
if (importString.startsWith('@')) {
|
|
615
|
+
return importString.split('/').slice(0, 2).join('/');
|
|
616
|
+
}
|
|
370
617
|
return importString.split('/')[0];
|
|
371
618
|
}
|
|
372
619
|
|
|
620
|
+
/**
|
|
621
|
+
* Fügt Header-Komponenten präzise unterhalb von Shebangs oder Systemdirektiven ein.
|
|
622
|
+
*/
|
|
373
623
|
function smartPrepend(originalCode, declarationBlock) {
|
|
374
624
|
const lines = originalCode.split(/\r?\n/);
|
|
375
625
|
let insertIdx = 0;
|
|
@@ -389,6 +639,9 @@ function smartPrepend(originalCode, declarationBlock) {
|
|
|
389
639
|
return lines.join('\n');
|
|
390
640
|
}
|
|
391
641
|
|
|
642
|
+
/**
|
|
643
|
+
* Validiert die Existenz eines Pakets auf der offiziellen NPM-Registry.
|
|
644
|
+
*/
|
|
392
645
|
async function inspectNpmPackage(pkgName) {
|
|
393
646
|
try {
|
|
394
647
|
const response = await fetch(`https://registry.npmjs.org/${pkgName}/latest`, {
|
|
@@ -397,15 +650,32 @@ async function inspectNpmPackage(pkgName) {
|
|
|
397
650
|
});
|
|
398
651
|
if (response.status === 200) {
|
|
399
652
|
const data = await response.json();
|
|
400
|
-
return {
|
|
653
|
+
return {
|
|
654
|
+
version: data.version,
|
|
655
|
+
deprecated: data.deprecated || null,
|
|
656
|
+
error: null
|
|
657
|
+
};
|
|
401
658
|
}
|
|
402
|
-
if (response.status === 404)
|
|
403
|
-
|
|
404
|
-
|
|
659
|
+
if (response.status === 404) {
|
|
660
|
+
return {
|
|
661
|
+
version: null,
|
|
662
|
+
deprecated: null,
|
|
663
|
+
error: 'NOT_FOUND'
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
} catch (networkInspectError) {
|
|
667
|
+
return {
|
|
668
|
+
version: 'latest',
|
|
669
|
+
deprecated: null,
|
|
670
|
+
error: 'NETWORK_FAIL'
|
|
671
|
+
};
|
|
405
672
|
}
|
|
406
673
|
return null;
|
|
407
674
|
}
|
|
408
675
|
|
|
676
|
+
/**
|
|
677
|
+
* Holt Lizenztexte aus der GitHub Legal API.
|
|
678
|
+
*/
|
|
409
679
|
async function fetchRemoteLicense(licenseKey) {
|
|
410
680
|
try {
|
|
411
681
|
const response = await fetch(`https://api.github.com/licenses/${licenseKey.toLowerCase()}`, {
|
|
@@ -416,23 +686,52 @@ async function fetchRemoteLicense(licenseKey) {
|
|
|
416
686
|
const data = await response.json();
|
|
417
687
|
return data.body;
|
|
418
688
|
}
|
|
419
|
-
} catch (
|
|
689
|
+
} catch (licenseApiError) {}
|
|
420
690
|
return null;
|
|
421
691
|
}
|
|
422
692
|
|
|
693
|
+
/**
|
|
694
|
+
* Liest Dateien synchron ein und bereinigt etwaige BOM-Byte-Anordnungen.
|
|
695
|
+
*/
|
|
423
696
|
function readFileSyncNormalized(fullPath) {
|
|
424
697
|
const buffer = fs.readFileSync(fullPath);
|
|
425
|
-
if (buffer[0] === 0xFF && buffer[1] === 0xFE)
|
|
426
|
-
|
|
698
|
+
if (buffer[0] === 0xFF && buffer[1] === 0xFE) {
|
|
699
|
+
return buffer.toString('utf16le');
|
|
700
|
+
}
|
|
701
|
+
if (buffer[0] === 0xFE && buffer[1] === 0xFF) {
|
|
702
|
+
return buffer.toString('utf8');
|
|
703
|
+
}
|
|
427
704
|
return buffer.toString('utf8');
|
|
428
705
|
}
|
|
429
706
|
|
|
707
|
+
/**
|
|
708
|
+
* Rekursiver Pfad-Auflösungsalgorithmus zur Verfolgung relativer Quellcodedateien.
|
|
709
|
+
*/
|
|
710
|
+
function resolveLocalModulePath(basePath, importSource) {
|
|
711
|
+
const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue', '.svelte'];
|
|
712
|
+
let absoluteTarget = path.resolve(path.dirname(basePath), importSource);
|
|
713
|
+
|
|
714
|
+
if (fs.existsSync(absoluteTarget) && fs.statSync(absoluteTarget).isDirectory()) {
|
|
715
|
+
const indexFile = extensions.map(ext => { return path.join(absoluteTarget, `index${ext}`); }).find(fs.existsSync);
|
|
716
|
+
if (indexFile) {
|
|
717
|
+
return indexFile;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const directFile = extensions.map(ext => { return absoluteTarget + ext; }).find(fs.existsSync);
|
|
721
|
+
if (directFile) {
|
|
722
|
+
return directFile;
|
|
723
|
+
}
|
|
724
|
+
if (fs.existsSync(absoluteTarget)) {
|
|
725
|
+
return absoluteTarget;
|
|
726
|
+
}
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
|
|
430
730
|
// ============================================================
|
|
431
731
|
// 🏗️ FRAMEWORK-SPECIFIC DEEP SCAN LOGIC
|
|
432
732
|
// ============================================================
|
|
433
733
|
class FrameworkAnalyzer {
|
|
434
734
|
static analyzeNextjsFile(filePath, content, stats) {
|
|
435
|
-
// Data Fetching Patterns (getServerSideProps, getStaticProps, getStaticPaths, Route Handlers)
|
|
436
735
|
if (filePath.includes("pages/") && content.includes("getServerSideProps")) {
|
|
437
736
|
stats.frameworkFiles.nextjs.dataFetching.set(filePath, "getServerSideProps");
|
|
438
737
|
stats.frameworkOptimizations.push(`Next.js: Consider using 'getStaticProps' or client-side fetching for '${path.relative(process.cwd(), filePath)}' if data is not highly dynamic.`);
|
|
@@ -446,7 +745,6 @@ class FrameworkAnalyzer {
|
|
|
446
745
|
if (filePath.includes("app/") && content.includes("export async function GET")) {
|
|
447
746
|
stats.frameworkFiles.nextjs.dataFetching.set(filePath, "Route Handler (GET)");
|
|
448
747
|
}
|
|
449
|
-
// More Next.js specific checks: Image optimization, Font optimization, Script optimization
|
|
450
748
|
if (content.includes("<img") && !content.includes("<Image")) {
|
|
451
749
|
stats.frameworkOptimizations.push(`Next.js: Use next/image for '${path.relative(process.cwd(), filePath)}' to optimize images.`);
|
|
452
750
|
}
|
|
@@ -456,45 +754,37 @@ class FrameworkAnalyzer {
|
|
|
456
754
|
}
|
|
457
755
|
|
|
458
756
|
static analyzeNuxtFile(filePath, content, stats) {
|
|
459
|
-
// Data Fetching Patterns (useAsyncData, useFetch)
|
|
460
757
|
if (content.includes("useAsyncData")) {
|
|
461
758
|
stats.frameworkFiles.nuxt.dataFetching.set(filePath, "useAsyncData");
|
|
462
759
|
}
|
|
463
760
|
if (content.includes("useFetch")) {
|
|
464
761
|
stats.frameworkFiles.nuxt.dataFetching.set(filePath, "useFetch");
|
|
465
762
|
}
|
|
466
|
-
// Nuxt specific checks: Auto-imports, module usage
|
|
467
763
|
if (filePath.includes("components/") && !content.includes("defineComponent")) {
|
|
468
764
|
stats.frameworkOptimizations.push(`Nuxt: Ensure components in '${path.relative(process.cwd(), filePath)}' are properly defined for auto-import or explicitly imported.`);
|
|
469
765
|
}
|
|
470
766
|
}
|
|
471
767
|
|
|
472
768
|
static analyzeSvelteKitFile(filePath, content, stats) {
|
|
473
|
-
// Data Fetching Patterns (load functions)
|
|
474
769
|
if (content.includes("export async function load")) {
|
|
475
770
|
stats.frameworkFiles.sveltekit.loadFunctions.set(filePath, "load");
|
|
476
771
|
}
|
|
477
|
-
// SvelteKit specific checks: endpoint usage, form actions
|
|
478
772
|
if (filePath.includes("src/routes/") && content.includes("export const actions")) {
|
|
479
773
|
stats.frameworkFiles.sveltekit.endpoints.add(filePath);
|
|
480
774
|
}
|
|
481
775
|
}
|
|
482
776
|
|
|
483
777
|
static analyzeReactFile(filePath, content, stats) {
|
|
484
|
-
|
|
485
|
-
if (
|
|
486
|
-
stats.frameworkOptimizations.push(`React:
|
|
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.`);
|
|
778
|
+
REGEX_PATTERNS.reactUseEffectNoDeps.lastIndex = 0;
|
|
779
|
+
if (REGEX_PATTERNS.reactUseEffectNoDeps.test(content)) {
|
|
780
|
+
stats.frameworkOptimizations.push(`React Warning: useEffect hook inside '${path.relative(process.cwd(), filePath)}' is missing a trailing dependency array, which can cause severe infinite re-render loops.`);
|
|
494
781
|
}
|
|
495
782
|
}
|
|
496
783
|
|
|
497
784
|
static analyzeFile(filePath, content, stats, detectedFrameworks) {
|
|
785
|
+
if (!detectedFrameworks || !Array.isArray(detectedFrameworks)) {
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
498
788
|
if (detectedFrameworks.includes("next")) {
|
|
499
789
|
FrameworkAnalyzer.analyzeNextjsFile(filePath, content, stats);
|
|
500
790
|
}
|
|
@@ -513,131 +803,172 @@ class FrameworkAnalyzer {
|
|
|
513
803
|
}
|
|
514
804
|
}
|
|
515
805
|
|
|
516
|
-
// ============================================================
|
|
517
|
-
// ⚙️ FRAMEWORK DETECTION ENGINE
|
|
518
|
-
// ============================================================
|
|
519
806
|
class FrameworkEngine {
|
|
520
807
|
static detect(targetDir, packageJson) {
|
|
521
808
|
const detected = new Set();
|
|
522
|
-
|
|
523
|
-
// Check package.json dependencies
|
|
524
809
|
const allDependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
525
|
-
|
|
526
|
-
if (allDependencies.
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if (allDependencies.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
810
|
+
|
|
811
|
+
if (allDependencies.next) {
|
|
812
|
+
detected.add("next");
|
|
813
|
+
}
|
|
814
|
+
if (allDependencies.nuxt) {
|
|
815
|
+
detected.add("nuxt");
|
|
816
|
+
}
|
|
817
|
+
if (allDependencies.sveltekit) {
|
|
818
|
+
detected.add("svelte");
|
|
819
|
+
}
|
|
820
|
+
if (allDependencies.react) {
|
|
821
|
+
detected.add("react");
|
|
822
|
+
}
|
|
823
|
+
if (allDependencies.vue) {
|
|
824
|
+
detected.add("vue");
|
|
540
825
|
}
|
|
541
826
|
|
|
827
|
+
if (fs.existsSync(path.join(targetDir, "next.config.js")) || fs.existsSync(path.join(targetDir, "next.config.mjs"))) {
|
|
828
|
+
detected.add("next");
|
|
829
|
+
}
|
|
830
|
+
if (fs.existsSync(path.join(targetDir, "nuxt.config.js")) || fs.existsSync(path.join(targetDir, "nuxt.config.ts"))) {
|
|
831
|
+
detected.add("nuxt");
|
|
832
|
+
}
|
|
833
|
+
if (fs.existsSync(path.join(targetDir, "svelte.config.js"))) {
|
|
834
|
+
detected.add("svelte");
|
|
835
|
+
}
|
|
836
|
+
if (fs.existsSync(path.join(targetDir, "vite.config.js")) || fs.existsSync(path.join(targetDir, "vite.config.ts"))) {
|
|
837
|
+
if (allDependencies["@vitejs/plugin-react"]) {
|
|
838
|
+
detected.add("react");
|
|
839
|
+
}
|
|
840
|
+
if (allDependencies["@vitejs/plugin-vue"]) {
|
|
841
|
+
detected.add("vue");
|
|
842
|
+
}
|
|
843
|
+
if (allDependencies["@sveltejs/vite-plugin-svelte"]) {
|
|
844
|
+
detected.add("svelte");
|
|
845
|
+
}
|
|
846
|
+
}
|
|
542
847
|
return Array.from(detected);
|
|
543
848
|
}
|
|
544
849
|
}
|
|
545
850
|
|
|
546
851
|
// ============================================================
|
|
547
|
-
//
|
|
852
|
+
// 🧱 CUSTOM STRUCTURAL HOISTED TEMPLATE MANAGER
|
|
548
853
|
// ============================================================
|
|
549
|
-
class
|
|
550
|
-
constructor(
|
|
551
|
-
this.
|
|
552
|
-
this.templatesDir = path.join(targetDir, ".templates");
|
|
854
|
+
class TemplateManager {
|
|
855
|
+
constructor(baseDir, safeQuestion) {
|
|
856
|
+
this.baseDir = baseDir;
|
|
553
857
|
this.safeQuestion = safeQuestion;
|
|
858
|
+
this.templateSources = [{ name: 'local', path: path.join(this.baseDir, '.templates') }];
|
|
554
859
|
}
|
|
555
860
|
|
|
556
|
-
async
|
|
557
|
-
|
|
558
|
-
|
|
861
|
+
async listAvailableTemplates() {
|
|
862
|
+
const allTemplates = new Set();
|
|
863
|
+
for (const source of this.templateSources) {
|
|
864
|
+
if (source.name === 'local') {
|
|
865
|
+
const localTemplatesPath = source.path;
|
|
866
|
+
if (fs.existsSync(localTemplatesPath)) {
|
|
867
|
+
const templates = fs.readdirSync(localTemplatesPath, { withFileTypes: true })
|
|
868
|
+
.filter(dirent => { return dirent.isDirectory(); })
|
|
869
|
+
.map(dirent => { return dirent.name; });
|
|
870
|
+
templates.forEach(t => { return allTemplates.add(t); });
|
|
871
|
+
}
|
|
872
|
+
}
|
|
559
873
|
}
|
|
560
|
-
|
|
561
|
-
.filter(dirent => dirent.isDirectory())
|
|
562
|
-
.map(dirent => dirent.name);
|
|
563
|
-
return templateFolders;
|
|
874
|
+
return Array.from(allTemplates);
|
|
564
875
|
}
|
|
565
876
|
|
|
566
|
-
async
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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)}`);
|
|
877
|
+
async getTemplatePath(templateName) {
|
|
878
|
+
for (const source of this.templateSources) {
|
|
879
|
+
if (source.name === 'local') {
|
|
880
|
+
const templatePath = path.join(source.path, templateName);
|
|
881
|
+
if (fs.existsSync(templatePath)) {
|
|
882
|
+
return templatePath;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
592
885
|
}
|
|
593
|
-
|
|
886
|
+
return null;
|
|
594
887
|
}
|
|
595
888
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
889
|
+
async promptForVariables(templateName) {
|
|
890
|
+
const templatePath = await this.getTemplatePath(templateName);
|
|
891
|
+
if (!templatePath) {
|
|
892
|
+
console.log(` ⚠️ Template '${templateName}' not found.`);
|
|
893
|
+
return {};
|
|
894
|
+
}
|
|
895
|
+
const configPath = path.join(templatePath, '_config.json');
|
|
896
|
+
if (fs.existsSync(configPath)) {
|
|
897
|
+
try {
|
|
898
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
899
|
+
const variables = {};
|
|
900
|
+
for (const key in config.prompts) {
|
|
901
|
+
const prompt = config.prompts[key];
|
|
902
|
+
let answer = await this.safeQuestion(`❓ ${prompt.message || key}: `);
|
|
903
|
+
if (prompt.type === 'boolean') {
|
|
904
|
+
answer = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
905
|
+
} else if (prompt.type === 'number') {
|
|
906
|
+
answer = parseFloat(answer);
|
|
907
|
+
}
|
|
908
|
+
variables[key] = answer;
|
|
909
|
+
}
|
|
910
|
+
return variables;
|
|
911
|
+
} catch (jsonConfigReadError) {
|
|
912
|
+
console.error(` ❌ Error reading template config for '${templateName}': ${jsonConfigReadError.message}`);
|
|
913
|
+
return {};
|
|
605
914
|
}
|
|
606
915
|
}
|
|
607
|
-
return
|
|
916
|
+
return {};
|
|
608
917
|
}
|
|
609
918
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
919
|
+
async generate(templateName, variables) {
|
|
920
|
+
const templatePath = await this.getTemplatePath(templateName);
|
|
921
|
+
if (!templatePath) {
|
|
922
|
+
return;
|
|
614
923
|
}
|
|
615
|
-
|
|
616
|
-
}
|
|
924
|
+
console.log(` 🚀 Generating '${templateName}' template with full route and token mutations...`);
|
|
617
925
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
926
|
+
const renderFile = async (srcPath, destPath, vars) => {
|
|
927
|
+
const content = fs.readFileSync(srcPath, 'utf8');
|
|
928
|
+
let renderedContent = content;
|
|
929
|
+
for (const key in vars) {
|
|
930
|
+
renderedContent = renderedContent.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), vars[key]);
|
|
931
|
+
}
|
|
932
|
+
fs.writeFileSync(destPath, renderedContent);
|
|
933
|
+
};
|
|
622
934
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
935
|
+
const processDirectory = async (currentSrcDir, currentDestDir, vars) => {
|
|
936
|
+
fs.mkdirSync(currentDestDir, { recursive: true });
|
|
937
|
+
const items = fs.readdirSync(currentSrcDir, { withFileTypes: true });
|
|
938
|
+
for (const item of items) {
|
|
939
|
+
if (item.name === '_config.json' || item.name === 'config.json') {
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
let mutatedName = item.name;
|
|
944
|
+
for (const key in vars) {
|
|
945
|
+
mutatedName = mutatedName.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), vars[key]);
|
|
946
|
+
}
|
|
947
|
+
mutatedName = mutatedName.replace(/_([a-zA-Z0-9_]+)_/g, (match, p1) => { return vars[p1] || match; });
|
|
948
|
+
|
|
949
|
+
const srcItemPath = path.join(currentSrcDir, item.name);
|
|
950
|
+
const destItemPath = path.join(currentDestDir, mutatedName);
|
|
951
|
+
|
|
952
|
+
if (item.isDirectory()) {
|
|
953
|
+
await processDirectory(srcItemPath, destItemPath, vars);
|
|
954
|
+
} else {
|
|
955
|
+
await renderFile(srcItemPath, destItemPath, vars);
|
|
629
956
|
}
|
|
630
957
|
}
|
|
631
|
-
}
|
|
632
|
-
|
|
958
|
+
};
|
|
959
|
+
await processDirectory(templatePath, this.baseDir, variables);
|
|
960
|
+
console.log(` ✅ Template '${templateName}' generated successfully.`);
|
|
633
961
|
}
|
|
634
962
|
}
|
|
635
963
|
|
|
964
|
+
/**
|
|
965
|
+
* Generiert einen sauberen ASCII-Layout-Baum für das automatisierte README-Dokument.
|
|
966
|
+
*/
|
|
636
967
|
function buildAsciiTree(dir, prefix = '') {
|
|
637
968
|
const results = [];
|
|
638
969
|
try {
|
|
639
970
|
const files = fs.readdirSync(dir);
|
|
640
|
-
const filtered = files.filter(f => !IGNORED_DIRS.has(f) && !f.startsWith('.'));
|
|
971
|
+
const filtered = files.filter(f => { return !IGNORED_DIRS.has(f) && !f.startsWith('.'); });
|
|
641
972
|
|
|
642
973
|
filtered.forEach((file, index) => {
|
|
643
974
|
const isLast = index === filtered.length - 1;
|
|
@@ -650,25 +981,91 @@ function buildAsciiTree(dir, prefix = '') {
|
|
|
650
981
|
results.push(...buildAsciiTree(fullPath, newPrefix));
|
|
651
982
|
}
|
|
652
983
|
});
|
|
653
|
-
} catch (
|
|
984
|
+
} catch (asciiTreeTreeError) {}
|
|
654
985
|
return results;
|
|
655
986
|
}
|
|
656
987
|
|
|
657
988
|
// ============================================================
|
|
658
|
-
//
|
|
659
|
-
// type-only imports, re-exports, and dynamic imports
|
|
989
|
+
// STRUKTURELLES IMPLEMENTIERUNGSHELFER SYSTEM
|
|
660
990
|
// ============================================================
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* KORREKTUR: Prüft, ob ein extrahierter Import-Bezeichner im Programmcode verwendet wird.
|
|
994
|
+
* Führt den regulären Ausdruck nun gegen das tatsächliche executionCode-Skelett aus, anstatt gegen den Alias selbst.
|
|
995
|
+
*/
|
|
996
|
+
function analyzeIdentifierUsage(pkg, identifiers, executionCode) {
|
|
997
|
+
const autoUsedMarkers = new Set(['__SIDE_EFFECT__', '__DYNAMIC__', '__REEXPORT__', '__TYPE_ONLY__']);
|
|
998
|
+
for (const id of identifiers) {
|
|
999
|
+
if (autoUsedMarkers.has(id)) {
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
for (const id of identifiers) {
|
|
1004
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1005
|
+
if (new RegExp(`\\b${escaped}\\b`).test(executionCode)) {
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const aliases = PACKAGE_IMPORT_ALIASES[pkg] || [];
|
|
1010
|
+
for (const alias of aliases) {
|
|
1011
|
+
const escapedAlias = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1012
|
+
if (new RegExp(`\\b${escapedAlias}\\b`).test(executionCode)) { // 👈 Use the properly escaped token here
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Findet Pakete, die im Programmcode importiert wurden, aber nicht in der package.json deklariert sind.
|
|
1021
|
+
*/
|
|
1022
|
+
function detectGhostDependencies(allImportedPackages, declaredDeps, declaredDevDeps) {
|
|
1023
|
+
const allDeclared = new Set([...declaredDeps, ...declaredDevDeps]);
|
|
1024
|
+
const ghosts = new Set();
|
|
1025
|
+
for (const pkg of allImportedPackages) {
|
|
1026
|
+
if (!allDeclared.has(pkg) && !builtinModules.includes(pkg)) {
|
|
1027
|
+
ghosts.add(pkg);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return ghosts;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Ermittelt ungenutzte verwaiste Abhängigkeiten aus der package.json.
|
|
1035
|
+
*/
|
|
1036
|
+
function detectOrphanedDependencies(declaredDeps, allImportedPackages, binariesUsed, devTooling) {
|
|
1037
|
+
const orphans = new Set();
|
|
1038
|
+
for (const dep of declaredDeps) {
|
|
1039
|
+
if (devTooling.has(dep) || dep.startsWith('@types/')) {
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
const binaryPkg = Object.values(BINARY_TO_PACKAGE_MAP).find(p => { return p === dep; });
|
|
1043
|
+
if (binaryPkg && binariesUsed.has(dep)) {
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
if (!allImportedPackages.has(dep)) {
|
|
1047
|
+
orphans.add(dep);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return orphans;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
661
1053
|
function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLocations, exportedSymbols, stats, currentFilePath) {
|
|
1054
|
+
extractAdvancedSymbols(ast, currentFilePath, stats);
|
|
1055
|
+
|
|
662
1056
|
walk.simple(ast, {
|
|
663
1057
|
ImportDeclaration(node) {
|
|
664
1058
|
const importSource = node.source.value;
|
|
665
1059
|
const pkg = cleanPackageName(importSource);
|
|
666
1060
|
|
|
667
1061
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
668
|
-
// External package import
|
|
669
1062
|
fileRawDeps.add(pkg);
|
|
670
|
-
if (!importedIdentifiers.has(pkg))
|
|
671
|
-
|
|
1063
|
+
if (!importedIdentifiers.has(pkg)) {
|
|
1064
|
+
importedIdentifiers.set(pkg, new Set());
|
|
1065
|
+
}
|
|
1066
|
+
if (!importedLocations.has(pkg)) {
|
|
1067
|
+
importedLocations.set(pkg, []);
|
|
1068
|
+
}
|
|
672
1069
|
importedLocations.get(pkg).push(node.loc?.start?.line ?? 0);
|
|
673
1070
|
|
|
674
1071
|
node.specifiers.forEach(spec => {
|
|
@@ -676,23 +1073,21 @@ function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLo
|
|
|
676
1073
|
importedIdentifiers.get(pkg).add(spec.local.name);
|
|
677
1074
|
} else if (spec.type === 'ImportSpecifier') {
|
|
678
1075
|
importedIdentifiers.get(pkg).add(spec.local.name);
|
|
679
|
-
// Also track the imported name (before 'as') for side-effect detection
|
|
680
1076
|
if (spec.imported && spec.imported.name !== spec.local.name) {
|
|
681
1077
|
importedIdentifiers.get(pkg).add(spec.imported.name);
|
|
682
1078
|
}
|
|
683
1079
|
}
|
|
684
1080
|
});
|
|
685
|
-
|
|
686
|
-
// Side-effect only import: import 'pkg' — always considered "used"
|
|
687
1081
|
if (node.specifiers.length === 0) {
|
|
688
1082
|
importedIdentifiers.get(pkg).add('__SIDE_EFFECT__');
|
|
689
1083
|
}
|
|
690
1084
|
} else if (importSource.startsWith('.') || importSource.startsWith('/')) {
|
|
691
|
-
// Local file import
|
|
692
1085
|
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
693
1086
|
const normalizedPath = path.normalize(resolvedPath);
|
|
694
1087
|
|
|
695
|
-
if (!stats.localFileImports)
|
|
1088
|
+
if (!stats.localFileImports) {
|
|
1089
|
+
stats.localFileImports = new Map();
|
|
1090
|
+
}
|
|
696
1091
|
if (!stats.localFileImports.has(normalizedPath)) {
|
|
697
1092
|
stats.localFileImports.set(normalizedPath, new Set());
|
|
698
1093
|
}
|
|
@@ -710,15 +1105,18 @@ function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLo
|
|
|
710
1105
|
}
|
|
711
1106
|
},
|
|
712
1107
|
VariableDeclarator(node) {
|
|
713
|
-
if (node.init && node.init.type === 'CallExpression' &&
|
|
714
|
-
node.init.callee.type === 'Identifier' && node.init.callee.name === 'require') {
|
|
1108
|
+
if (node.init && node.init.type === 'CallExpression' && node.init.callee.type === 'Identifier' && node.init.callee.name === 'require') {
|
|
715
1109
|
const arg = node.init.arguments[0];
|
|
716
1110
|
if (arg && arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
717
1111
|
const pkg = cleanPackageName(arg.value);
|
|
718
1112
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
719
1113
|
fileRawDeps.add(pkg);
|
|
720
|
-
if (!importedIdentifiers.has(pkg))
|
|
721
|
-
|
|
1114
|
+
if (!importedIdentifiers.has(pkg)) {
|
|
1115
|
+
importedIdentifiers.set(pkg, new Set());
|
|
1116
|
+
}
|
|
1117
|
+
if (!importedLocations.has(pkg)) {
|
|
1118
|
+
importedLocations.set(pkg, []);
|
|
1119
|
+
}
|
|
722
1120
|
importedLocations.get(pkg).push(node.loc?.start?.line ?? 0);
|
|
723
1121
|
|
|
724
1122
|
const extractBindings = (idNode) => {
|
|
@@ -726,8 +1124,12 @@ function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLo
|
|
|
726
1124
|
importedIdentifiers.get(pkg).add(idNode.name);
|
|
727
1125
|
} else if (idNode.type === 'ObjectPattern') {
|
|
728
1126
|
idNode.properties.forEach(p => {
|
|
729
|
-
if (p.value && p.value.type === 'Identifier')
|
|
730
|
-
|
|
1127
|
+
if (p.value && p.value.type === 'Identifier') {
|
|
1128
|
+
importedIdentifiers.get(pkg).add(p.value.name);
|
|
1129
|
+
}
|
|
1130
|
+
if (p.key && p.key.type === 'Identifier') {
|
|
1131
|
+
importedIdentifiers.get(pkg).add(p.key.name);
|
|
1132
|
+
}
|
|
731
1133
|
});
|
|
732
1134
|
}
|
|
733
1135
|
};
|
|
@@ -741,7 +1143,9 @@ function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLo
|
|
|
741
1143
|
const pkg = cleanPackageName(node.source.value);
|
|
742
1144
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
743
1145
|
fileRawDeps.add(pkg);
|
|
744
|
-
if (!importedIdentifiers.has(pkg))
|
|
1146
|
+
if (!importedIdentifiers.has(pkg)) {
|
|
1147
|
+
importedIdentifiers.set(pkg, new Set());
|
|
1148
|
+
}
|
|
745
1149
|
importedIdentifiers.get(pkg).add('__DYNAMIC__');
|
|
746
1150
|
}
|
|
747
1151
|
}
|
|
@@ -774,7 +1178,9 @@ function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLo
|
|
|
774
1178
|
const pkg = cleanPackageName(node.source.value);
|
|
775
1179
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
776
1180
|
fileRawDeps.add(pkg);
|
|
777
|
-
if (!importedIdentifiers.has(pkg))
|
|
1181
|
+
if (!importedIdentifiers.has(pkg)) {
|
|
1182
|
+
importedIdentifiers.set(pkg, new Set());
|
|
1183
|
+
}
|
|
778
1184
|
importedIdentifiers.get(pkg).add('__REEXPORT__');
|
|
779
1185
|
}
|
|
780
1186
|
}
|
|
@@ -784,7 +1190,9 @@ function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLo
|
|
|
784
1190
|
const pkg = cleanPackageName(node.source.value);
|
|
785
1191
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
786
1192
|
fileRawDeps.add(pkg);
|
|
787
|
-
if (!importedIdentifiers.has(pkg))
|
|
1193
|
+
if (!importedIdentifiers.has(pkg)) {
|
|
1194
|
+
importedIdentifiers.set(pkg, new Set());
|
|
1195
|
+
}
|
|
788
1196
|
importedIdentifiers.get(pkg).add('__REEXPORT__');
|
|
789
1197
|
}
|
|
790
1198
|
}
|
|
@@ -792,38 +1200,40 @@ function extractImportsFromAST(ast, fileRawDeps, importedIdentifiers, importedLo
|
|
|
792
1200
|
});
|
|
793
1201
|
}
|
|
794
1202
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1203
|
+
/**
|
|
1204
|
+
* Textbasierter Regex-Ersatz-Parser, falls Acorn auf Syntaxfehler stößt.
|
|
1205
|
+
*/
|
|
798
1206
|
function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, importedLocations, stats, currentFilePath) {
|
|
799
1207
|
codeLines.forEach((line, lineIdx) => {
|
|
800
1208
|
const lineNum = lineIdx + 1;
|
|
801
|
-
|
|
802
|
-
// import type { ... } from '...' — type-only, mark as side-effect
|
|
803
1209
|
const typeImportMatch = line.match(/\bimport\s+type\s+\{[^}]*\}\s+from\s+['"]([^'"]+)['"]/);
|
|
804
1210
|
if (typeImportMatch) {
|
|
805
1211
|
const importSource = typeImportMatch[1];
|
|
806
1212
|
const pkg = cleanPackageName(importSource);
|
|
807
1213
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
808
1214
|
fileRawDeps.add(pkg);
|
|
809
|
-
if (!importedIdentifiers.has(pkg))
|
|
1215
|
+
if (!importedIdentifiers.has(pkg)) {
|
|
1216
|
+
importedIdentifiers.set(pkg, new Set());
|
|
1217
|
+
}
|
|
810
1218
|
importedIdentifiers.get(pkg).add('__TYPE_ONLY__');
|
|
811
|
-
if (!importedLocations.has(pkg))
|
|
1219
|
+
if (!importedLocations.has(pkg)) {
|
|
1220
|
+
importedLocations.set(pkg, []);
|
|
1221
|
+
}
|
|
812
1222
|
importedLocations.get(pkg).push(lineNum);
|
|
813
1223
|
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
814
1224
|
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
815
1225
|
const normalizedPath = path.normalize(resolvedPath);
|
|
816
|
-
if (!stats.localFileImports)
|
|
1226
|
+
if (!stats.localFileImports) {
|
|
1227
|
+
stats.localFileImports = new Map();
|
|
1228
|
+
}
|
|
817
1229
|
if (!stats.localFileImports.has(normalizedPath)) {
|
|
818
1230
|
stats.localFileImports.set(normalizedPath, new Set());
|
|
819
1231
|
}
|
|
820
|
-
stats.localFileImports.get(normalizedPath).add('__TYPE_ONLY__');
|
|
1232
|
+
stats.localFileImports.get(normalizedPath).add('__TYPE_ONLY__');
|
|
821
1233
|
}
|
|
822
1234
|
return;
|
|
823
1235
|
}
|
|
824
1236
|
|
|
825
|
-
// import DefaultExport from '...'
|
|
826
|
-
// import * as Namespace from '...'
|
|
827
1237
|
const esmDefaultMatch = line.match(/\bimport\s+(?:\*\s+as\s+)?([a-zA-Z0-9_$]+)\s+from\s+['"]([^'"]+)['"]/);
|
|
828
1238
|
if (esmDefaultMatch) {
|
|
829
1239
|
const id = esmDefaultMatch[1];
|
|
@@ -831,14 +1241,20 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
831
1241
|
const pkg = cleanPackageName(importSource);
|
|
832
1242
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
833
1243
|
fileRawDeps.add(pkg);
|
|
834
|
-
if (!importedIdentifiers.has(pkg))
|
|
1244
|
+
if (!importedIdentifiers.has(pkg)) {
|
|
1245
|
+
importedIdentifiers.set(pkg, new Set());
|
|
1246
|
+
}
|
|
835
1247
|
importedIdentifiers.get(pkg).add(id);
|
|
836
|
-
if (!importedLocations.has(pkg))
|
|
1248
|
+
if (!importedLocations.has(pkg)) {
|
|
1249
|
+
importedLocations.set(pkg, []);
|
|
1250
|
+
}
|
|
837
1251
|
importedLocations.get(pkg).push(lineNum);
|
|
838
1252
|
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
839
1253
|
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
840
1254
|
const normalizedPath = path.normalize(resolvedPath);
|
|
841
|
-
if (!stats.localFileImports)
|
|
1255
|
+
if (!stats.localFileImports) {
|
|
1256
|
+
stats.localFileImports = new Map();
|
|
1257
|
+
}
|
|
842
1258
|
if (!stats.localFileImports.has(normalizedPath)) {
|
|
843
1259
|
stats.localFileImports.set(normalizedPath, new Set());
|
|
844
1260
|
}
|
|
@@ -847,57 +1263,68 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
847
1263
|
return;
|
|
848
1264
|
}
|
|
849
1265
|
|
|
850
|
-
// import { named, exports } from '...'
|
|
851
1266
|
const esmNamedMatch = line.match(/\bimport\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/);
|
|
852
1267
|
if (esmNamedMatch) {
|
|
853
1268
|
const importSource = esmNamedMatch[2];
|
|
854
1269
|
const pkg = cleanPackageName(importSource);
|
|
855
1270
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
856
|
-
if (!importedIdentifiers.has(pkg))
|
|
1271
|
+
if (!importedIdentifiers.has(pkg)) {
|
|
1272
|
+
importedIdentifiers.set(pkg, new Set());
|
|
1273
|
+
}
|
|
857
1274
|
fileRawDeps.add(pkg);
|
|
858
1275
|
esmNamedMatch[1].split(',').forEach(part => {
|
|
859
1276
|
const chunk = part.trim();
|
|
860
|
-
if (!chunk)
|
|
1277
|
+
if (!chunk) {
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
861
1280
|
const id = chunk.includes(' as ') ? chunk.split(' as ')[1].trim() : chunk;
|
|
862
1281
|
importedIdentifiers.get(pkg).add(id);
|
|
863
|
-
// Also add the original name
|
|
864
|
-
if (chunk.includes(' as ')) importedIdentifiers.get(pkg).add(chunk.split(' as ')[0].trim());
|
|
865
1282
|
});
|
|
866
|
-
if (!importedLocations.has(pkg))
|
|
1283
|
+
if (!importedLocations.has(pkg)) {
|
|
1284
|
+
importedLocations.set(pkg, []);
|
|
1285
|
+
}
|
|
867
1286
|
importedLocations.get(pkg).push(lineNum);
|
|
868
1287
|
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
869
1288
|
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
870
1289
|
const normalizedPath = path.normalize(resolvedPath);
|
|
871
|
-
if (!stats.localFileImports)
|
|
1290
|
+
if (!stats.localFileImports) {
|
|
1291
|
+
stats.localFileImports = new Map();
|
|
1292
|
+
}
|
|
872
1293
|
if (!stats.localFileImports.has(normalizedPath)) {
|
|
873
1294
|
stats.localFileImports.set(normalizedPath, new Set());
|
|
874
1295
|
}
|
|
875
1296
|
esmNamedMatch[1].split(',').forEach(part => {
|
|
876
1297
|
const chunk = part.trim();
|
|
877
|
-
if (!chunk)
|
|
1298
|
+
if (!chunk) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
878
1301
|
const id = chunk.includes(' as ') ? chunk.split(' as ')[1].trim() : chunk;
|
|
879
1302
|
stats.localFileImports.get(normalizedPath).add(id);
|
|
880
|
-
if (chunk.includes(' as ')) stats.localFileImports.get(normalizedPath).add(chunk.split(' as ')[0].trim());
|
|
881
1303
|
});
|
|
882
1304
|
}
|
|
883
1305
|
return;
|
|
884
1306
|
}
|
|
885
1307
|
|
|
886
|
-
// Side-effect only: import '...'
|
|
887
1308
|
const sideEffectMatch = line.match(/\bimport\s+['"]([^'"]+)['"]/);
|
|
888
1309
|
if (sideEffectMatch) {
|
|
889
1310
|
const importSource = sideEffectMatch[1];
|
|
890
1311
|
const pkg = cleanPackageName(importSource);
|
|
891
1312
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
892
1313
|
fileRawDeps.add(pkg);
|
|
893
|
-
if (!importedIdentifiers.has(pkg))
|
|
1314
|
+
if (!importedIdentifiers.has(pkg)) {
|
|
1315
|
+
importedIdentifiers.set(pkg, new Set());
|
|
1316
|
+
}
|
|
894
1317
|
importedIdentifiers.get(pkg).add('__SIDE_EFFECT__');
|
|
895
|
-
if (!importedLocations.has(pkg))
|
|
1318
|
+
if (!importedLocations.has(pkg)) {
|
|
1319
|
+
importedLocations.set(pkg, []);
|
|
1320
|
+
}
|
|
896
1321
|
importedLocations.get(pkg).push(lineNum);
|
|
897
1322
|
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
898
1323
|
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
899
1324
|
const normalizedPath = path.normalize(resolvedPath);
|
|
900
|
-
if (!stats.localFileImports)
|
|
1325
|
+
if (!stats.localFileImports) {
|
|
1326
|
+
stats.localFileImports = new Map();
|
|
1327
|
+
}
|
|
901
1328
|
if (!stats.localFileImports.has(normalizedPath)) {
|
|
902
1329
|
stats.localFileImports.set(normalizedPath, new Set());
|
|
903
1330
|
}
|
|
@@ -906,22 +1333,27 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
906
1333
|
return;
|
|
907
1334
|
}
|
|
908
1335
|
|
|
909
|
-
|
|
910
|
-
const cjsMatch = line.match(/\b(?:const|let|var)\s+([a-zA-Z0-9_$]+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
1336
|
+
const cjsMatch = line.match(/\b(const|let|var)\s+([a-zA-Z0-9_$]+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
911
1337
|
if (cjsMatch) {
|
|
912
|
-
const id = cjsMatch[
|
|
913
|
-
const importSource = cjsMatch[
|
|
1338
|
+
const id = cjsMatch[2];
|
|
1339
|
+
const importSource = cjsMatch[3];
|
|
914
1340
|
const pkg = cleanPackageName(importSource);
|
|
915
1341
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
916
1342
|
fileRawDeps.add(pkg);
|
|
917
|
-
if (!importedIdentifiers.has(pkg))
|
|
1343
|
+
if (!importedIdentifiers.has(pkg)) {
|
|
1344
|
+
importedIdentifiers.set(pkg, new Set());
|
|
1345
|
+
}
|
|
918
1346
|
importedIdentifiers.get(pkg).add(id);
|
|
919
|
-
if (!importedLocations.has(pkg))
|
|
1347
|
+
if (!importedLocations.has(pkg)) {
|
|
1348
|
+
importedLocations.set(pkg, []);
|
|
1349
|
+
}
|
|
920
1350
|
importedLocations.get(pkg).push(lineNum);
|
|
921
1351
|
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
922
1352
|
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
923
1353
|
const normalizedPath = path.normalize(resolvedPath);
|
|
924
|
-
if (!stats.localFileImports)
|
|
1354
|
+
if (!stats.localFileImports) {
|
|
1355
|
+
stats.localFileImports = new Map();
|
|
1356
|
+
}
|
|
925
1357
|
if (!stats.localFileImports.has(normalizedPath)) {
|
|
926
1358
|
stats.localFileImports.set(normalizedPath, new Set());
|
|
927
1359
|
}
|
|
@@ -930,32 +1362,43 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
930
1362
|
return;
|
|
931
1363
|
}
|
|
932
1364
|
|
|
933
|
-
|
|
934
|
-
const cjsDestructMatch = line.match(/\b(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
1365
|
+
const cjsDestructMatch = line.match(/\b(const|let|var)\s*\{([^}]+)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
935
1366
|
if (cjsDestructMatch) {
|
|
936
|
-
const importSource = cjsDestructMatch[
|
|
1367
|
+
const importSource = cjsDestructMatch[3];
|
|
937
1368
|
const pkg = cleanPackageName(importSource);
|
|
938
1369
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
939
|
-
if (!importedIdentifiers.has(pkg))
|
|
1370
|
+
if (!importedIdentifiers.has(pkg)) {
|
|
1371
|
+
importedIdentifiers.set(pkg, new Set());
|
|
1372
|
+
}
|
|
940
1373
|
fileRawDeps.add(pkg);
|
|
941
|
-
cjsDestructMatch[
|
|
1374
|
+
cjsDestructMatch[2].split(',').forEach(part => {
|
|
942
1375
|
const chunk = part.trim();
|
|
943
|
-
if (!chunk)
|
|
1376
|
+
if (!chunk) {
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
944
1379
|
const id = chunk.includes(':') ? chunk.split(':')[1].trim() : chunk;
|
|
945
1380
|
importedIdentifiers.get(pkg).add(id);
|
|
946
1381
|
});
|
|
947
|
-
|
|
1382
|
+
|
|
1383
|
+
// KORREKTUR: Korrekte Initialisierung des Arrays innerhalb von importedLocations zur Vermeidung von TypeErrors
|
|
1384
|
+
if (!importedLocations.has(pkg)) {
|
|
1385
|
+
importedLocations.set(pkg, []);
|
|
1386
|
+
}
|
|
948
1387
|
importedLocations.get(pkg).push(lineNum);
|
|
949
1388
|
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
950
1389
|
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
951
1390
|
const normalizedPath = path.normalize(resolvedPath);
|
|
952
|
-
if (!stats.localFileImports)
|
|
1391
|
+
if (!stats.localFileImports) {
|
|
1392
|
+
stats.localFileImports = new Map();
|
|
1393
|
+
}
|
|
953
1394
|
if (!stats.localFileImports.has(normalizedPath)) {
|
|
954
1395
|
stats.localFileImports.set(normalizedPath, new Set());
|
|
955
1396
|
}
|
|
956
|
-
cjsDestructMatch[
|
|
1397
|
+
cjsDestructMatch[2].split(',').forEach(part => {
|
|
957
1398
|
const chunk = part.trim();
|
|
958
|
-
if (!chunk)
|
|
1399
|
+
if (!chunk) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
959
1402
|
const id = chunk.includes(':') ? chunk.split(':')[1].trim() : chunk;
|
|
960
1403
|
stats.localFileImports.get(normalizedPath).add(id);
|
|
961
1404
|
});
|
|
@@ -963,21 +1406,26 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
963
1406
|
return;
|
|
964
1407
|
}
|
|
965
1408
|
|
|
966
|
-
// Dynamic import: import('...')
|
|
967
1409
|
const dynamicMatch = line.match(/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
968
1410
|
if (dynamicMatch) {
|
|
969
1411
|
const importSource = dynamicMatch[1];
|
|
970
1412
|
const pkg = cleanPackageName(importSource);
|
|
971
1413
|
if (pkg && !builtinModules.includes(pkg)) {
|
|
972
1414
|
fileRawDeps.add(pkg);
|
|
973
|
-
if (!importedIdentifiers.has(pkg))
|
|
1415
|
+
if (!importedIdentifiers.has(pkg)) {
|
|
1416
|
+
importedIdentifiers.set(pkg, new Set());
|
|
1417
|
+
}
|
|
974
1418
|
importedIdentifiers.get(pkg).add('__DYNAMIC__');
|
|
975
|
-
if (!importedLocations.has(pkg))
|
|
1419
|
+
if (!importedLocations.has(pkg)) {
|
|
1420
|
+
importedLocations.set(pkg, []);
|
|
1421
|
+
}
|
|
976
1422
|
importedLocations.get(pkg).push(lineNum);
|
|
977
1423
|
} else if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
978
1424
|
const resolvedPath = path.resolve(path.dirname(currentFilePath), importSource);
|
|
979
1425
|
const normalizedPath = path.normalize(resolvedPath);
|
|
980
|
-
if (!stats.localFileImports)
|
|
1426
|
+
if (!stats.localFileImports) {
|
|
1427
|
+
stats.localFileImports = new Map();
|
|
1428
|
+
}
|
|
981
1429
|
if (!stats.localFileImports.has(normalizedPath)) {
|
|
982
1430
|
stats.localFileImports.set(normalizedPath, new Set());
|
|
983
1431
|
}
|
|
@@ -987,85 +1435,11 @@ function extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, imp
|
|
|
987
1435
|
});
|
|
988
1436
|
}
|
|
989
1437
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
// ============================================================
|
|
994
|
-
function analyzeIdentifierUsage(pkg, identifiers, executionCode) {
|
|
995
|
-
// Always-used markers: side-effect, dynamic, re-export, type-only
|
|
996
|
-
const autoUsedMarkers = new Set(['__SIDE_EFFECT__', '__DYNAMIC__', '__REEXPORT__', '__TYPE_ONLY__']);
|
|
997
|
-
for (const id of identifiers) {
|
|
998
|
-
if (autoUsedMarkers.has(id)) return true;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// Check known aliases for this package
|
|
1002
|
-
const knownAliases = PACKAGE_IMPORT_ALIASES[pkg] || [];
|
|
1003
|
-
|
|
1004
|
-
for (const identifier of identifiers) {
|
|
1005
|
-
if (!identifier || identifier.startsWith('__')) continue;
|
|
1006
|
-
// Escape special regex chars in identifier
|
|
1007
|
-
const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1008
|
-
const usagePattern = new RegExp(`\\b${escaped}\\b`);
|
|
1009
|
-
if (usagePattern.test(executionCode)) return true;
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
// Check if any known alias for this package appears in the code
|
|
1013
|
-
for (const alias of knownAliases) {
|
|
1014
|
-
const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1015
|
-
const aliasPattern = new RegExp(`\\b${escaped}\\b`);
|
|
1016
|
-
if (aliasPattern.test(executionCode)) return true;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
return false;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// ============================================================
|
|
1023
|
-
// GHOST DEPENDENCY DETECTOR
|
|
1024
|
-
// Finds packages used in code but NOT declared in package.json
|
|
1025
|
-
// This is the most critical error: will fail at runtime/deploy
|
|
1026
|
-
// ============================================================
|
|
1027
|
-
function detectGhostDependencies(allImportedPackages, declaredDeps, declaredDevDeps) {
|
|
1028
|
-
const allDeclared = new Set([...declaredDeps, ...declaredDevDeps]);
|
|
1029
|
-
const ghosts = new Set();
|
|
1030
|
-
|
|
1031
|
-
for (const pkg of allImportedPackages) {
|
|
1032
|
-
if (!allDeclared.has(pkg) && !builtinModules.includes(pkg)) {
|
|
1033
|
-
ghosts.add(pkg);
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
return ghosts;
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
// ============================================================
|
|
1040
|
-
// ORPHANED DEPENDENCY DETECTOR
|
|
1041
|
-
// Finds packages in package.json that are never imported anywhere
|
|
1042
|
-
// in the codebase (candidates for removal)
|
|
1043
|
-
// ============================================================
|
|
1044
|
-
function detectOrphanedDependencies(declaredDeps, allImportedPackages, binariesUsed, devTooling) {
|
|
1045
|
-
const orphans = new Set();
|
|
1046
|
-
|
|
1047
|
-
for (const dep of declaredDeps) {
|
|
1048
|
-
// Skip dev tooling — they're used via CLI, not imports
|
|
1049
|
-
if (devTooling.has(dep) || dep.startsWith('@types/')) continue;
|
|
1050
|
-
|
|
1051
|
-
// Check if it's used as a binary
|
|
1052
|
-
const binaryPkg = Object.values(BINARY_TO_PACKAGE_MAP).find(p => p === dep);
|
|
1053
|
-
if (binaryPkg && binariesUsed.has(dep)) continue;
|
|
1054
|
-
|
|
1055
|
-
// Check if it's imported anywhere
|
|
1056
|
-
if (!allImportedPackages.has(dep)) {
|
|
1057
|
-
orphans.add(dep);
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
return orphans;
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
// ============================================================
|
|
1064
|
-
// HIGH PERFORMANCE AST WORKSPACE PARSING ENGINE
|
|
1065
|
-
// ============================================================
|
|
1438
|
+
/**
|
|
1439
|
+
* Kernfunktion zur iterativen Rekursion durch alle Ordnerstrukturen des Workspaces.
|
|
1440
|
+
*/
|
|
1066
1441
|
function scanWorkspace(dir, stats, rootNamespace, detectedFrameworks) {
|
|
1067
1442
|
const files = fs.readdirSync(dir);
|
|
1068
|
-
|
|
1069
1443
|
for (const file of files) {
|
|
1070
1444
|
const fullPath = path.join(dir, file);
|
|
1071
1445
|
const stat = fs.statSync(fullPath);
|
|
@@ -1076,27 +1450,51 @@ function scanWorkspace(dir, stats, rootNamespace, detectedFrameworks) {
|
|
|
1076
1450
|
}
|
|
1077
1451
|
} else {
|
|
1078
1452
|
const ext = path.extname(file);
|
|
1453
|
+
if (file === 'index.html' || REGEX_PATTERNS.configFile.test(file)) {
|
|
1454
|
+
stats.hasHtml = true;
|
|
1455
|
+
}
|
|
1456
|
+
if (REGEX_PATTERNS.testFile.test(file)) {
|
|
1457
|
+
stats.hasTests = true;
|
|
1458
|
+
}
|
|
1459
|
+
if (ext === '.ts' || ext === '.tsx') {
|
|
1460
|
+
stats.tsFiles++;
|
|
1461
|
+
}
|
|
1462
|
+
if (ext === '.js' || ext === '.jsx' || ext === '.mjs') {
|
|
1463
|
+
stats.jsFiles++;
|
|
1464
|
+
}
|
|
1079
1465
|
|
|
1080
|
-
if (
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
if (
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
if (REGEX_PATTERNS.
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
if (REGEX_PATTERNS.nuxtPage.test(fullPath))
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
if (REGEX_PATTERNS.
|
|
1093
|
-
|
|
1094
|
-
|
|
1466
|
+
if (REGEX_PATTERNS.nextjsPage.test(fullPath)) {
|
|
1467
|
+
stats.frameworkFiles.nextjs.pages.add(fullPath);
|
|
1468
|
+
}
|
|
1469
|
+
if (REGEX_PATTERNS.nextjsApi.test(fullPath)) {
|
|
1470
|
+
stats.frameworkFiles.nextjs.apiRoutes.add(fullPath);
|
|
1471
|
+
}
|
|
1472
|
+
if (REGEX_PATTERNS.nextjsComponent.test(fullPath)) {
|
|
1473
|
+
stats.frameworkFiles.nextjs.components.add(fullPath);
|
|
1474
|
+
}
|
|
1475
|
+
if (REGEX_PATTERNS.nuxtPage.test(fullPath)) {
|
|
1476
|
+
stats.frameworkFiles.nuxt.pages.add(fullPath);
|
|
1477
|
+
}
|
|
1478
|
+
if (REGEX_PATTERNS.nuxtComponent.test(fullPath)) {
|
|
1479
|
+
stats.frameworkFiles.nuxt.components.add(fullPath);
|
|
1480
|
+
}
|
|
1481
|
+
if (REGEX_PATTERNS.sveltekitPage.test(fullPath)) {
|
|
1482
|
+
stats.frameworkFiles.sveltekit.pages.add(fullPath);
|
|
1483
|
+
}
|
|
1484
|
+
if (REGEX_PATTERNS.sveltekitComponent.test(fullPath)) {
|
|
1485
|
+
stats.frameworkFiles.sveltekit.components.add(fullPath);
|
|
1486
|
+
}
|
|
1487
|
+
if (REGEX_PATTERNS.reactHook.test(fullPath)) {
|
|
1488
|
+
stats.frameworkFiles.react.hooks.add(fullPath);
|
|
1489
|
+
}
|
|
1490
|
+
if (REGEX_PATTERNS.vueComposable.test(fullPath)) {
|
|
1491
|
+
stats.frameworkFiles.vue.composables.add(fullPath);
|
|
1492
|
+
}
|
|
1095
1493
|
|
|
1096
1494
|
if (VALID_EXTENSIONS.has(ext)) {
|
|
1097
1495
|
stats.scannedFiles++;
|
|
1496
|
+
stats.scannedFilePaths.push(fullPath);
|
|
1098
1497
|
const rawContent = readFileSyncNormalized(fullPath);
|
|
1099
|
-
// Strip non-printable chars but keep Unicode letters (important for identifiers)
|
|
1100
1498
|
const content = rawContent.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
1101
1499
|
|
|
1102
1500
|
const codeLines = content.split(/\r?\n/);
|
|
@@ -1106,14 +1504,13 @@ function scanWorkspace(dir, stats, rootNamespace, detectedFrameworks) {
|
|
|
1106
1504
|
|
|
1107
1505
|
analyzeCodeStyle(content, stats);
|
|
1108
1506
|
|
|
1109
|
-
// Universal Cryptographic Leak Interception (Expanded)
|
|
1110
1507
|
for (const [patternName, patternRegex] of Object.entries(REGEX_PATTERNS)) {
|
|
1111
1508
|
if (patternName.startsWith("secretKeys") || patternName.endsWith("Keys") || patternName.endsWith("Tokens")) {
|
|
1112
1509
|
patternRegex.lastIndex = 0;
|
|
1113
1510
|
let match;
|
|
1114
1511
|
while ((match = patternRegex.exec(content)) !== null) {
|
|
1115
|
-
const keyName = match[1] || patternName;
|
|
1116
|
-
const secretValue = match[2] || match[0];
|
|
1512
|
+
const keyName = match[1] || patternName;
|
|
1513
|
+
const secretValue = match[2] || match[0];
|
|
1117
1514
|
const envVarName = `${rootNamespace.toUpperCase().replace(/[^A-Z0-9]/g, '_')}_${keyName.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
|
|
1118
1515
|
stats.discoveredSecrets.push({ filePath: fullPath, keyName, secretValue, envVarName, type: patternName });
|
|
1119
1516
|
stats.envVars.add(envVarName);
|
|
@@ -1141,23 +1538,15 @@ function scanWorkspace(dir, stats, rootNamespace, detectedFrameworks) {
|
|
|
1141
1538
|
stats.quality.largeImageImports.push({ filePath: fullPath, type: patternName, line, code: match[0] });
|
|
1142
1539
|
}
|
|
1143
1540
|
} else if (patternName.startsWith("unoptimizedLoop")) {
|
|
1144
|
-
patternRegex.lastIndex = 0;
|
|
1541
|
+
patternRegex.lastIndex = 0; // 👈 Add this line to reset the pointer for the next file pass
|
|
1145
1542
|
let match;
|
|
1146
1543
|
while ((match = patternRegex.exec(content)) !== null) {
|
|
1147
1544
|
const line = content.substring(0, match.index).split("\n").length;
|
|
1148
1545
|
stats.quality.unoptimizedLoops.push({ filePath: fullPath, type: patternName, line, code: match[0] });
|
|
1149
1546
|
}
|
|
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
1547
|
}
|
|
1158
1548
|
}
|
|
1159
1549
|
|
|
1160
|
-
// Global Regex Environmental Extraction Module
|
|
1161
1550
|
let fileHasEnv = false;
|
|
1162
1551
|
let envMatch;
|
|
1163
1552
|
REGEX_PATTERNS.env.lastIndex = 0;
|
|
@@ -1165,14 +1554,15 @@ function scanWorkspace(dir, stats, rootNamespace, detectedFrameworks) {
|
|
|
1165
1554
|
stats.envVars.add(envMatch[1]);
|
|
1166
1555
|
fileHasEnv = true;
|
|
1167
1556
|
}
|
|
1168
|
-
if (fileHasEnv)
|
|
1169
|
-
|
|
1170
|
-
|
|
1557
|
+
if (fileHasEnv) {
|
|
1558
|
+
stats.filesWithEnvVars.add(fullPath);
|
|
1559
|
+
}
|
|
1560
|
+
if (content.includes('import ') || content.includes('export ')) {
|
|
1561
|
+
stats.usesEsm = true;
|
|
1562
|
+
}
|
|
1171
1563
|
|
|
1172
|
-
// Perform framework-specific analysis
|
|
1173
1564
|
FrameworkAnalyzer.analyzeFile(fullPath, content, stats, detectedFrameworks);
|
|
1174
1565
|
|
|
1175
|
-
// --- AST Parsing (preferred) ---
|
|
1176
1566
|
let ast = null;
|
|
1177
1567
|
try {
|
|
1178
1568
|
ast = acorn.parse(content, { ecmaVersion: 'latest', sourceType: 'module', allowHashBang: true, locations: true });
|
|
@@ -1189,22 +1579,16 @@ function scanWorkspace(dir, stats, rootNamespace, detectedFrameworks) {
|
|
|
1189
1579
|
stats.exportedSymbols.set(fullPath, currentFileExportedSymbols);
|
|
1190
1580
|
}
|
|
1191
1581
|
} else {
|
|
1192
|
-
// Regex fallback for TypeScript generics / decorators / etc.
|
|
1193
1582
|
extractImportsFromText(codeLines, fileRawDeps, importedIdentifiers, importedLocations, stats, fullPath);
|
|
1194
1583
|
}
|
|
1195
1584
|
|
|
1196
|
-
|
|
1197
|
-
fileRawDeps.forEach(dep => stats.
|
|
1198
|
-
fileRawDeps.forEach(dep => stats.rawDeps.add(dep));
|
|
1585
|
+
fileRawDeps.forEach(dep => { return stats.allImportedPackages.add(dep); });
|
|
1586
|
+
fileRawDeps.forEach(dep => { return stats.rawDeps.add(dep); });
|
|
1199
1587
|
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
const t = l.trim();
|
|
1205
|
-
return !t.startsWith('import ') && !/\brequire\s*\(/.test(t);
|
|
1206
|
-
})
|
|
1207
|
-
.join('\n');
|
|
1588
|
+
const executionCode = codeLines.filter(l => {
|
|
1589
|
+
const t = l.trim();
|
|
1590
|
+
return !t.startsWith('import ') && !/\brequire\s*\(/.test(t);
|
|
1591
|
+
}).join('\n');
|
|
1208
1592
|
|
|
1209
1593
|
for (const [pkg, identifiers] of importedIdentifiers.entries()) {
|
|
1210
1594
|
const isUsed = analyzeIdentifierUsage(pkg, identifiers, executionCode);
|
|
@@ -1222,7 +1606,156 @@ function scanWorkspace(dir, stats, rootNamespace, detectedFrameworks) {
|
|
|
1222
1606
|
}
|
|
1223
1607
|
}
|
|
1224
1608
|
|
|
1609
|
+
// ============================================================
|
|
1610
|
+
// 🌳 KNIP-LEVEL REACHABILITY TRAVERSAL ENGINE
|
|
1611
|
+
// ============================================================
|
|
1612
|
+
class KnipEcosystemGraph {
|
|
1613
|
+
constructor(stats) {
|
|
1614
|
+
this.stats = stats;
|
|
1615
|
+
this.reachableFiles = new Set();
|
|
1616
|
+
this.usedSymbolsMap = new Map();
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* Traversiert den Modulgraphen ausgehend von den Entrypoints, um erreichbare Dateien und Tokens zu mappen.
|
|
1621
|
+
* @param {Array<string>} entrypoints Liste absoluter Dateipfade als Einstiegspunkte.
|
|
1622
|
+
*/
|
|
1623
|
+
traceReachability(entrypoints) {
|
|
1624
|
+
const queue = [...entrypoints];
|
|
1625
|
+
const visited = new Set();
|
|
1626
|
+
|
|
1627
|
+
while (queue.length > 0) {
|
|
1628
|
+
const currentFile = queue.shift();
|
|
1629
|
+
if (visited.has(currentFile)) {
|
|
1630
|
+
continue;
|
|
1631
|
+
}
|
|
1632
|
+
visited.add(currentFile);
|
|
1633
|
+
this.reachableFiles.add(currentFile);
|
|
1634
|
+
|
|
1635
|
+
const meta = this.stats.fileSymbolMetadata?.get(currentFile);
|
|
1636
|
+
if (!meta) {
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
meta.imports.forEach(imp => {
|
|
1641
|
+
const targetPath = resolveLocalModulePath(currentFile, imp.source);
|
|
1642
|
+
if (!targetPath) {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (!this.usedSymbolsMap.has(targetPath)) {
|
|
1647
|
+
this.usedSymbolsMap.set(targetPath, new Set());
|
|
1648
|
+
}
|
|
1649
|
+
const targetUses = this.usedSymbolsMap.get(targetPath);
|
|
1650
|
+
|
|
1651
|
+
imp.specifiers.forEach(spec => {
|
|
1652
|
+
if (spec.type === 'ImportNamespaceSpecifier') {
|
|
1653
|
+
const propsAccessed = meta.namespaceUses.get(spec.local);
|
|
1654
|
+
if (propsAccessed) {
|
|
1655
|
+
propsAccessed.forEach(p => { return targetUses.add(p); });
|
|
1656
|
+
} else {
|
|
1657
|
+
targetUses.add('*');
|
|
1658
|
+
}
|
|
1659
|
+
} else {
|
|
1660
|
+
targetUses.add(spec.imported);
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
if (!visited.has(targetPath)) {
|
|
1664
|
+
queue.push(targetPath);
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
meta.reExports.forEach(re => {
|
|
1669
|
+
const targetPath = resolveLocalModulePath(currentFile, re.source);
|
|
1670
|
+
if (!targetPath) {
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
if (!this.usedSymbolsMap.has(targetPath)) {
|
|
1675
|
+
this.usedSymbolsMap.set(targetPath, new Set());
|
|
1676
|
+
}
|
|
1677
|
+
const targetUses = this.usedSymbolsMap.get(targetPath);
|
|
1678
|
+
|
|
1679
|
+
re.specifiers.forEach(spec => {
|
|
1680
|
+
targetUses.add(spec.local || spec.exported);
|
|
1681
|
+
});
|
|
1682
|
+
if (!visited.has(targetPath)) {
|
|
1683
|
+
queue.push(targetPath);
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// ============================================================
|
|
1691
|
+
// 📊 POST-PROCESSING ANALYSIS PASS
|
|
1692
|
+
// ============================================================
|
|
1693
|
+
/**
|
|
1694
|
+
* Führt nach der Traversierung den Abgleich zwischen deklarierten und ungenutzten Exporten/Dateien durch.
|
|
1695
|
+
* @param {Object} stats Globales Analyseobjekt.
|
|
1696
|
+
* @param {Object} graphEngine Instanz der Graphen-Traversierung.
|
|
1697
|
+
*/
|
|
1698
|
+
function postProcessAnalysis(stats, graphEngine) {
|
|
1699
|
+
stats.unusedFiles = new Set();
|
|
1700
|
+
stats.unusedExportsPerFile = new Map();
|
|
1701
|
+
|
|
1702
|
+
stats.scannedFilePaths.forEach(filePath => {
|
|
1703
|
+
if (!graphEngine.reachableFiles.has(filePath)) {
|
|
1704
|
+
stats.unusedFiles.add(filePath);
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
for (const filePath of graphEngine.reachableFiles) {
|
|
1709
|
+
const meta = stats.fileSymbolMetadata?.get(filePath);
|
|
1710
|
+
if (!meta) {
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
const globalUses = graphEngine.usedSymbolsMap.get(filePath) || new Set();
|
|
1715
|
+
if (globalUses.has('*')) {
|
|
1716
|
+
continue;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
const unusedSet = new Set();
|
|
1720
|
+
for (const [exportName] of meta.exports.entries()) {
|
|
1721
|
+
if (!globalUses.has(exportName)) {
|
|
1722
|
+
unusedSet.add(exportName);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
if (unusedSet.size > 0) {
|
|
1726
|
+
stats.unusedExportsPerFile.set(filePath, unusedSet);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
if (stats.detectedFrameworks && stats.detectedFrameworks.includes('tailwind')) {
|
|
1731
|
+
const tailwindConfigPath = path.join(stats.targetDir, 'tailwind.config.js');
|
|
1732
|
+
if (fs.existsSync(tailwindConfigPath)) {
|
|
1733
|
+
try {
|
|
1734
|
+
const tailwindContent = fs.readFileSync(tailwindConfigPath, 'utf8');
|
|
1735
|
+
const contentArrayMatch = tailwindContent.match(/content:\s*\[([^\]]+)\]/s);
|
|
1736
|
+
if (contentArrayMatch && contentArrayMatch[1]) {
|
|
1737
|
+
const globPatterns = contentArrayMatch[1].split(',').map(s => { return s.trim().replace(/["']/g, ''); });
|
|
1738
|
+
if (globPatterns.length > 0) {
|
|
1739
|
+
console.log(` ℹ️ Tailwind Context Scan: Registered ${globPatterns.length} content matching globs vectors.`);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
} catch (tailwindFileReadException) {}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// ============================================================
|
|
1748
|
+
// INTERACTIVE ENGINE COMMAND LINE SYSTEM
|
|
1749
|
+
// ============================================================
|
|
1225
1750
|
async function main() {
|
|
1751
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
1752
|
+
console.log(`\n📦 pkg-scaffold v2.2.0: Advanced Dependency Intelligence Engine\n`);
|
|
1753
|
+
console.log(`Usage: npx pkg-scaffold [options]\n`);
|
|
1754
|
+
console.log(`Options:`);
|
|
1755
|
+
console.log(` -h, --help Show this comprehensive workspace helper panel`);
|
|
1756
|
+
process.exit(0);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1226
1759
|
if (process.env.INIT_CWD && !process.env.NPX_CLI_JS) {
|
|
1227
1760
|
console.log("\x1b[31m%s\x1b[0m", "🛑 Wait! Do not install this package locally.");
|
|
1228
1761
|
console.log("Please run it directly using: \x1b[36mnpx pkg-scaffold\x1b[0m\n");
|
|
@@ -1236,49 +1769,52 @@ async function main() {
|
|
|
1236
1769
|
let rlClosed = false;
|
|
1237
1770
|
rl.on('close', () => { rlClosed = true; });
|
|
1238
1771
|
const safeQuestion = async (prompt) => {
|
|
1239
|
-
if (rlClosed || !process.stdin.readable)
|
|
1240
|
-
|
|
1772
|
+
if (rlClosed || !process.stdin.readable) {
|
|
1773
|
+
return '';
|
|
1774
|
+
}
|
|
1775
|
+
try {
|
|
1776
|
+
return await rl.question(prompt);
|
|
1777
|
+
} catch (readlineQuestionPromptError) {
|
|
1778
|
+
return '';
|
|
1779
|
+
}
|
|
1241
1780
|
};
|
|
1242
1781
|
|
|
1243
1782
|
const stats = {
|
|
1244
|
-
tsFiles: 0,
|
|
1783
|
+
tsFiles: 0,
|
|
1784
|
+
jsFiles: 0,
|
|
1785
|
+
usesEsm: false,
|
|
1786
|
+
hasHtml: false,
|
|
1787
|
+
hasTests: false,
|
|
1245
1788
|
scannedFiles: 0,
|
|
1789
|
+
scannedFilePaths: [],
|
|
1246
1790
|
rawDeps: new Set(),
|
|
1247
1791
|
allImportedPackages: new Set(),
|
|
1248
1792
|
envVars: new Set(),
|
|
1249
1793
|
style: { semiCount: 0, noSemiCount: 0, tabCount: 0, space2Count: 0, space4Count: 0 },
|
|
1250
1794
|
quality: {
|
|
1251
|
-
varCount: 0,
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
insecurePatterns: [],
|
|
1255
|
-
complexRegexes: [],
|
|
1256
|
-
insecureCryptoUsage: [],
|
|
1257
|
-
sqlInjectionVulnerabilities: [],
|
|
1258
|
-
xssVulnerabilities: [],
|
|
1259
|
-
largeImageImports: [],
|
|
1260
|
-
unoptimizedLoops: [],
|
|
1261
|
-
frameworkSpecificIssues: []
|
|
1795
|
+
varCount: 0, hasEval: false, syncFsCount: 0, insecurePatterns: [], complexRegexes: [],
|
|
1796
|
+
insecureCryptoUsage: [], sqlInjectionVulnerabilities: [], xssVulnerabilities: [],
|
|
1797
|
+
largeImageImports: [], unoptimizedLoops: [], frameworkSpecificIssues: []
|
|
1262
1798
|
},
|
|
1263
1799
|
phantomInjections: new Map(),
|
|
1264
1800
|
discoveredSecrets: [],
|
|
1265
|
-
insecureCodePatterns: [],
|
|
1801
|
+
insecureCodePatterns: [],
|
|
1266
1802
|
subWorkspaces: [],
|
|
1267
1803
|
conflictingLockfiles: [],
|
|
1268
|
-
exportedSymbols: new Map(),
|
|
1269
|
-
usedExports: new Map(),
|
|
1270
|
-
unusedFiles: new Set(),
|
|
1271
|
-
unusedExportsPerFile: new Map(),
|
|
1272
|
-
localFileImports: new Map(),
|
|
1804
|
+
exportedSymbols: new Map(),
|
|
1805
|
+
usedExports: new Map(),
|
|
1806
|
+
unusedFiles: new Set(),
|
|
1807
|
+
unusedExportsPerFile: new Map(),
|
|
1808
|
+
localFileImports: new Map(),
|
|
1273
1809
|
unusedDepsInCode: new Set(),
|
|
1274
|
-
unusedImportsPerFile: new Map(),
|
|
1275
|
-
filesWithEnvVars: new Set(),
|
|
1276
|
-
injectDotenvEngine: false,
|
|
1810
|
+
unusedImportsPerFile: new Map(),
|
|
1811
|
+
filesWithEnvVars: new Set(),
|
|
1812
|
+
injectDotenvEngine: false,
|
|
1277
1813
|
bootstrapEslintSuite: false,
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1814
|
+
ghostDependencies: new Set(),
|
|
1815
|
+
orphanedDependencies: new Set(),
|
|
1816
|
+
deprecatedPackages: new Map(),
|
|
1817
|
+
fileSymbolMetadata: new Map(),
|
|
1282
1818
|
frameworkFiles: {
|
|
1283
1819
|
nextjs: { pages: new Set(), apiRoutes: new Set(), components: new Set(), dataFetching: new Map(), optimizations: [] },
|
|
1284
1820
|
nuxt: { pages: new Set(), components: new Set(), modules: new Set(), dataFetching: new Map(), optimizations: [] },
|
|
@@ -1286,25 +1822,21 @@ async function main() {
|
|
|
1286
1822
|
react: { hooks: new Set(), components: new Set(), optimizations: [] },
|
|
1287
1823
|
vue: { composables: new Set(), components: new Set(), optimizations: [] },
|
|
1288
1824
|
},
|
|
1289
|
-
frameworkOptimizations: [],
|
|
1825
|
+
frameworkOptimizations: [],
|
|
1826
|
+
packageJson: null,
|
|
1827
|
+
targetDir: targetDir,
|
|
1828
|
+
detectedFrameworks: []
|
|
1290
1829
|
};
|
|
1291
1830
|
|
|
1292
1831
|
const activePkgManager = detectPackageManager(targetDir, stats);
|
|
1293
1832
|
const pkgPath = path.join(targetDir, 'package.json');
|
|
1294
|
-
let preExistingLicense = null;
|
|
1295
|
-
let
|
|
1296
|
-
let preExistingDevDeps = [];
|
|
1297
|
-
let existingPackageJson = null;
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1833
|
+
let preExistingLicense = null, preExistingDeps = [], preExistingDevDeps = [], existingPackageJson = null;
|
|
1834
|
+
let detectedFrameworks = [];
|
|
1302
1835
|
|
|
1303
1836
|
console.log(`\n${'═'.repeat(67)}`);
|
|
1304
|
-
console.log(`🚀 pkg-scaffold v2.0:
|
|
1837
|
+
console.log(`🚀 pkg-scaffold v2.2.0: Enterprise Graph Intelligence Analyzer`);
|
|
1305
1838
|
console.log(`${'═'.repeat(67)}\n`);
|
|
1306
1839
|
|
|
1307
|
-
// --- Sub-workspace detection ---
|
|
1308
1840
|
const topLevelItems = fs.readdirSync(targetDir);
|
|
1309
1841
|
const potentialSubModules = [];
|
|
1310
1842
|
for (const item of topLevelItems) {
|
|
@@ -1317,95 +1849,121 @@ async function main() {
|
|
|
1317
1849
|
for (const entry of subEntries) {
|
|
1318
1850
|
const entryPath = path.join(d, entry);
|
|
1319
1851
|
if (fs.statSync(entryPath).isDirectory()) {
|
|
1320
|
-
if (!IGNORED_DIRS.has(entry) && !entry.startsWith('.'))
|
|
1852
|
+
if (!IGNORED_DIRS.has(entry) && !entry.startsWith('.')) {
|
|
1853
|
+
examineDirectory(entryPath);
|
|
1854
|
+
}
|
|
1321
1855
|
} else if (VALID_EXTENSIONS.has(path.extname(entry))) {
|
|
1322
1856
|
containsSourceCode = true;
|
|
1323
1857
|
}
|
|
1324
1858
|
}
|
|
1325
|
-
} catch {}
|
|
1859
|
+
} catch (subDirReadError) {}
|
|
1326
1860
|
};
|
|
1327
1861
|
examineDirectory(fullPath);
|
|
1328
|
-
if (containsSourceCode)
|
|
1862
|
+
if (containsSourceCode) {
|
|
1863
|
+
potentialSubModules.push(item);
|
|
1864
|
+
}
|
|
1329
1865
|
}
|
|
1330
1866
|
}
|
|
1331
|
-
if (potentialSubModules.length > 1)
|
|
1867
|
+
if (potentialSubModules.length > 1) {
|
|
1868
|
+
stats.subWorkspaces = potentialSubModules;
|
|
1869
|
+
}
|
|
1332
1870
|
|
|
1333
|
-
// --- Existing package.json analysis ---
|
|
1334
1871
|
if (fs.existsSync(pkgPath)) {
|
|
1335
1872
|
console.log(`⚠️ An existing package.json was found in this working directory.`);
|
|
1336
1873
|
console.log(`📡 Analyzing existing installation arrays for invalid metrics...`);
|
|
1337
1874
|
try {
|
|
1338
1875
|
existingPackageJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
1876
|
+
stats.packageJson = existingPackageJson;
|
|
1339
1877
|
if (existingPackageJson.license && typeof existingPackageJson.license === 'string' && existingPackageJson.license.toLowerCase() !== 'none') {
|
|
1340
1878
|
preExistingLicense = existingPackageJson.license;
|
|
1341
1879
|
}
|
|
1342
|
-
if (existingPackageJson.dependencies)
|
|
1343
|
-
|
|
1880
|
+
if (existingPackageJson.dependencies) {
|
|
1881
|
+
preExistingDeps = Object.keys(existingPackageJson.dependencies);
|
|
1882
|
+
}
|
|
1883
|
+
if (existingPackageJson.devDependencies) {
|
|
1884
|
+
preExistingDevDeps = Object.keys(existingPackageJson.devDependencies);
|
|
1885
|
+
}
|
|
1344
1886
|
|
|
1345
|
-
|
|
1346
|
-
const detectedFrameworks = FrameworkEngine.detect(targetDir, existingPackageJson);
|
|
1887
|
+
detectedFrameworks = FrameworkEngine.detect(targetDir, existingPackageJson);
|
|
1347
1888
|
stats.detectedFrameworks = detectedFrameworks;
|
|
1348
1889
|
|
|
1349
1890
|
const combinedDeps = [...preExistingDeps, ...preExistingDevDeps];
|
|
1350
|
-
let brokenEcosystem =
|
|
1891
|
+
let brokenEcosystem = false;
|
|
1351
1892
|
|
|
1352
|
-
// Check for non-existent AND deprecated packages
|
|
1353
1893
|
if (combinedDeps.length > 0) {
|
|
1354
|
-
console.log(`
|
|
1894
|
+
console.log(` 🔍 Validating ${combinedDeps.length} declared package(s) against npm registry...`);
|
|
1355
1895
|
for (const dep of combinedDeps) {
|
|
1356
1896
|
const check = await inspectNpmPackage(dep);
|
|
1897
|
+
|
|
1357
1898
|
if (check && check.error === 'NOT_FOUND') {
|
|
1358
|
-
|
|
1359
|
-
|
|
1899
|
+
if (dep.startsWith('@')) {
|
|
1900
|
+
console.log(` ℹ️ Scoped or private module bypassed registry assertion: "${dep}"`);
|
|
1901
|
+
} else {
|
|
1902
|
+
brokenEcosystem = true;
|
|
1903
|
+
console.log(` ❌ Non-existent package on registry: "${dep}"`);
|
|
1904
|
+
}
|
|
1360
1905
|
} else if (check && check.deprecated) {
|
|
1361
1906
|
stats.deprecatedPackages.set(dep, check.deprecated);
|
|
1362
|
-
console.log(`
|
|
1907
|
+
console.log(` ⚠️ Deprecated package detected: "${dep}" — ${check.deprecated}`);
|
|
1363
1908
|
}
|
|
1364
1909
|
}
|
|
1365
1910
|
}
|
|
1366
|
-
|
|
1367
1911
|
if (brokenEcosystem) {
|
|
1368
|
-
console.log(`\n🛑 CRITICAL COMPLIANCE BREAK: Your current package.json
|
|
1912
|
+
console.log(`\n🛑 CRITICAL COMPLIANCE BREAK: Your current package.json contains non-existent packages.`);
|
|
1369
1913
|
console.log(`👉 Action Required: Please remove or backup the existing 'package.json' from this folder.\n`);
|
|
1370
1914
|
rl.close();
|
|
1371
1915
|
return;
|
|
1372
1916
|
}
|
|
1373
|
-
} catch (
|
|
1917
|
+
} catch (packageJsonParseRuntimeError) {
|
|
1374
1918
|
console.log(`\n🛑 CRITICAL: Existing package.json is malformed or corrupt.\n`);
|
|
1375
1919
|
rl.close();
|
|
1376
1920
|
return;
|
|
1377
1921
|
}
|
|
1378
1922
|
}
|
|
1379
1923
|
|
|
1380
|
-
// --- Workspace scan ---
|
|
1381
1924
|
console.log(`\n🔬 Scanning workspace source files...`);
|
|
1382
1925
|
scanWorkspace(targetDir, stats, folderName, detectedFrameworks);
|
|
1383
|
-
console.log(`
|
|
1926
|
+
console.log(` Janitor: Found ${stats.scannedFiles} source module assets.`);
|
|
1927
|
+
|
|
1928
|
+
// --- TRIGGER RE-ARCHITECTED KNIP GRAPH INTELLIGENCE ---
|
|
1929
|
+
const graphEngine = new KnipEcosystemGraph(stats);
|
|
1930
|
+
const initialEntries = new Set();
|
|
1931
|
+
const baseEntryFallbacks = ['index.js', 'index.ts', 'src/index.js', 'src/index.ts', 'main.js', 'src/main.js', 'src/main.ts'];
|
|
1932
|
+
baseEntryFallbacks.forEach(f => {
|
|
1933
|
+
const absolutePath = path.join(targetDir, f);
|
|
1934
|
+
if (fs.existsSync(absolutePath)) {
|
|
1935
|
+
initialEntries.add(absolutePath);
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
|
|
1939
|
+
if (existingPackageJson) {
|
|
1940
|
+
if (existingPackageJson.main) {
|
|
1941
|
+
initialEntries.add(path.resolve(targetDir, existingPackageJson.main));
|
|
1942
|
+
}
|
|
1943
|
+
if (existingPackageJson.module) {
|
|
1944
|
+
initialEntries.add(path.resolve(targetDir, existingPackageJson.module));
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1384
1947
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1948
|
+
stats.frameworkFiles.nextjs.pages.forEach(file => { return initialEntries.add(file); });
|
|
1949
|
+
stats.frameworkFiles.nextjs.apiRoutes.forEach(file => { return initialEntries.add(file); });
|
|
1950
|
+
stats.frameworkFiles.nuxt.pages.forEach(file => { return initialEntries.add(file); });
|
|
1951
|
+
stats.frameworkFiles.sveltekit.pages.forEach(file => { return initialEntries.add(file); });
|
|
1952
|
+
|
|
1953
|
+
graphEngine.traceReachability(Array.from(initialEntries));
|
|
1954
|
+
postProcessAnalysis(stats, graphEngine);
|
|
1387
1955
|
|
|
1388
|
-
// --- Binary-to-package resolution ---
|
|
1389
1956
|
const binariesInScripts = existingPackageJson ? getBinariesFromPackageJson(existingPackageJson) : [];
|
|
1390
1957
|
const resolvedBinaryPackages = new Set();
|
|
1391
1958
|
for (const binary of binariesInScripts) {
|
|
1392
1959
|
const pkgName = BINARY_TO_PACKAGE_MAP[binary] || binary;
|
|
1393
1960
|
resolvedBinaryPackages.add(pkgName);
|
|
1394
1961
|
stats.rawDeps.add(pkgName);
|
|
1395
|
-
stats.allImportedPackages.add(pkgName);
|
|
1962
|
+
stats.allImportedPackages.add(pkgName);
|
|
1396
1963
|
}
|
|
1397
1964
|
|
|
1398
|
-
// ============================================================
|
|
1399
|
-
// GHOST DEPENDENCY ANALYSIS
|
|
1400
|
-
// Packages imported in code but missing from package.json
|
|
1401
|
-
// ============================================================
|
|
1402
1965
|
if (preExistingDeps.length > 0 || preExistingDevDeps.length > 0) {
|
|
1403
|
-
stats.ghostDependencies = detectGhostDependencies(
|
|
1404
|
-
stats.allImportedPackages,
|
|
1405
|
-
preExistingDeps,
|
|
1406
|
-
preExistingDevDeps
|
|
1407
|
-
);
|
|
1408
|
-
// Remove dev tooling from ghost list (they may be globally installed)
|
|
1966
|
+
stats.ghostDependencies = detectGhostDependencies(stats.allImportedPackages, preExistingDeps, preExistingDevDeps);
|
|
1409
1967
|
for (const dep of stats.ghostDependencies) {
|
|
1410
1968
|
if (DEV_TOOLING_ECOSYSTEM.has(dep) || dep.startsWith('@types/')) {
|
|
1411
1969
|
stats.ghostDependencies.delete(dep);
|
|
@@ -1413,78 +1971,54 @@ async function main() {
|
|
|
1413
1971
|
}
|
|
1414
1972
|
}
|
|
1415
1973
|
|
|
1416
|
-
// ============================================================
|
|
1417
|
-
// ORPHANED DEPENDENCY ANALYSIS
|
|
1418
|
-
// Packages in package.json that are never imported
|
|
1419
|
-
// ============================================================
|
|
1420
1974
|
if (preExistingDeps.length > 0) {
|
|
1421
|
-
stats.orphanedDependencies = detectOrphanedDependencies(
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
resolvedBinaryPackages,
|
|
1425
|
-
DEV_TOOLING_ECOSYSTEM
|
|
1426
|
-
);
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
// ============================================================
|
|
1430
|
-
// UNUSED IMPORTS ANALYSIS (cross-file aggregation)
|
|
1431
|
-
// A package is only truly "unused" if it's never used in ANY file
|
|
1432
|
-
// ============================================================
|
|
1433
|
-
// Build a set of packages that ARE used in at least one file
|
|
1434
|
-
const usedInAtLeastOneFile = new Set();
|
|
1435
|
-
for (const [, fileImports] of stats.unusedImportsPerFile.entries()) {
|
|
1436
|
-
// If a package appears in unusedImportsPerFile for this file,
|
|
1437
|
-
// it might still be used in another file — check allImportedPackages
|
|
1438
|
-
}
|
|
1439
|
-
// Refine: unusedDepsInCode should only include packages that are
|
|
1440
|
-
// imported but never referenced across the entire codebase
|
|
1441
|
-
const trulyUnusedImports = new Set();
|
|
1442
|
-
for (const pkg of stats.unusedDepsInCode) {
|
|
1443
|
-
// If the package is used (identifier found) in ANY file, remove from unused
|
|
1444
|
-
let foundUsedElsewhere = false;
|
|
1445
|
-
for (const [filePath, fileUnused] of stats.unusedImportsPerFile.entries()) {
|
|
1446
|
-
if (!fileUnused.has(pkg)) {
|
|
1447
|
-
// This file imports pkg and DOES use it
|
|
1448
|
-
if (stats.allImportedPackages.has(pkg)) {
|
|
1449
|
-
foundUsedElsewhere = true;
|
|
1450
|
-
break;
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
if (!foundUsedElsewhere) trulyUnusedImports.add(pkg);
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
// ============================================================
|
|
1458
|
-
// DISPLAY: GHOST DEPENDENCIES (critical — will break at runtime)
|
|
1459
|
-
// ============================================================
|
|
1975
|
+
stats.orphanedDependencies = detectOrphanedDependencies(preExistingDeps, stats.allImportedPackages, resolvedBinaryPackages, DEV_TOOLING_ECOSYSTEM);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1460
1978
|
if (stats.ghostDependencies.size > 0) {
|
|
1461
1979
|
console.log(`\n${'─'.repeat(67)}`);
|
|
1462
1980
|
console.log(`🚨 GHOST DEPENDENCIES DETECTED (CRITICAL — Runtime/Deploy will FAIL)`);
|
|
1463
1981
|
console.log(`${'─'.repeat(67)}`);
|
|
1464
|
-
console.log(`
|
|
1465
|
-
console.log(` They may work locally (if globally installed) but WILL FAIL in CI/CD.\n`);
|
|
1982
|
+
console.log(` Diese Pakete fehlen in deiner package.json, werden aber aktiv importiert:\n`);
|
|
1466
1983
|
for (const pkg of stats.ghostDependencies) {
|
|
1467
|
-
console.log(`
|
|
1984
|
+
console.log(` ❌ \x1b[31m"${pkg}"\x1b[0m — missing from package.json`);
|
|
1468
1985
|
}
|
|
1469
1986
|
console.log(`${'─'.repeat(67)}`);
|
|
1470
1987
|
const addGhosts = await safeQuestion(`❓ Add these missing packages to package.json automatically? (Y/n): `);
|
|
1471
1988
|
if (addGhosts.trim().toLowerCase() !== 'n' && addGhosts.trim().toLowerCase() !== 'no') {
|
|
1472
|
-
for (const pkg of stats.ghostDependencies)
|
|
1473
|
-
|
|
1989
|
+
for (const pkg of stats.ghostDependencies) {
|
|
1990
|
+
stats.rawDeps.add(pkg);
|
|
1991
|
+
}
|
|
1992
|
+
console.log(` ✅ Ghost dependencies queued for package.json registration.`);
|
|
1474
1993
|
}
|
|
1475
1994
|
}
|
|
1476
1995
|
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1996
|
+
if (stats.unusedFiles.size > 0) {
|
|
1997
|
+
console.log(`\n${'─'.repeat(67)}`);
|
|
1998
|
+
console.log(`🗑️ DEAD CODE FILES DETECTED (Unreachable from Entrypoints)`);
|
|
1999
|
+
console.log(`${'─'.repeat(67)}`);
|
|
2000
|
+
stats.unusedFiles.forEach(f => {
|
|
2001
|
+
console.log(` 💀 \x1b[31m"${path.relative(targetDir, f)}"\x1b[0m — file never mapped or imported.`);
|
|
2002
|
+
});
|
|
2003
|
+
console.log(`${'─'.repeat(67)}`);
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
if (stats.unusedExportsPerFile.size > 0) {
|
|
2007
|
+
console.log(`\n${'─'.repeat(67)}`);
|
|
2008
|
+
console.log(`📤 UNUSED EXPORTS DETECTED (Dead Public API Symbols)`);
|
|
2009
|
+
console.log(`${'─'.repeat(67)}`);
|
|
2010
|
+
for (const [file, symbols] of stats.unusedExportsPerFile.entries()) {
|
|
2011
|
+
console.log(` ⚡ \x1b[33m"${path.relative(targetDir, file)}"\x1b[0m -> Dead Token(s): [ ${Array.from(symbols).join(', ')} ]`);
|
|
2012
|
+
}
|
|
2013
|
+
console.log(`${'─'.repeat(67)}`);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
1480
2016
|
if (stats.orphanedDependencies.size > 0) {
|
|
1481
2017
|
console.log(`\n${'─'.repeat(67)}`);
|
|
1482
2018
|
console.log(`📦 ORPHANED DEPENDENCIES DETECTED (in package.json, never imported)`);
|
|
1483
2019
|
console.log(`${'─'.repeat(67)}`);
|
|
1484
|
-
console.log(` These packages are declared in package.json but never imported`);
|
|
1485
|
-
console.log(` anywhere in your source code. Safe to remove.\n`);
|
|
1486
2020
|
for (const pkg of stats.orphanedDependencies) {
|
|
1487
|
-
console.log(`
|
|
2021
|
+
console.log(` 🗑️ \x1b[33m"${pkg}"\x1b[0m — declared but never imported`);
|
|
1488
2022
|
}
|
|
1489
2023
|
console.log(`${'─'.repeat(67)}`);
|
|
1490
2024
|
const pruneOrphans = await safeQuestion(`❓ Remove these orphaned packages from package.json? (y/N): `);
|
|
@@ -1494,87 +2028,23 @@ async function main() {
|
|
|
1494
2028
|
delete existingPackageJson.dependencies?.[pkg];
|
|
1495
2029
|
}
|
|
1496
2030
|
fs.writeFileSync(pkgPath, JSON.stringify(existingPackageJson, null, 2));
|
|
1497
|
-
console.log(`
|
|
2031
|
+
console.log(` 🗑️ Orphaned dependencies removed from package.json.`);
|
|
1498
2032
|
}
|
|
1499
2033
|
}
|
|
1500
2034
|
}
|
|
1501
2035
|
|
|
1502
|
-
// ============================================================
|
|
1503
|
-
// DISPLAY: UNUSED IMPORTS (imported but never referenced in code)
|
|
1504
|
-
// ============================================================
|
|
1505
|
-
const allDiscoveredUnused = new Set([...trulyUnusedImports]);
|
|
1506
|
-
// Also add packages in package.json not found in code at all
|
|
1507
|
-
if (preExistingDeps.length > 0) {
|
|
1508
|
-
preExistingDeps.forEach(dep => {
|
|
1509
|
-
if (!stats.rawDeps.has(dep) && !DEV_TOOLING_ECOSYSTEM.has(dep) && !dep.startsWith('@types/')) {
|
|
1510
|
-
allDiscoveredUnused.add(dep);
|
|
1511
|
-
}
|
|
1512
|
-
});
|
|
1513
|
-
}
|
|
1514
|
-
// Remove dev tooling from unused list
|
|
1515
|
-
for (const dep of allDiscoveredUnused) {
|
|
1516
|
-
if (DEV_TOOLING_ECOSYSTEM.has(dep) || dep.startsWith('@types/')) {
|
|
1517
|
-
allDiscoveredUnused.delete(dep);
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
if (allDiscoveredUnused.size > 0) {
|
|
1522
|
-
console.log(`\n${'─'.repeat(67)}`);
|
|
1523
|
-
console.log(`⚠️ UNUSED IMPORTS DETECTED (imported but never referenced in code)`);
|
|
1524
|
-
console.log(`${'─'.repeat(67)}`);
|
|
1525
|
-
console.log(` These modules are imported but their identifiers are never used`);
|
|
1526
|
-
console.log(` in executable code paths.\n`);
|
|
1527
|
-
|
|
1528
|
-
for (const dep of allDiscoveredUnused) {
|
|
1529
|
-
// Show which files have this unused import
|
|
1530
|
-
const filesWithUnused = [];
|
|
1531
|
-
for (const [filePath, fileUnused] of stats.unusedImportsPerFile.entries()) {
|
|
1532
|
-
if (fileUnused.has(dep)) {
|
|
1533
|
-
const lines = fileUnused.get(dep);
|
|
1534
|
-
const lineStr = lines.length > 0 ? `:${lines[0]}` : '';
|
|
1535
|
-
filesWithUnused.push(`${path.relative(targetDir, filePath)}${lineStr}`);
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
if (filesWithUnused.length > 0) {
|
|
1539
|
-
console.log(` ⚡ \x1b[33m"${dep}"\x1b[0m`);
|
|
1540
|
-
filesWithUnused.forEach(f => console.log(` └─ ${f}`));
|
|
1541
|
-
} else {
|
|
1542
|
-
console.log(` ⚡ \x1b[33m"${dep}"\x1b[0m`);
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
console.log(`${'─'.repeat(67)}`);
|
|
1546
|
-
|
|
1547
|
-
const pruneChoice = await safeQuestion(`❓ Exclude these unused imports from your package.json setup? (y/N): `);
|
|
1548
|
-
if (pruneChoice.trim().toLowerCase() === 'y' || pruneChoice.trim().toLowerCase() === 'yes') {
|
|
1549
|
-
for (const deadDep of allDiscoveredUnused) stats.rawDeps.delete(deadDep);
|
|
1550
|
-
console.log(` 🗑️ Pruned unused imports from configuration blueprint.`);
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
// ============================================================
|
|
1555
|
-
// DISPLAY: DEPRECATED PACKAGES
|
|
1556
|
-
// ============================================================
|
|
1557
2036
|
if (stats.deprecatedPackages.size > 0) {
|
|
1558
2037
|
console.log(`\n${'─'.repeat(67)}`);
|
|
1559
2038
|
console.log(`⚠️ DEPRECATED PACKAGES DETECTED`);
|
|
1560
2039
|
console.log(`${'─'.repeat(67)}`);
|
|
1561
2040
|
for (const [pkg, msg] of stats.deprecatedPackages.entries()) {
|
|
1562
|
-
|
|
2041
|
+
// FIX: Behebt das fehlerhafte Ersetzungstoken "Badge" durch das vorgesehene Warnungs-Emoji Layout
|
|
2042
|
+
console.log(` 📛 \x1b[33m"${pkg}"\x1b[0m — ${msg}`);
|
|
1563
2043
|
}
|
|
1564
2044
|
console.log(`${'─'.repeat(67)}`);
|
|
1565
2045
|
}
|
|
1566
2046
|
|
|
1567
|
-
// ============================================================
|
|
1568
|
-
// PHANTOM INJECTION DETECTION
|
|
1569
|
-
// Packages used in code (by identifier) but never imported
|
|
1570
|
-
// ============================================================
|
|
1571
|
-
// Build phantom detection from ALL declared packages
|
|
1572
2047
|
const allDeclaredForPhantom = new Set([...preExistingDeps, ...preExistingDevDeps]);
|
|
1573
|
-
for (const [filePath] of stats.unusedImportsPerFile.entries()) {
|
|
1574
|
-
// Already handled above
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
// Scan for identifiers used without import (using declared package names as hints)
|
|
1578
2048
|
const phantomScanContent = new Map();
|
|
1579
2049
|
function collectExecutionContent(dir) {
|
|
1580
2050
|
try {
|
|
@@ -1586,27 +2056,26 @@ async function main() {
|
|
|
1586
2056
|
} else if (VALID_EXTENSIONS.has(path.extname(file))) {
|
|
1587
2057
|
try {
|
|
1588
2058
|
const content = readFileSyncNormalized(fullPath);
|
|
1589
|
-
const execCode = content.split(/\r?\n/)
|
|
1590
|
-
.
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
})
|
|
1594
|
-
.join('\n');
|
|
2059
|
+
const execCode = content.split(/\r?\n/).filter(l => {
|
|
2060
|
+
const t = l.trim();
|
|
2061
|
+
return !t.startsWith('import ') && !/\brequire\s*\(/.test(t);
|
|
2062
|
+
}).join('\n');
|
|
1595
2063
|
phantomScanContent.set(fullPath, execCode);
|
|
1596
|
-
} catch {}
|
|
2064
|
+
} catch (readExecContentError) {}
|
|
1597
2065
|
}
|
|
1598
2066
|
}
|
|
1599
|
-
} catch {}
|
|
2067
|
+
} catch (fsCollectExecError) {}
|
|
1600
2068
|
}
|
|
1601
2069
|
collectExecutionContent(targetDir);
|
|
1602
2070
|
|
|
1603
2071
|
for (const [filePath, execCode] of phantomScanContent.entries()) {
|
|
1604
2072
|
for (const token of allDeclaredForPhantom) {
|
|
1605
2073
|
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1606
|
-
|
|
1607
|
-
if (tokenPattern.test(execCode) && !stats.allImportedPackages.has(token)) {
|
|
2074
|
+
if (new RegExp(`\\b${escaped}\\b`).test(execCode) && !stats.allImportedPackages.has(token)) {
|
|
1608
2075
|
stats.rawDeps.add(token);
|
|
1609
|
-
if (!stats.phantomInjections.has(filePath))
|
|
2076
|
+
if (!stats.phantomInjections.has(filePath)) {
|
|
2077
|
+
stats.phantomInjections.set(filePath, new Set());
|
|
2078
|
+
}
|
|
1610
2079
|
stats.phantomInjections.get(filePath).add(token);
|
|
1611
2080
|
}
|
|
1612
2081
|
}
|
|
@@ -1615,7 +2084,6 @@ async function main() {
|
|
|
1615
2084
|
const isTypeScript = stats.tsFiles > stats.jsFiles;
|
|
1616
2085
|
const isFrontendWeb = stats.hasHtml || stats.rawDeps.has('react') || stats.rawDeps.has('vue') || stats.rawDeps.has('vite') || stats.rawDeps.has('svelte') || stats.rawDeps.has('next') || stats.rawDeps.has('nuxt');
|
|
1617
2086
|
|
|
1618
|
-
// --- dotenv suggestion ---
|
|
1619
2087
|
if (stats.envVars.size > 0 && !stats.rawDeps.has('dotenv') && !isFrontendWeb) {
|
|
1620
2088
|
console.log(`\n📡 CONFIGURATION COMPLIANCE GAP: UNMANAGED ENVIRONMENT VARIABLES`);
|
|
1621
2089
|
console.log(`${'─'.repeat(67)}`);
|
|
@@ -1628,7 +2096,6 @@ async function main() {
|
|
|
1628
2096
|
}
|
|
1629
2097
|
}
|
|
1630
2098
|
|
|
1631
|
-
// --- Build package.json ---
|
|
1632
2099
|
const packageJson = {
|
|
1633
2100
|
name: folderName.toLowerCase().replace(/[^a-z0-9-_]/g, '-'),
|
|
1634
2101
|
version: '1.0.0',
|
|
@@ -1637,11 +2104,10 @@ async function main() {
|
|
|
1637
2104
|
author: gitInfo.author || undefined,
|
|
1638
2105
|
repository: gitInfo.repository ? { type: "git", url: `git+${gitInfo.repository}.git` } : undefined,
|
|
1639
2106
|
scripts: { test: stats.hasTests ? (isFrontendWeb ? 'vitest' : 'jest') : 'echo "No workspace test vectors specified" && exit 0' },
|
|
1640
|
-
dependencies: {},
|
|
2107
|
+
dependencies: {},
|
|
1641
2108
|
devDependencies: {}
|
|
1642
2109
|
};
|
|
1643
2110
|
|
|
1644
|
-
// --- ESLint suggestion ---
|
|
1645
2111
|
const eslintConfigFile = path.join(targetDir, 'eslint.config.js');
|
|
1646
2112
|
const linterPresent = fs.existsSync(eslintConfigFile) || fs.existsSync(path.join(targetDir, '.eslintrc.json')) || fs.existsSync(path.join(targetDir, '.eslintrc.js'));
|
|
1647
2113
|
|
|
@@ -1654,8 +2120,11 @@ async function main() {
|
|
|
1654
2120
|
if (choiceLintSetup.trim().toLowerCase() !== 'n' && choiceLintSetup.trim().toLowerCase() !== 'no') {
|
|
1655
2121
|
stats.bootstrapEslintSuite = true;
|
|
1656
2122
|
stats.rawDeps.add('eslint');
|
|
1657
|
-
if (isTypeScript)
|
|
1658
|
-
|
|
2123
|
+
if (isTypeScript) {
|
|
2124
|
+
stats.rawDeps.add('typescript-eslint');
|
|
2125
|
+
} else {
|
|
2126
|
+
stats.rawDeps.add('@eslint/js');
|
|
2127
|
+
}
|
|
1659
2128
|
}
|
|
1660
2129
|
}
|
|
1661
2130
|
|
|
@@ -1664,7 +2133,9 @@ async function main() {
|
|
|
1664
2133
|
packageJson.scripts.build = 'vite build';
|
|
1665
2134
|
packageJson.scripts.preview = 'vite preview';
|
|
1666
2135
|
stats.rawDeps.add('vite');
|
|
1667
|
-
if (stats.hasTests)
|
|
2136
|
+
if (stats.hasTests) {
|
|
2137
|
+
stats.rawDeps.add('vitest');
|
|
2138
|
+
}
|
|
1668
2139
|
} else {
|
|
1669
2140
|
if (isTypeScript) {
|
|
1670
2141
|
packageJson.scripts.build = 'tsc';
|
|
@@ -1677,10 +2148,11 @@ async function main() {
|
|
|
1677
2148
|
|
|
1678
2149
|
if (isTypeScript) {
|
|
1679
2150
|
packageJson.devDependencies.typescript = '^5.4.0';
|
|
1680
|
-
if (!isFrontendWeb)
|
|
2151
|
+
if (!isFrontendWeb) {
|
|
2152
|
+
packageJson.devDependencies['@types/node'] = '^20.11.0';
|
|
2153
|
+
}
|
|
1681
2154
|
}
|
|
1682
2155
|
|
|
1683
|
-
// --- Resolve package versions from npm ---
|
|
1684
2156
|
if (stats.rawDeps.size > 0) {
|
|
1685
2157
|
console.log(`\n📡 Resolving baseline package registry definitions...`);
|
|
1686
2158
|
for (const pkg of stats.rawDeps) {
|
|
@@ -1689,47 +2161,44 @@ async function main() {
|
|
|
1689
2161
|
const check = await inspectNpmPackage(cleaned);
|
|
1690
2162
|
if (check && check.error !== 'NOT_FOUND') {
|
|
1691
2163
|
const version = check.version || 'latest';
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
'mocha', 'ava', 'tap', 'jasmine', 'storybook', 'turbo', 'nx',
|
|
1699
|
-
'biome', '@biomejs/biome', 'oxlint', 'xo', 'standard',
|
|
1700
|
-
].includes(cleaned) || cleaned.startsWith('@types/');
|
|
1701
|
-
|
|
1702
|
-
if (isDevDep) packageJson.devDependencies[cleaned] = `^${version}`;
|
|
1703
|
-
else packageJson.dependencies[cleaned] = `^${version}`;
|
|
2164
|
+
const isDevDep = ['vite', 'vitest', 'typescript', 'eslint', 'typescript-eslint', '@eslint/js', 'prettier', 'jest', 'nodemon', 'ts-node', 'tsup', 'esbuild', '@swc/cli', 'tsx', 'rimraf', 'copyfiles', 'mkdirp', 'husky', 'lint-staged', '@commitlint/cli', 'typedoc', 'c8', 'nyc', 'mocha', 'ava', 'tap', 'jasmine', 'storybook', 'turbo', 'nx', 'biome', '@biomejs/biome', 'oxlint', 'xo', 'standard'].includes(cleaned) || cleaned.startsWith('@types/');
|
|
2165
|
+
if (isDevDep) {
|
|
2166
|
+
packageJson.devDependencies[cleaned] = `^${version}`;
|
|
2167
|
+
} else {
|
|
2168
|
+
packageJson.dependencies[cleaned] = `^${version}`;
|
|
2169
|
+
}
|
|
1704
2170
|
console.log(` ✔ Synced: ${cleaned}@^${version}${check.deprecated ? ' \x1b[33m[DEPRECATED]\x1b[0m' : ''}`);
|
|
1705
2171
|
}
|
|
1706
2172
|
}
|
|
1707
2173
|
}
|
|
1708
2174
|
}
|
|
1709
2175
|
|
|
1710
|
-
// --- Phantom injection report ---
|
|
1711
2176
|
if (stats.phantomInjections.size > 0) {
|
|
1712
2177
|
console.log(`\n${'─'.repeat(67)}`);
|
|
1713
2178
|
console.log(`👻 PHANTOM STRUCTURE ALERT: UNIMPORTED EXECUTIONS DETECTED`);
|
|
1714
2179
|
console.log(`${'─'.repeat(67)}`);
|
|
1715
2180
|
for (const [filePath, missingModules] of stats.phantomInjections.entries()) {
|
|
1716
2181
|
console.log(`📂 File: ${path.relative(targetDir, filePath)}`);
|
|
1717
|
-
console.log(` ❌ Used but never imported: ${Array.from(missingModules).map(m => `"${m}"
|
|
2182
|
+
console.log(` ❌ Used but never imported: ${Array.from(missingModules).map(m => { return `"${m}"`; }).join(', ')}`);
|
|
1718
2183
|
}
|
|
1719
2184
|
console.log(`${'─'.repeat(67)}`);
|
|
1720
2185
|
}
|
|
1721
2186
|
|
|
1722
|
-
// --- Code quality warnings ---
|
|
1723
2187
|
if (stats.quality.varCount > 0 || stats.quality.hasEval || stats.quality.syncFsCount > 0) {
|
|
1724
2188
|
console.log(`\n⚠️ CODE ARCHITECTURE & MODERNIZATION COMPLIANCE WARNINGS:`);
|
|
1725
2189
|
console.log(`${'─'.repeat(67)}`);
|
|
1726
|
-
if (stats.quality.varCount > 0)
|
|
1727
|
-
|
|
1728
|
-
|
|
2190
|
+
if (stats.quality.varCount > 0) {
|
|
2191
|
+
console.log(` ⚡ Found ${stats.quality.varCount} instances of legacy 'var'. Transition to 'let' / 'const'.`);
|
|
2192
|
+
}
|
|
2193
|
+
if (stats.quality.hasEval) {
|
|
2194
|
+
console.log(` 🔥 DANGER: 'eval()' detected! Refactor to mitigate remote code execution vectors.`);
|
|
2195
|
+
}
|
|
2196
|
+
if (stats.quality.syncFsCount > 0) {
|
|
2197
|
+
console.log(` 📉 Performance: Found ${stats.quality.syncFsCount} synchronous fs calls. Transition to 'fs/promises'.`);
|
|
2198
|
+
}
|
|
1729
2199
|
console.log(`${'─'.repeat(67)}`);
|
|
1730
2200
|
}
|
|
1731
2201
|
|
|
1732
|
-
// --- Security: hardcoded secrets ---
|
|
1733
2202
|
if (stats.discoveredSecrets.length > 0) {
|
|
1734
2203
|
console.log(`\n🚨 CRITICAL SECURITY COMPLIANCE ALERT: HARDCODED CREDENTIALS DETECTED`);
|
|
1735
2204
|
console.log(`${'─'.repeat(67)}`);
|
|
@@ -1747,25 +2216,25 @@ async function main() {
|
|
|
1747
2216
|
for (const secretMeta of stats.discoveredSecrets) {
|
|
1748
2217
|
let currentCodeContent = readFileSyncNormalized(secretMeta.filePath);
|
|
1749
2218
|
const envAccessor = isFrontendWeb ? `import.meta.env.${secretMeta.envVarName}` : `process.env.${secretMeta.envVarName}`;
|
|
1750
|
-
const exactLiteralPattern = new RegExp(`\\b${secretMeta.keyName}\\s*=\\s*['"
|
|
2219
|
+
const exactLiteralPattern = new RegExp(`\\b${secretMeta.keyName}\\s*=\\s*['"\\ ]${secretMeta.secretValue.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')}['"\\ ]`, 'g');
|
|
1751
2220
|
currentCodeContent = currentCodeContent.replace(exactLiteralPattern, `${secretMeta.keyName} = ${envAccessor}`);
|
|
1752
2221
|
fs.writeFileSync(secretMeta.filePath, currentCodeContent);
|
|
1753
|
-
if (!envBuffer.includes(`${secretMeta.envVarName}=`))
|
|
2222
|
+
if (!envBuffer.includes(`${secretMeta.envVarName}=`)) {
|
|
2223
|
+
envBuffer += `${secretMeta.envVarName}=${secretMeta.secretValue}\n`;
|
|
2224
|
+
}
|
|
1754
2225
|
console.log(` 🔒 Isolated: ${secretMeta.keyName} → ${envAccessor}`);
|
|
1755
2226
|
}
|
|
1756
2227
|
fs.writeFileSync(envPath, envBuffer);
|
|
1757
2228
|
}
|
|
1758
2229
|
}
|
|
1759
2230
|
|
|
1760
|
-
// --- Monorepo detection ---
|
|
1761
2231
|
if (stats.subWorkspaces && stats.subWorkspaces.length > 1) {
|
|
1762
2232
|
console.log(`\n📂 MULTI-WORKSPACE SEGMENTATION DETECTED`);
|
|
1763
|
-
console.log(` Identified sub-module paths: ${stats.subWorkspaces.map(w => `/${w}
|
|
2233
|
+
console.log(` Identified sub-module paths: ${stats.subWorkspaces.map(w => { return `/${w}`; }).join(', ')}`);
|
|
1764
2234
|
const setupWorkspace = await safeQuestion(`❓ Setup as a multi-package Monorepo Workspace layout? (y/N): `);
|
|
1765
2235
|
if (setupWorkspace.trim().toLowerCase() === 'y' || setupWorkspace.trim().toLowerCase() === 'yes') {
|
|
1766
2236
|
if (activePkgManager === 'pnpm') {
|
|
1767
|
-
|
|
1768
|
-
fs.writeFileSync(workspaceYamlPath, `packages:\n${stats.subWorkspaces.map(w => ` - '${w}'`).join('\n')}\n`);
|
|
2237
|
+
fs.writeFileSync(path.join(targetDir, 'pnpm-workspace.yaml'), `packages:\n${stats.subWorkspaces.map(w => { return ` - '${w}'`; }).join('\n')}\n`);
|
|
1769
2238
|
console.log(` 🏗️ Generated: pnpm-workspace.yaml`);
|
|
1770
2239
|
} else {
|
|
1771
2240
|
packageJson.workspaces = stats.subWorkspaces;
|
|
@@ -1774,7 +2243,6 @@ async function main() {
|
|
|
1774
2243
|
}
|
|
1775
2244
|
}
|
|
1776
2245
|
|
|
1777
|
-
// --- License ---
|
|
1778
2246
|
const licensePath = path.join(targetDir, 'LICENSE');
|
|
1779
2247
|
let chosenLicenseType = preExistingLicense || 'None';
|
|
1780
2248
|
|
|
@@ -1786,14 +2254,11 @@ async function main() {
|
|
|
1786
2254
|
console.log(` 📡 Querying GitHub Legal Databases for "${cleanedInput.toUpperCase()}"...`);
|
|
1787
2255
|
const rawTemplate = await fetchRemoteLicense(cleanedInput);
|
|
1788
2256
|
if (rawTemplate) {
|
|
1789
|
-
const parsedText = rawTemplate
|
|
1790
|
-
.replace(/\[year\]|<year>/gi, new Date().getFullYear().toString())
|
|
1791
|
-
.replace(/\[fullname\]|\[name of copyright owner\]|<copyright holders>|<name of author>/gi, gitInfo.name);
|
|
2257
|
+
const parsedText = rawTemplate.replace(/\[year\]|<year>/gi, new Date().getFullYear().toString()).replace(/\[fullname\]|\[name of copyright owner\]|<copyright holders>|<name of author>/gi, gitInfo.name);
|
|
1792
2258
|
fs.writeFileSync(licensePath, parsedText);
|
|
1793
2259
|
chosenLicenseType = cleanedInput.toUpperCase();
|
|
1794
2260
|
console.log(` ⚖️ Provisioned: LICENSE`);
|
|
1795
2261
|
} else {
|
|
1796
|
-
console.log(` ⚠️ License "${cleanedInput}" not found. Saving custom label.`);
|
|
1797
2262
|
chosenLicenseType = cleanedInput;
|
|
1798
2263
|
}
|
|
1799
2264
|
packageJson.license = chosenLicenseType;
|
|
@@ -1804,24 +2269,25 @@ async function main() {
|
|
|
1804
2269
|
if (!fs.existsSync(licensePath) && ['mit', 'apache-2.0', 'gpl-3.0'].includes(preExistingLicense.toLowerCase())) {
|
|
1805
2270
|
const rawTemplate = await fetchRemoteLicense(preExistingLicense);
|
|
1806
2271
|
if (rawTemplate) {
|
|
1807
|
-
const parsedText = rawTemplate
|
|
1808
|
-
.replace(/\[year\]|<year>/gi, new Date().getFullYear().toString())
|
|
1809
|
-
.replace(/\[fullname\]|\[name of copyright owner\]|<copyright holders>|<name of author>/gi, gitInfo.name);
|
|
2272
|
+
const parsedText = rawTemplate.replace(/\[year\]|<year>/gi, new Date().getFullYear().toString()).replace(/\[fullname\]|\[name of copyright owner\]|<copyright holders>|<name of author>/gi, gitInfo.name);
|
|
1810
2273
|
fs.writeFileSync(licensePath, parsedText);
|
|
1811
2274
|
}
|
|
1812
2275
|
}
|
|
1813
2276
|
} else if (fs.existsSync(licensePath)) {
|
|
1814
2277
|
try {
|
|
1815
2278
|
const currentLicenseContent = fs.readFileSync(licensePath, 'utf8');
|
|
1816
|
-
if (currentLicenseContent.includes('MIT'))
|
|
1817
|
-
|
|
1818
|
-
else
|
|
1819
|
-
|
|
2279
|
+
if (currentLicenseContent.includes('MIT')) {
|
|
2280
|
+
chosenLicenseType = 'MIT';
|
|
2281
|
+
} else if (currentLicenseContent.includes('Apache')) {
|
|
2282
|
+
chosenLicenseType = 'Apache-2.0';
|
|
2283
|
+
} else {
|
|
2284
|
+
chosenLicenseType = 'Custom';
|
|
2285
|
+
}
|
|
2286
|
+
} catch (licenseFileReadSyncError) {}
|
|
1820
2287
|
}
|
|
1821
2288
|
packageJson.license = chosenLicenseType;
|
|
1822
2289
|
}
|
|
1823
2290
|
|
|
1824
|
-
// --- Test scaffolding ---
|
|
1825
2291
|
if (!stats.hasTests) {
|
|
1826
2292
|
const bootstrapTest = await safeQuestion(`\n❓ No test files detected. Scaffold a zero-bloat testing harness via Node native test runner? (y/N): `);
|
|
1827
2293
|
if (bootstrapTest.trim().toLowerCase() === 'y' || bootstrapTest.trim().toLowerCase() === 'yes') {
|
|
@@ -1834,13 +2300,12 @@ async function main() {
|
|
|
1834
2300
|
fs.writeFileSync(testFilePath, testTemplate);
|
|
1835
2301
|
packageJson.scripts.test = 'node --test';
|
|
1836
2302
|
stats.hasTests = true;
|
|
1837
|
-
console.log(`
|
|
2303
|
+
console.log(` 🧪 Generated: index${testExt}`);
|
|
1838
2304
|
}
|
|
1839
2305
|
}
|
|
1840
2306
|
|
|
1841
2307
|
console.log(`\n⚙️ Writing ecosystem configuration artifacts...`);
|
|
1842
2308
|
|
|
1843
|
-
// --- ESLint config ---
|
|
1844
2309
|
if (stats.bootstrapEslintSuite) {
|
|
1845
2310
|
packageJson.scripts.lint = 'eslint .';
|
|
1846
2311
|
let eslintConfigContent = '';
|
|
@@ -1854,10 +2319,9 @@ async function main() {
|
|
|
1854
2319
|
}
|
|
1855
2320
|
}
|
|
1856
2321
|
fs.writeFileSync(eslintConfigFile, eslintConfigContent);
|
|
1857
|
-
console.log(`
|
|
2322
|
+
console.log(` 🎨 Provisioned: eslint.config.js`);
|
|
1858
2323
|
}
|
|
1859
2324
|
|
|
1860
|
-
// --- Write / merge package.json ---
|
|
1861
2325
|
if (fs.existsSync(pkgPath)) {
|
|
1862
2326
|
try {
|
|
1863
2327
|
const currentPackageJson = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
@@ -1868,40 +2332,36 @@ async function main() {
|
|
|
1868
2332
|
currentPackageJson.scripts.lint = packageJson.scripts.lint;
|
|
1869
2333
|
}
|
|
1870
2334
|
fs.writeFileSync(pkgPath, JSON.stringify(currentPackageJson, null, 2));
|
|
1871
|
-
console.log(`
|
|
1872
|
-
} catch (
|
|
2335
|
+
console.log(` 🔄 Safely merged discovered dependencies into existing package.json`);
|
|
2336
|
+
} catch (mergePackageJsonError) {}
|
|
1873
2337
|
} else {
|
|
1874
2338
|
fs.writeFileSync(pkgPath, JSON.stringify(packageJson, null, 2));
|
|
1875
|
-
console.log(`
|
|
2339
|
+
console.log(` 📝 Generated: package.json`);
|
|
1876
2340
|
}
|
|
1877
2341
|
|
|
1878
|
-
// --- Prettier config ---
|
|
1879
2342
|
const prettierPath = path.join(targetDir, '.prettierrc');
|
|
1880
2343
|
if (!fs.existsSync(prettierPath)) {
|
|
1881
2344
|
const useTabs = stats.style.tabCount > (stats.style.space2Count + stats.style.space4Count);
|
|
1882
2345
|
const useSemi = stats.style.semiCount >= stats.style.noSemiCount;
|
|
1883
2346
|
const tabWidth = stats.style.space4Count > stats.style.space2Count ? 4 : 2;
|
|
1884
2347
|
fs.writeFileSync(prettierPath, JSON.stringify({ semi: useSemi, useTabs, tabWidth, singleQuote: true, trailingComma: "es5" }, null, 2));
|
|
1885
|
-
console.log(`
|
|
2348
|
+
console.log(` 🎨 Code formatting mirror locked: .prettierrc`);
|
|
1886
2349
|
}
|
|
1887
2350
|
|
|
1888
|
-
// --- .env.example ---
|
|
1889
2351
|
if (stats.envVars.size > 0) {
|
|
1890
2352
|
const envExamplePath = path.join(targetDir, '.env.example');
|
|
1891
2353
|
if (!fs.existsSync(envExamplePath)) {
|
|
1892
|
-
fs.writeFileSync(envExamplePath, Array.from(stats.envVars).map(v => `${v}
|
|
1893
|
-
console.log(`
|
|
2354
|
+
fs.writeFileSync(envExamplePath, Array.from(stats.envVars).map(v => { return `${v}=`; }).join('\n') + '\n');
|
|
2355
|
+
console.log(` 🔒 Extracted environmental configurations: .env.example`);
|
|
1894
2356
|
}
|
|
1895
2357
|
}
|
|
1896
2358
|
|
|
1897
|
-
// --- .gitignore ---
|
|
1898
2359
|
const gitignorePath = path.join(targetDir, '.gitignore');
|
|
1899
2360
|
if (!fs.existsSync(gitignorePath)) {
|
|
1900
2361
|
fs.writeFileSync(gitignorePath, `node_modules/\ndist/\nbuild/\n.env\n.env.local\n.DS_Store\n*.log\n`);
|
|
1901
|
-
console.log(`
|
|
2362
|
+
console.log(` ⚙️ Generated: .gitignore`);
|
|
1902
2363
|
}
|
|
1903
2364
|
|
|
1904
|
-
// --- tsconfig.json ---
|
|
1905
2365
|
if (isTypeScript) {
|
|
1906
2366
|
const tsconfigPath = path.join(targetDir, 'tsconfig.json');
|
|
1907
2367
|
if (!fs.existsSync(tsconfigPath)) {
|
|
@@ -1909,61 +2369,29 @@ async function main() {
|
|
|
1909
2369
|
compilerOptions: { target: "ES2022", module: "NodeNext", moduleResolution: "NodeNext", esModuleInterop: true, strict: true, skipLibCheck: true, outDir: "./dist" },
|
|
1910
2370
|
include: ["src/**/*", "**/*.ts"]
|
|
1911
2371
|
}, null, 2));
|
|
1912
|
-
console.log(`
|
|
2372
|
+
console.log(` ⚙️ Generated: tsconfig.json`);
|
|
1913
2373
|
}
|
|
1914
2374
|
}
|
|
1915
2375
|
|
|
1916
|
-
// --- README ---
|
|
1917
2376
|
const readmePath = path.join(targetDir, 'README.md');
|
|
1918
2377
|
if (!fs.existsSync(readmePath)) {
|
|
1919
2378
|
const pName = packageJson.name;
|
|
1920
2379
|
const layoutTree = buildAsciiTree(targetDir).join('\n');
|
|
1921
|
-
const displayDeps = Object.keys(packageJson.dependencies).map(d => `* \`${d}
|
|
1922
|
-
const displayDevDeps = Object.keys(packageJson.devDependencies).map(d => `* \`${d}
|
|
2380
|
+
const displayDeps = Object.keys(packageJson.dependencies).map(d => { return `* \`${d}\``; }).join('\n') || '* None extracted';
|
|
2381
|
+
const displayDevDeps = Object.keys(packageJson.devDependencies).map(d => { return `* \`${d}\``; }).join('\n') || '* None extracted';
|
|
1923
2382
|
const licenseBadgeParam = encodeURIComponent(chosenLicenseType.replace(/-/g, '_'));
|
|
1924
2383
|
|
|
1925
|
-
const documentationTemplate =
|
|
1926
|
-
`# ${pName}
|
|
1927
|
-
|
|
1928
|
-

|
|
1929
|
-

|
|
1930
|
-

|
|
1931
|
-
|
|
1932
|
-
${packageJson.description}
|
|
1933
|
-
|
|
1934
|
-
## Workspace Dependency Landscapes
|
|
1935
|
-
|
|
1936
|
-
### Core Infrastructure Runtimes (\`dependencies\`)
|
|
1937
|
-
${displayDeps}
|
|
1938
|
-
|
|
1939
|
-
### System Tooling Engines (\`devDependencies\`)
|
|
1940
|
-
${displayDevDeps}
|
|
1941
|
-
|
|
1942
|
-
---
|
|
1943
|
-
|
|
1944
|
-
## Project Architecture Layout
|
|
1945
|
-
\`\`\`text
|
|
1946
|
-
${layoutTree}
|
|
1947
|
-
\`\`\`
|
|
1948
|
-
|
|
1949
|
-
## Installation
|
|
1950
|
-
|
|
1951
|
-
\`\`\`bash
|
|
1952
|
-
${activePkgManager} install
|
|
1953
|
-
\`\`\`
|
|
1954
|
-
`;
|
|
2384
|
+
const documentationTemplate = `# ${pName}\n\n\n\n\n\n${packageJson.description}\n\n## Workspace Dependency Landscapes\n\n### Core Infrastructure Runtimes (\`dependencies\`)\n${displayDeps}\n\n### System Tooling Engines (\`devDependencies\`)\n${displayDevDeps}\n\n---\n\n## Project Architecture Layout\n\`\`\`text\n${layoutTree}\n\`\`\`\n\n## Installation\n\n\`\`\`bash\n${activePkgManager} install\n\`\`\`\n`;
|
|
1955
2385
|
fs.writeFileSync(readmePath, documentationTemplate);
|
|
1956
|
-
console.log(`
|
|
2386
|
+
console.log(` 📖 Generated: README.md`);
|
|
1957
2387
|
}
|
|
1958
2388
|
|
|
1959
|
-
// --- Phantom injection fix ---
|
|
1960
2389
|
if (stats.phantomInjections.size > 0 || (stats.injectDotenvEngine && stats.filesWithEnvVars.size > 0)) {
|
|
1961
2390
|
console.log(`\n💡 Source Code Modification Subsystem:`);
|
|
1962
2391
|
const injectChoice = await safeQuestion(`❓ Found phantom modules or unmanaged env components. Mutate file headers cleanly now? (y/N): `);
|
|
1963
2392
|
|
|
1964
2393
|
if (injectChoice.trim().toLowerCase() === 'y' || injectChoice.trim().toLowerCase() === 'yes') {
|
|
1965
2394
|
const allTargets = new Set([...stats.phantomInjections.keys(), ...stats.filesWithEnvVars]);
|
|
1966
|
-
|
|
1967
2395
|
for (const filePath of allTargets) {
|
|
1968
2396
|
const originalCode = readFileSyncNormalized(filePath);
|
|
1969
2397
|
let declarationBlock = '';
|
|
@@ -1971,39 +2399,39 @@ ${activePkgManager} install
|
|
|
1971
2399
|
const missingModules = stats.phantomInjections.get(filePath);
|
|
1972
2400
|
if (missingModules) {
|
|
1973
2401
|
for (const mod of missingModules) {
|
|
1974
|
-
if (packageJson.type === 'module')
|
|
1975
|
-
|
|
2402
|
+
if (packageJson.type === 'module') {
|
|
2403
|
+
declarationBlock += `import ${mod} from '${mod}';\n`;
|
|
2404
|
+
} else {
|
|
2405
|
+
declarationBlock += `const ${mod} = require('${mod}');\n`;
|
|
2406
|
+
}
|
|
1976
2407
|
}
|
|
1977
2408
|
}
|
|
1978
|
-
|
|
1979
2409
|
if (stats.injectDotenvEngine && stats.filesWithEnvVars.has(filePath) && !originalCode.includes('dotenv')) {
|
|
1980
|
-
if (packageJson.type === 'module')
|
|
1981
|
-
|
|
2410
|
+
if (packageJson.type === 'module') {
|
|
2411
|
+
declarationBlock += `import 'dotenv/config';\n`;
|
|
2412
|
+
} else {
|
|
2413
|
+
declarationBlock += `require('dotenv').config();\n`;
|
|
2414
|
+
}
|
|
1982
2415
|
}
|
|
1983
|
-
|
|
1984
2416
|
if (declarationBlock !== '') {
|
|
1985
2417
|
fs.writeFileSync(filePath, smartPrepend(originalCode, declarationBlock));
|
|
1986
|
-
console.log(`
|
|
2418
|
+
console.log(` ⚡ Injected headers: ${path.relative(targetDir, filePath)}`);
|
|
1987
2419
|
}
|
|
1988
2420
|
}
|
|
1989
2421
|
}
|
|
1990
2422
|
}
|
|
1991
2423
|
|
|
1992
|
-
// --- Deprecation scan via npm-deprecated-check ---
|
|
1993
2424
|
console.log(`\n🛑 INITIALIZING LIVE ECOSYSTEM DEPRECATION SECURITY SCAN...`);
|
|
1994
|
-
console.log(`
|
|
2425
|
+
console.log(` Running integrated npm-deprecated-check validation:\n`);
|
|
1995
2426
|
try {
|
|
1996
2427
|
const localRequire = createRequire(import.meta.url);
|
|
1997
2428
|
const dependencyPkgJsonPath = localRequire.resolve('npm-deprecated-check/package.json');
|
|
1998
2429
|
const dependencyPkgJson = JSON.parse(fs.readFileSync(dependencyPkgJsonPath, 'utf8'));
|
|
1999
|
-
const binRelativeMapping = typeof dependencyPkgJson.bin === 'string'
|
|
2000
|
-
? dependencyPkgJson.bin
|
|
2001
|
-
: (dependencyPkgJson.bin['npm-deprecated-check'] || dependencyPkgJson.bin['ndc']);
|
|
2430
|
+
const binRelativeMapping = typeof dependencyPkgJson.bin === 'string' ? dependencyPkgJson.bin : (dependencyPkgJson.bin['npm-deprecated-check'] || dependencyPkgJson.bin['ndc']);
|
|
2002
2431
|
const absoluteExecutablePath = path.join(path.dirname(dependencyPkgJsonPath), binRelativeMapping);
|
|
2003
2432
|
execSync(`node "${absoluteExecutablePath}" current`, { stdio: 'inherit', cwd: targetDir });
|
|
2004
|
-
} catch (
|
|
2433
|
+
} catch (deprecationBinaryRunError) {}
|
|
2005
2434
|
|
|
2006
|
-
// --- Conflicting lockfiles ---
|
|
2007
2435
|
if (stats.conflictingLockfiles.length > 1) {
|
|
2008
2436
|
console.log(`\n⚠️ CONFLICTING LOCKFILES DETECTED: [${stats.conflictingLockfiles.join(', ')}]`);
|
|
2009
2437
|
const cleanLocks = await safeQuestion(`❓ Purge legacy/mismatched lockfiles to protect package integrity? (y/N): `);
|
|
@@ -2014,62 +2442,76 @@ ${activePkgManager} install
|
|
|
2014
2442
|
if (lockfile !== operationalLockfile) {
|
|
2015
2443
|
try {
|
|
2016
2444
|
fs.unlinkSync(path.join(targetDir, lockfile));
|
|
2017
|
-
console.log(`
|
|
2018
|
-
} catch (
|
|
2445
|
+
console.log(` 🗑 Cleaned: ${lockfile}`);
|
|
2446
|
+
} catch (lockFileUnlinkError) {}
|
|
2019
2447
|
}
|
|
2020
2448
|
}
|
|
2021
2449
|
}
|
|
2022
2450
|
}
|
|
2023
2451
|
|
|
2024
|
-
// --- Final install prompt ---
|
|
2025
2452
|
console.log(`\n📦 Auto-scaffolding pipeline complete!`);
|
|
2026
2453
|
|
|
2027
|
-
|
|
2028
|
-
|
|
2454
|
+
if (stats.frameworkOptimizations.length > 0) {
|
|
2455
|
+
console.log(`\n🧩 FRAMEWORK ARCHITECTURE OPTIMIZATIONS:`);
|
|
2456
|
+
console.log('─'.repeat(67));
|
|
2457
|
+
for (const optimization of stats.frameworkOptimizations) {
|
|
2458
|
+
console.log(` 💡 ${optimization}`);
|
|
2459
|
+
}
|
|
2460
|
+
console.log('─'.repeat(67));
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2029
2463
|
console.log(`\n${'═'.repeat(67)}`);
|
|
2030
2464
|
console.log(`📊 DEPENDENCY INTELLIGENCE SUMMARY`);
|
|
2031
2465
|
console.log(`${'═'.repeat(67)}`);
|
|
2032
|
-
console.log(`
|
|
2033
|
-
console.log(`
|
|
2034
|
-
if (stats.ghostDependencies.size > 0)
|
|
2035
|
-
console.log(`
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2466
|
+
console.log(` 📁 Files scanned: ${stats.scannedFiles}`);
|
|
2467
|
+
console.log(` 📦 Packages imported: ${stats.allImportedPackages.size}`);
|
|
2468
|
+
if (stats.ghostDependencies.size > 0) {
|
|
2469
|
+
console.log(` 🚨 Ghost deps (missing): ${stats.ghostDependencies.size} — \x1b[31mCRITICAL\x1b[0m`);
|
|
2470
|
+
}
|
|
2471
|
+
if (stats.orphanedDependencies.size > 0) {
|
|
2472
|
+
console.log(` 🗑️ Orphaned deps (unused): ${stats.orphanedDependencies.size}`);
|
|
2473
|
+
}
|
|
2040
2474
|
if (stats.unusedExportsPerFile.size > 0) {
|
|
2041
|
-
console.log(`
|
|
2475
|
+
console.log(` 📤 Unused public symbols: ${Array.from(stats.unusedExportsPerFile.values()).reduce((acc, val) => { return acc + val.size; }, 0)} tokens`);
|
|
2042
2476
|
}
|
|
2043
2477
|
if (stats.unusedFiles.size > 0) {
|
|
2044
|
-
console.log(`
|
|
2045
|
-
}
|
|
2046
|
-
if (stats.deprecatedPackages.size > 0)
|
|
2047
|
-
console.log(`
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
if (stats.
|
|
2053
|
-
console.log(`
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
if (stats.quality.
|
|
2059
|
-
console.log(`
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2478
|
+
console.log(` 🗑️ Unused dead files: ${stats.unusedFiles.size} files`);
|
|
2479
|
+
}
|
|
2480
|
+
if (stats.deprecatedPackages.size > 0) {
|
|
2481
|
+
console.log(` ` + `📛 Deprecated packages: ${stats.deprecatedPackages.size}`);
|
|
2482
|
+
}
|
|
2483
|
+
if (stats.phantomInjections.size > 0) {
|
|
2484
|
+
console.log(` 👻 Phantom injections: ${stats.phantomInjections.size} file(s)`);
|
|
2485
|
+
}
|
|
2486
|
+
if (stats.discoveredSecrets.length > 0) {
|
|
2487
|
+
console.log(` 🔐 Hardcoded secrets: ${stats.discoveredSecrets.length} — \x1b[31mSECURITY RISK\x1b[0m`);
|
|
2488
|
+
}
|
|
2489
|
+
if (stats.quality.insecureCryptoUsage.length > 0) {
|
|
2490
|
+
console.log(` 🚫 Insecure Crypto: ${stats.quality.insecureCryptoUsage.length} — \x1b[31mSECURITY RISK\x1b[0m`);
|
|
2491
|
+
}
|
|
2492
|
+
if (stats.quality.sqlInjectionVulnerabilities.length > 0) {
|
|
2493
|
+
console.log(` 💉 SQL Injection: ${stats.quality.sqlInjectionVulnerabilities.length} — \x1b[31mSECURITY RISK\x1b[0m`);
|
|
2494
|
+
}
|
|
2495
|
+
if (stats.quality.xssVulnerabilities.length > 0) {
|
|
2496
|
+
console.log(` 🌐 XSS Vulnerabilities: ${stats.quality.xssVulnerabilities.length} — \x1b[31mSECURITY RISK\x1b[0m`);
|
|
2497
|
+
}
|
|
2498
|
+
if (stats.quality.insecurePatterns.length > 0) {
|
|
2499
|
+
console.log(` 🌐 Insecure DOM Patterns: ${stats.quality.insecurePatterns.length} — \x1b[31mSECURITY RISK\x1b[0m`);
|
|
2500
|
+
}
|
|
2501
|
+
if (stats.quality.largeImageImports.length > 0) {
|
|
2502
|
+
console.log(` 🖼️ Large Image Imports: ${stats.quality.largeImageImports.length} — \x1b[33mPERFORMANCE WARNING\x1b[0m`);
|
|
2503
|
+
}
|
|
2504
|
+
if (stats.quality.unoptimizedLoops.length > 0) {
|
|
2505
|
+
console.log(` 🐌 Unoptimized Loops: ${stats.quality.unoptimizedLoops.length} — \x1b[33mPERFORMANCE WARNING\x1b[0m`);
|
|
2506
|
+
}
|
|
2064
2507
|
console.log(`${'═'.repeat(67)}`);
|
|
2065
2508
|
|
|
2066
|
-
// 6. Hygen-like Templating and Scaffolding
|
|
2067
2509
|
const templateManager = new TemplateManager(targetDir, safeQuestion);
|
|
2068
2510
|
const availableTemplates = await templateManager.listAvailableTemplates();
|
|
2069
2511
|
|
|
2070
2512
|
if (availableTemplates.length > 0) {
|
|
2071
2513
|
console.log(`\n🧩 \x1b[1mCustom Templating Engine Detected:\x1b[0m`);
|
|
2072
|
-
console.log(`
|
|
2514
|
+
console.log(` Available templates: ${availableTemplates.join(", ")}`);
|
|
2073
2515
|
const useTemplate = await safeQuestion(`❓ Do you want to generate code from a template? (y/N): `);
|
|
2074
2516
|
if (useTemplate.toLowerCase() === 'y') {
|
|
2075
2517
|
const chosenTemplate = await safeQuestion(`❓ Enter template name: `);
|
|
@@ -2077,7 +2519,7 @@ ${activePkgManager} install
|
|
|
2077
2519
|
const templateVars = await templateManager.promptForVariables(chosenTemplate);
|
|
2078
2520
|
await templateManager.generate(chosenTemplate, templateVars);
|
|
2079
2521
|
} else {
|
|
2080
|
-
console.log(`
|
|
2522
|
+
console.log(` ⚠️ Template '${chosenTemplate}' not found.`);
|
|
2081
2523
|
}
|
|
2082
2524
|
}
|
|
2083
2525
|
}
|
|
@@ -2091,7 +2533,7 @@ ${activePkgManager} install
|
|
|
2091
2533
|
try {
|
|
2092
2534
|
execSync(`${activePkgManager} install`, { stdio: 'inherit', cwd: targetDir });
|
|
2093
2535
|
console.log(`\n🎉 Project fully mapped, configured, and installed successfully!`);
|
|
2094
|
-
} catch (
|
|
2536
|
+
} catch (installProcessRuntimeError) {
|
|
2095
2537
|
console.error(`\n❌ Installation returned an issue. Please run "${activePkgManager} install" manually.`);
|
|
2096
2538
|
}
|
|
2097
2539
|
} else {
|
|
@@ -2099,354 +2541,4 @@ ${activePkgManager} install
|
|
|
2099
2541
|
}
|
|
2100
2542
|
}
|
|
2101
2543
|
|
|
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
|
-
}
|
|
2544
|
+
main();
|