voltjs-framework 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1265 -0
  3. package/bin/volt.js +139 -0
  4. package/package.json +56 -0
  5. package/src/api/graphql.js +399 -0
  6. package/src/api/rest.js +204 -0
  7. package/src/api/websocket.js +285 -0
  8. package/src/cli/build.js +111 -0
  9. package/src/cli/create.js +371 -0
  10. package/src/cli/db.js +106 -0
  11. package/src/cli/dev.js +114 -0
  12. package/src/cli/generate.js +278 -0
  13. package/src/cli/lint.js +172 -0
  14. package/src/cli/routes.js +118 -0
  15. package/src/cli/start.js +42 -0
  16. package/src/cli/test.js +138 -0
  17. package/src/core/app.js +701 -0
  18. package/src/core/config.js +232 -0
  19. package/src/core/middleware.js +133 -0
  20. package/src/core/plugins.js +88 -0
  21. package/src/core/react-renderer.js +244 -0
  22. package/src/core/renderer.js +337 -0
  23. package/src/core/router.js +183 -0
  24. package/src/database/index.js +461 -0
  25. package/src/database/migration.js +192 -0
  26. package/src/database/model.js +285 -0
  27. package/src/database/query.js +394 -0
  28. package/src/database/seeder.js +89 -0
  29. package/src/index.js +156 -0
  30. package/src/security/auth.js +425 -0
  31. package/src/security/cors.js +80 -0
  32. package/src/security/csrf.js +125 -0
  33. package/src/security/encryption.js +110 -0
  34. package/src/security/helmet.js +103 -0
  35. package/src/security/index.js +75 -0
  36. package/src/security/rateLimit.js +119 -0
  37. package/src/security/sanitizer.js +113 -0
  38. package/src/security/xss.js +110 -0
  39. package/src/ui/component.js +224 -0
  40. package/src/ui/reactive.js +503 -0
  41. package/src/ui/template.js +448 -0
  42. package/src/utils/cache.js +216 -0
  43. package/src/utils/collection.js +772 -0
  44. package/src/utils/cron.js +213 -0
  45. package/src/utils/date.js +223 -0
  46. package/src/utils/events.js +181 -0
  47. package/src/utils/excel.js +482 -0
  48. package/src/utils/form.js +547 -0
  49. package/src/utils/hash.js +121 -0
  50. package/src/utils/http.js +461 -0
  51. package/src/utils/logger.js +186 -0
  52. package/src/utils/mail.js +347 -0
  53. package/src/utils/paginator.js +179 -0
  54. package/src/utils/pdf.js +417 -0
  55. package/src/utils/queue.js +199 -0
  56. package/src/utils/schema.js +985 -0
  57. package/src/utils/sms.js +243 -0
  58. package/src/utils/storage.js +348 -0
  59. package/src/utils/string.js +236 -0
  60. package/src/utils/validation.js +318 -0
@@ -0,0 +1,371 @@
1
+ /**
2
+ * VoltJS CLI — `volt create <project-name>`
3
+ * Scaffolds a new VoltJS project.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const c = {
12
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
13
+ red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m',
14
+ };
15
+
16
+ module.exports = function create(args) {
17
+ const projectName = args[0];
18
+ if (!projectName) {
19
+ console.error(`${c.red}Error: Please specify a project name.${c.reset}`);
20
+ console.log(` ${c.cyan}volt create my-app${c.reset}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ const projectDir = path.resolve(process.cwd(), projectName);
25
+
26
+ if (fs.existsSync(projectDir)) {
27
+ console.error(`${c.red}Error: Directory "${projectName}" already exists.${c.reset}`);
28
+ process.exit(1);
29
+ }
30
+
31
+ console.log(`\n${c.cyan}${c.bold}⚡ Creating VoltJS project: ${projectName}${c.reset}\n`);
32
+
33
+ // Create directory structure
34
+ const dirs = [
35
+ '',
36
+ 'pages',
37
+ 'api',
38
+ 'components',
39
+ 'public',
40
+ 'public/css',
41
+ 'public/js',
42
+ 'public/images',
43
+ 'views',
44
+ 'views/layouts',
45
+ 'views/partials',
46
+ 'models',
47
+ 'middleware',
48
+ 'config',
49
+ 'database',
50
+ 'database/migrations',
51
+ 'database/seeders',
52
+ 'storage',
53
+ 'tests',
54
+ 'logs',
55
+ ];
56
+
57
+ for (const dir of dirs) {
58
+ const fullPath = path.join(projectDir, dir);
59
+ fs.mkdirSync(fullPath, { recursive: true });
60
+ console.log(` ${c.green}✓${c.reset} ${c.dim}${dir || projectName}/${c.reset}`);
61
+ }
62
+
63
+ // Create files
64
+ const files = {
65
+ 'package.json': JSON.stringify({
66
+ name: projectName,
67
+ version: '1.0.0',
68
+ private: true,
69
+ scripts: {
70
+ dev: 'volt dev',
71
+ start: 'volt start',
72
+ build: 'volt build',
73
+ test: 'volt test',
74
+ 'db:migrate': 'volt db:migrate',
75
+ 'db:seed': 'volt db:seed',
76
+ },
77
+ dependencies: {
78
+ voltjs: '^1.0.0',
79
+ },
80
+ }, null, 2),
81
+
82
+ 'app.js': `const { Volt } = require('voltjs');
83
+
84
+ const app = new Volt();
85
+
86
+ // Your routes
87
+ app.get('/', (req, res) => {
88
+ res.render('index', {
89
+ title: '${projectName}',
90
+ message: 'Welcome to VoltJS! ⚡',
91
+ });
92
+ });
93
+
94
+ // API example
95
+ app.get('/api/hello', (req, res) => {
96
+ res.json({ message: 'Hello from VoltJS API!', time: new Date().toISOString() });
97
+ });
98
+
99
+ // Start the server
100
+ app.listen(3000, () => {
101
+ console.log('⚡ Server running at http://localhost:3000');
102
+ });
103
+ `,
104
+
105
+ 'volt.config.js': `module.exports = {
106
+ // Server
107
+ port: process.env.PORT || 3000,
108
+
109
+ // Security (all enabled by default)
110
+ security: {
111
+ csrf: true,
112
+ cors: { origin: '*' },
113
+ rateLimit: { windowMs: 15 * 60 * 1000, max: 100 },
114
+ helmet: true,
115
+ },
116
+
117
+ // Views
118
+ views: {
119
+ dir: './views',
120
+ engine: 'volt',
121
+ },
122
+
123
+ // Database
124
+ database: {
125
+ driver: 'memory', // 'memory', 'sqlite', 'mysql', 'postgres'
126
+ // host: 'localhost',
127
+ // port: 3306,
128
+ // database: '${projectName}',
129
+ // user: 'root',
130
+ // password: '',
131
+ },
132
+
133
+ // Mail
134
+ mail: {
135
+ host: process.env.MAIL_HOST || 'smtp.example.com',
136
+ port: process.env.MAIL_PORT || 587,
137
+ secure: false,
138
+ user: process.env.MAIL_USER || '',
139
+ pass: process.env.MAIL_PASS || '',
140
+ from: '${projectName} <noreply@example.com>',
141
+ },
142
+
143
+ // Logging
144
+ logging: {
145
+ level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
146
+ file: './logs/app.log',
147
+ },
148
+ };
149
+ `,
150
+
151
+ '.env': `# Environment
152
+ NODE_ENV=development
153
+ PORT=3000
154
+
155
+ # Database
156
+ DB_DRIVER=memory
157
+ # DB_HOST=localhost
158
+ # DB_PORT=3306
159
+ # DB_DATABASE=${projectName}
160
+ # DB_USER=root
161
+ # DB_PASSWORD=
162
+
163
+ # Mail
164
+ # MAIL_HOST=smtp.example.com
165
+ # MAIL_PORT=587
166
+ # MAIL_USER=
167
+ # MAIL_PASS=
168
+
169
+ # Security
170
+ APP_SECRET=${require('crypto').randomBytes(32).toString('hex')}
171
+ `,
172
+
173
+ '.env.example': `# Environment
174
+ NODE_ENV=development
175
+ PORT=3000
176
+
177
+ # Database
178
+ DB_DRIVER=memory
179
+ DB_HOST=localhost
180
+ DB_PORT=3306
181
+ DB_DATABASE=myapp
182
+ DB_USER=root
183
+ DB_PASSWORD=
184
+
185
+ # Mail
186
+ MAIL_HOST=smtp.example.com
187
+ MAIL_PORT=587
188
+ MAIL_USER=
189
+ MAIL_PASS=
190
+
191
+ # Security
192
+ APP_SECRET=your-secret-key-here
193
+ `,
194
+
195
+ '.gitignore': `node_modules/
196
+ .env
197
+ logs/
198
+ storage/
199
+ dist/
200
+ *.log
201
+ .DS_Store
202
+ Thumbs.db
203
+ `,
204
+
205
+ 'views/layouts/main.volt': `<!DOCTYPE html>
206
+ <html lang="en">
207
+ <head>
208
+ <meta charset="UTF-8">
209
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
210
+ <title>{{ title }} | ${projectName}</title>
211
+ <link rel="stylesheet" href="/css/app.css">
212
+ </head>
213
+ <body>
214
+ {> header}
215
+ <main>
216
+ {#slot content}
217
+ </main>
218
+ {> footer}
219
+ <script src="/js/app.js"></script>
220
+ </body>
221
+ </html>
222
+ `,
223
+
224
+ 'views/partials/header.volt': `<header style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 1rem 2rem;">
225
+ <nav style="display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto;">
226
+ <a href="/" style="color: white; text-decoration: none; font-size: 1.5rem; font-weight: bold;">⚡ ${projectName}</a>
227
+ <div>
228
+ <a href="/" style="color: white; text-decoration: none; margin-left: 1.5rem;">Home</a>
229
+ <a href="/api/hello" style="color: white; text-decoration: none; margin-left: 1.5rem;">API</a>
230
+ </div>
231
+ </nav>
232
+ </header>
233
+ `,
234
+
235
+ 'views/partials/footer.volt': `<footer style="text-align: center; padding: 2rem; color: #666; border-top: 1px solid #eee; margin-top: 2rem;">
236
+ <p>Built with ⚡ <a href="https://github.com/voltjs" style="color: #667eea;">VoltJS</a></p>
237
+ </footer>
238
+ `,
239
+
240
+ 'views/index.volt': `{#layout layouts/main}
241
+
242
+ {#block content}
243
+ <div style="text-align: center; padding: 4rem 2rem; max-width: 800px; margin: 0 auto;">
244
+ <h1 style="font-size: 3rem; margin-bottom: 0.5rem;">⚡ {{ title }}</h1>
245
+ <p style="font-size: 1.3rem; color: #666;">{{ message }}</p>
246
+
247
+ <div style="margin-top: 3rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; text-align: left;">
248
+ <div style="padding: 1.5rem; border-radius: 8px; border: 1px solid #e2e8f0;">
249
+ <h3>🔒 Security First</h3>
250
+ <p style="color: #666;">CSRF, XSS, CORS, rate limiting, and helmet — all enabled by default.</p>
251
+ </div>
252
+ <div style="padding: 1.5rem; border-radius: 8px; border: 1px solid #e2e8f0;">
253
+ <h3>📦 Batteries Included</h3>
254
+ <p style="color: #666;">Email, SMS, PDF, Excel, caching, queues, cron — ready to use.</p>
255
+ </div>
256
+ <div style="padding: 1.5rem; border-radius: 8px; border: 1px solid #e2e8f0;">
257
+ <h3>🚀 Zero Config</h3>
258
+ <p style="color: #666;">Just 2 files to start. Everything works out of the box.</p>
259
+ </div>
260
+ </div>
261
+ </div>
262
+ {/block}
263
+ `,
264
+
265
+ 'public/css/app.css': `/* VoltJS Default Styles */
266
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
267
+
268
+ body {
269
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
270
+ line-height: 1.6;
271
+ color: #333;
272
+ min-height: 100vh;
273
+ }
274
+
275
+ a { color: #667eea; text-decoration: none; }
276
+ a:hover { text-decoration: underline; }
277
+
278
+ code {
279
+ background: #f4f4f5;
280
+ padding: 0.2em 0.4em;
281
+ border-radius: 4px;
282
+ font-size: 0.9em;
283
+ }
284
+ `,
285
+
286
+ 'public/js/app.js': `// VoltJS Client-Side JavaScript
287
+ console.log('⚡ VoltJS loaded');
288
+ `,
289
+
290
+ 'tests/app.test.js': `/**
291
+ * Basic tests for your VoltJS app
292
+ * Run: volt test
293
+ */
294
+
295
+ const assert = require('assert');
296
+
297
+ describe('App', () => {
298
+ it('should load without errors', () => {
299
+ assert.ok(true);
300
+ });
301
+ });
302
+ `,
303
+
304
+ 'README.md': `# ${projectName}
305
+
306
+ Built with ⚡ [VoltJS](https://github.com/voltjs) — the batteries-included, security-first JavaScript framework.
307
+
308
+ ## Quick Start
309
+
310
+ \`\`\`bash
311
+ npm install
312
+ npm run dev
313
+ \`\`\`
314
+
315
+ Open [http://localhost:3000](http://localhost:3000) in your browser.
316
+
317
+ ## Available Commands
318
+
319
+ | Command | Description |
320
+ |---------|-------------|
321
+ | \`npm run dev\` | Start development server with hot reload |
322
+ | \`npm start\` | Start production server |
323
+ | \`npm run build\` | Build for production |
324
+ | \`npm test\` | Run tests |
325
+ | \`npm run db:migrate\` | Run database migrations |
326
+ | \`npm run db:seed\` | Seed the database |
327
+
328
+ ## Project Structure
329
+
330
+ \`\`\`
331
+ ${projectName}/
332
+ ├── app.js # Main application entry
333
+ ├── volt.config.js # Framework configuration
334
+ ├── pages/ # File-based routes (auto-discovered)
335
+ ├── api/ # API routes (auto-discovered)
336
+ ├── views/ # Templates
337
+ │ ├── layouts/ # Layout files
338
+ │ └── partials/ # Partial templates
339
+ ├── components/ # Reusable components
340
+ ├── models/ # Database models
341
+ ├── middleware/ # Custom middleware
342
+ ├── public/ # Static assets
343
+ ├── database/ # Migrations & seeders
344
+ ├── storage/ # File uploads
345
+ ├── tests/ # Test files
346
+ └── logs/ # Log files
347
+ \`\`\`
348
+ `,
349
+ };
350
+
351
+ // Write files
352
+ for (const [filePath, content] of Object.entries(files)) {
353
+ const fullPath = path.join(projectDir, filePath);
354
+ const dir = path.dirname(fullPath);
355
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
356
+ fs.writeFileSync(fullPath, content);
357
+ console.log(` ${c.green}✓${c.reset} ${filePath}`);
358
+ }
359
+
360
+ console.log(`
361
+ ${c.green}${c.bold}✓ Project created successfully!${c.reset}
362
+
363
+ ${c.bold}Next steps:${c.reset}
364
+
365
+ ${c.cyan}cd ${projectName}${c.reset}
366
+ ${c.cyan}npm install${c.reset}
367
+ ${c.cyan}npm run dev${c.reset}
368
+
369
+ ${c.dim}Your app will be running at ${c.cyan}http://localhost:3000${c.reset}
370
+ `);
371
+ };
package/src/cli/db.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * VoltJS CLI — `volt db:migrate`, `volt db:seed`, `volt db:rollback`
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ const path = require('path');
8
+
9
+ const c = {
10
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
11
+ red: '\x1b[31m', green: '\x1b[32m', cyan: '\x1b[36m',
12
+ };
13
+
14
+ module.exports = async function db(args) {
15
+ const action = args[0];
16
+ const cwd = process.cwd();
17
+
18
+ // Load config
19
+ let config = {};
20
+ try {
21
+ config = require(path.join(cwd, 'volt.config.js'));
22
+ } catch { }
23
+
24
+ // Import database and migration/seeder modules
25
+ let Database, Migration, Seeder;
26
+ try {
27
+ ({ Database } = require('../database'));
28
+ ({ Migration } = require('../database/migration'));
29
+ ({ Seeder } = require('../database/seeder'));
30
+ } catch (err) {
31
+ console.error(`${c.red}Error loading database modules: ${err.message}${c.reset}`);
32
+ process.exit(1);
33
+ }
34
+
35
+ const db = new Database(config.database || {});
36
+ await db.connect();
37
+
38
+ switch (action) {
39
+ case 'migrate': {
40
+ console.log(`${c.cyan}${c.bold}⚡ Running migrations...${c.reset}\n`);
41
+ const migrationsDir = path.join(cwd, 'database', 'migrations');
42
+ try {
43
+ const result = await Migration.run(db, migrationsDir);
44
+ if (result && result.length > 0) {
45
+ for (const m of result) {
46
+ console.log(` ${c.green}✓${c.reset} ${m}`);
47
+ }
48
+ } else {
49
+ console.log(` ${c.dim}Nothing to migrate.${c.reset}`);
50
+ }
51
+ console.log(`\n${c.green}Migrations complete.${c.reset}`);
52
+ } catch (err) {
53
+ console.error(`${c.red}Migration failed: ${err.message}${c.reset}`);
54
+ process.exit(1);
55
+ }
56
+ break;
57
+ }
58
+
59
+ case 'rollback': {
60
+ console.log(`${c.cyan}${c.bold}⚡ Rolling back...${c.reset}\n`);
61
+ const migrationsDir = path.join(cwd, 'database', 'migrations');
62
+ try {
63
+ const result = await Migration.rollback(db, migrationsDir);
64
+ if (result && result.length > 0) {
65
+ for (const m of result) {
66
+ console.log(` ${c.green}✓${c.reset} Rolled back: ${m}`);
67
+ }
68
+ } else {
69
+ console.log(` ${c.dim}Nothing to rollback.${c.reset}`);
70
+ }
71
+ console.log(`\n${c.green}Rollback complete.${c.reset}`);
72
+ } catch (err) {
73
+ console.error(`${c.red}Rollback failed: ${err.message}${c.reset}`);
74
+ process.exit(1);
75
+ }
76
+ break;
77
+ }
78
+
79
+ case 'seed': {
80
+ console.log(`${c.cyan}${c.bold}⚡ Running seeders...${c.reset}\n`);
81
+ const seedersDir = path.join(cwd, 'database', 'seeders');
82
+ try {
83
+ const result = await Seeder.run(db, seedersDir);
84
+ if (result && result.length > 0) {
85
+ for (const s of result) {
86
+ console.log(` ${c.green}✓${c.reset} ${s}`);
87
+ }
88
+ } else {
89
+ console.log(` ${c.dim}No seeders found.${c.reset}`);
90
+ }
91
+ console.log(`\n${c.green}Seeding complete.${c.reset}`);
92
+ } catch (err) {
93
+ console.error(`${c.red}Seeding failed: ${err.message}${c.reset}`);
94
+ process.exit(1);
95
+ }
96
+ break;
97
+ }
98
+
99
+ default:
100
+ console.error(`${c.red}Unknown db command: ${action}${c.reset}`);
101
+ console.log(` Available: ${c.cyan}migrate${c.reset}, ${c.cyan}rollback${c.reset}, ${c.cyan}seed${c.reset}`);
102
+ process.exit(1);
103
+ }
104
+
105
+ process.exit(0);
106
+ };
package/src/cli/dev.js ADDED
@@ -0,0 +1,114 @@
1
+ /**
2
+ * VoltJS CLI — `volt dev`
3
+ * Starts a development server with hot reload (file watching).
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { spawn } = require('child_process');
11
+
12
+ const c = {
13
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
14
+ red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m',
15
+ };
16
+
17
+ module.exports = function dev(args) {
18
+ const cwd = process.cwd();
19
+ const entry = findEntry(cwd);
20
+
21
+ if (!entry) {
22
+ console.error(`${c.red}Error: No app entry point found (app.js, index.js, or server.js).${c.reset}`);
23
+ process.exit(1);
24
+ }
25
+
26
+ console.log(`${c.cyan}${c.bold}⚡ VoltJS Dev Server${c.reset}`);
27
+ console.log(`${c.dim}Watching for file changes...${c.reset}\n`);
28
+
29
+ let child = null;
30
+ let restarting = false;
31
+ let debounceTimer = null;
32
+
33
+ function startServer() {
34
+ if (child) {
35
+ child.kill('SIGTERM');
36
+ }
37
+
38
+ child = spawn(process.execPath, [entry], {
39
+ cwd,
40
+ stdio: 'inherit',
41
+ env: { ...process.env, NODE_ENV: 'development', VOLT_DEV: '1' },
42
+ });
43
+
44
+ child.on('exit', (code, signal) => {
45
+ if (!restarting && signal !== 'SIGTERM') {
46
+ if (code !== 0) {
47
+ console.log(`\n${c.yellow}Process exited with code ${code}. Waiting for changes...${c.reset}`);
48
+ }
49
+ }
50
+ });
51
+
52
+ child.on('error', (err) => {
53
+ console.error(`${c.red}Failed to start: ${err.message}${c.reset}`);
54
+ });
55
+ }
56
+
57
+ function restart() {
58
+ if (debounceTimer) clearTimeout(debounceTimer);
59
+ debounceTimer = setTimeout(() => {
60
+ restarting = true;
61
+ console.log(`\n${c.cyan}↻ Restarting...${c.reset}`);
62
+ startServer();
63
+ restarting = false;
64
+ }, 200);
65
+ }
66
+
67
+ // Watch for file changes
68
+ const watchDirs = ['pages', 'api', 'views', 'components', 'models', 'middleware', 'config', 'src']
69
+ .map(d => path.join(cwd, d))
70
+ .filter(d => fs.existsSync(d));
71
+
72
+ // Also watch root JS files
73
+ watchDirs.push(cwd);
74
+
75
+ const watchedExtensions = ['.js', '.json', '.volt', '.html', '.css'];
76
+
77
+ for (const dir of watchDirs) {
78
+ try {
79
+ fs.watch(dir, { recursive: true }, (eventType, filename) => {
80
+ if (!filename) return;
81
+ const ext = path.extname(filename);
82
+ if (watchedExtensions.includes(ext)) {
83
+ console.log(`${c.dim} Changed: ${filename}${c.reset}`);
84
+ restart();
85
+ }
86
+ });
87
+ } catch {
88
+ // Directory may not support recursive watching
89
+ }
90
+ }
91
+
92
+ // Graceful shutdown
93
+ process.on('SIGINT', () => {
94
+ console.log(`\n${c.dim}Shutting down dev server...${c.reset}`);
95
+ if (child) child.kill('SIGTERM');
96
+ process.exit(0);
97
+ });
98
+
99
+ process.on('SIGTERM', () => {
100
+ if (child) child.kill('SIGTERM');
101
+ process.exit(0);
102
+ });
103
+
104
+ startServer();
105
+ };
106
+
107
+ function findEntry(cwd) {
108
+ const candidates = ['app.js', 'index.js', 'server.js', 'src/app.js', 'src/index.js'];
109
+ for (const file of candidates) {
110
+ const fullPath = path.join(cwd, file);
111
+ if (fs.existsSync(fullPath)) return fullPath;
112
+ }
113
+ return null;
114
+ }