@nitronjs/framework 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +429 -0
  2. package/cli/create.js +260 -0
  3. package/cli/njs.js +164 -0
  4. package/lib/Auth/Manager.js +111 -0
  5. package/lib/Build/Manager.js +1232 -0
  6. package/lib/Console/Commands/BuildCommand.js +25 -0
  7. package/lib/Console/Commands/DevCommand.js +385 -0
  8. package/lib/Console/Commands/MakeCommand.js +110 -0
  9. package/lib/Console/Commands/MigrateCommand.js +98 -0
  10. package/lib/Console/Commands/MigrateFreshCommand.js +97 -0
  11. package/lib/Console/Commands/SeedCommand.js +92 -0
  12. package/lib/Console/Commands/StorageLinkCommand.js +31 -0
  13. package/lib/Console/Stubs/controller.js +19 -0
  14. package/lib/Console/Stubs/middleware.js +9 -0
  15. package/lib/Console/Stubs/migration.js +23 -0
  16. package/lib/Console/Stubs/model.js +7 -0
  17. package/lib/Console/Stubs/page-hydration.tsx +54 -0
  18. package/lib/Console/Stubs/seeder.js +9 -0
  19. package/lib/Console/Stubs/vendor.tsx +11 -0
  20. package/lib/Core/Config.js +86 -0
  21. package/lib/Core/Environment.js +21 -0
  22. package/lib/Core/Paths.js +188 -0
  23. package/lib/Database/Connection.js +61 -0
  24. package/lib/Database/DB.js +84 -0
  25. package/lib/Database/Drivers/MySQLDriver.js +234 -0
  26. package/lib/Database/Manager.js +162 -0
  27. package/lib/Database/Model.js +161 -0
  28. package/lib/Database/QueryBuilder.js +714 -0
  29. package/lib/Database/QueryValidation.js +62 -0
  30. package/lib/Database/Schema/Blueprint.js +126 -0
  31. package/lib/Database/Schema/Manager.js +116 -0
  32. package/lib/Date/DateTime.js +108 -0
  33. package/lib/Date/Locale.js +68 -0
  34. package/lib/Encryption/Manager.js +47 -0
  35. package/lib/Filesystem/Manager.js +49 -0
  36. package/lib/Hashing/Manager.js +25 -0
  37. package/lib/Http/Server.js +317 -0
  38. package/lib/Logging/Manager.js +153 -0
  39. package/lib/Mail/Manager.js +120 -0
  40. package/lib/Route/Loader.js +81 -0
  41. package/lib/Route/Manager.js +265 -0
  42. package/lib/Runtime/Entry.js +11 -0
  43. package/lib/Session/File.js +299 -0
  44. package/lib/Session/Manager.js +259 -0
  45. package/lib/Session/Memory.js +67 -0
  46. package/lib/Session/Session.js +196 -0
  47. package/lib/Support/Str.js +100 -0
  48. package/lib/Translation/Manager.js +49 -0
  49. package/lib/Validation/MimeTypes.js +39 -0
  50. package/lib/Validation/Validator.js +691 -0
  51. package/lib/View/Manager.js +544 -0
  52. package/lib/View/Templates/default/Home.tsx +262 -0
  53. package/lib/View/Templates/default/MainLayout.tsx +44 -0
  54. package/lib/View/Templates/errors/404.tsx +13 -0
  55. package/lib/View/Templates/errors/500.tsx +13 -0
  56. package/lib/View/Templates/errors/ErrorLayout.tsx +112 -0
  57. package/lib/View/Templates/messages/Maintenance.tsx +17 -0
  58. package/lib/View/Templates/messages/MessageLayout.tsx +136 -0
  59. package/lib/index.js +57 -0
  60. package/package.json +47 -0
  61. package/skeleton/.env.example +26 -0
  62. package/skeleton/app/Controllers/HomeController.js +9 -0
  63. package/skeleton/app/Kernel.js +11 -0
  64. package/skeleton/app/Middlewares/Authentication.js +9 -0
  65. package/skeleton/app/Middlewares/Guest.js +9 -0
  66. package/skeleton/app/Middlewares/VerifyCsrf.js +24 -0
  67. package/skeleton/app/Models/User.js +7 -0
  68. package/skeleton/config/app.js +4 -0
  69. package/skeleton/config/auth.js +16 -0
  70. package/skeleton/config/database.js +27 -0
  71. package/skeleton/config/hash.js +3 -0
  72. package/skeleton/config/server.js +28 -0
  73. package/skeleton/config/session.js +21 -0
  74. package/skeleton/database/migrations/2025_01_01_00_00_users.js +20 -0
  75. package/skeleton/database/seeders/UserSeeder.js +15 -0
  76. package/skeleton/globals.d.ts +1 -0
  77. package/skeleton/package.json +24 -0
  78. package/skeleton/public/.gitkeep +0 -0
  79. package/skeleton/resources/css/.gitkeep +0 -0
  80. package/skeleton/resources/langs/.gitkeep +0 -0
  81. package/skeleton/resources/views/Site/Home.tsx +66 -0
  82. package/skeleton/routes/web.js +4 -0
  83. package/skeleton/storage/app/private/.gitkeep +0 -0
  84. package/skeleton/storage/app/public/.gitkeep +0 -0
  85. package/skeleton/storage/framework/sessions/.gitkeep +0 -0
  86. package/skeleton/storage/logs/.gitkeep +0 -0
  87. package/skeleton/tsconfig.json +33 -0
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import Builder from "../../Build/Manager.js";
3
+
4
+ export default async function Build(only = null) {
5
+ const builder = new Builder();
6
+
7
+ try {
8
+ const success = await builder.run(only);
9
+ return success;
10
+ } catch (error) {
11
+ console.error("Build failed:", error.message);
12
+ throw error;
13
+ }
14
+ }
15
+
16
+ // Auto-run when called directly
17
+ const isMain = process.argv[1]?.endsWith("BuildCommand.js");
18
+ if (isMain) {
19
+ // process.argv[2] would be --only=views or --only=css
20
+ const onlyArg = process.argv.find(arg => arg.startsWith("--only="));
21
+ const only = onlyArg?.split("=")[1] || null;
22
+ Build(only)
23
+ .then(success => process.exit(success ? 0 : 1))
24
+ .catch(() => process.exit(1));
25
+ }
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env node
2
+ import dotenv from "dotenv";
3
+ import { spawn } from "child_process";
4
+ import chokidar from "chokidar";
5
+ import path from "path";
6
+ import Paths from "../../Core/Paths.js";
7
+
8
+ dotenv.config({ quiet: true });
9
+
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+ // Constants
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+
14
+ const FRAMEWORK_DIR = Paths.framework;
15
+ const PROJECT_DIR = Paths.project;
16
+
17
+ const COLORS = {
18
+ reset: "\x1b[0m",
19
+ bold: "\x1b[1m",
20
+ dim: "\x1b[2m",
21
+ red: "\x1b[31m",
22
+ green: "\x1b[32m",
23
+ yellow: "\x1b[33m",
24
+ blue: "\x1b[34m",
25
+ magenta: "\x1b[35m",
26
+ cyan: "\x1b[36m"
27
+ };
28
+
29
+ // Watch configuration (relative to PROJECT_DIR)
30
+ const WATCH_CONFIG = {
31
+ // Files that trigger only a build (no server restart)
32
+ // Controllers are hot-reloaded via dynamic import in dev mode
33
+ buildOnly: [
34
+ "resources/views/**/*.tsx",
35
+ "resources/views/**/*.ts",
36
+ "resources/css/**/*.css",
37
+ "app/Controllers/*.js",
38
+ "app/Middlewares/*.js",
39
+ "app/Middlewares/**/*.js"
40
+ ],
41
+
42
+ // Files that trigger a full server restart
43
+ fullRestart: [
44
+ "config/**/*.js",
45
+ "app/Kernel.js",
46
+ "app/Models/**/*.js",
47
+ "routes/**/*.js",
48
+ "app.js"
49
+ ],
50
+
51
+ // Framework files (relative to FRAMEWORK_DIR) - trigger build only
52
+ frameworkBuildOnly: [
53
+ "lib/View/Templates/**/*.tsx",
54
+ "lib/View/Templates/**/*.ts"
55
+ ],
56
+
57
+ // Ignored patterns
58
+ ignore: [
59
+ "**/node_modules/**",
60
+ "**/build/**",
61
+ "**/storage/app/public/**",
62
+ "**/storage/framework/**",
63
+ "**/storage/logs/**",
64
+ "**/.nitron/**",
65
+ "**/.git/**",
66
+ "**/package-lock.json"
67
+ ]
68
+ };
69
+
70
+ // ─────────────────────────────────────────────────────────────────────────────
71
+ // Dev Server Class
72
+ // ─────────────────────────────────────────────────────────────────────────────
73
+
74
+ class DevServer {
75
+ #serverProcess = null;
76
+ #isBuilding = false;
77
+ #pendingRestart = false;
78
+ #pendingBuild = false;
79
+ #buildDebounceTimer = null;
80
+ #restartDebounceTimer = null;
81
+ #watcherReady = false;
82
+
83
+ constructor() {
84
+ //
85
+ }
86
+
87
+ // ─────────────────────────────────────────────────────────────────────────
88
+ // Logging
89
+ // ─────────────────────────────────────────────────────────────────────────
90
+
91
+ #log(type, message, details = null) {
92
+ const now = new Date();
93
+ const time = `${COLORS.dim}${now.toLocaleTimeString()}${COLORS.reset}`;
94
+
95
+ const icons = {
96
+ info: `${COLORS.blue}[i]${COLORS.reset}`,
97
+ success: `${COLORS.green}[✓]${COLORS.reset}`,
98
+ warning: `${COLORS.yellow}[!]${COLORS.reset}`,
99
+ error: `${COLORS.red}[✗]${COLORS.reset}`,
100
+ build: `${COLORS.magenta}[~]${COLORS.reset}`,
101
+ reload: `${COLORS.cyan}[↻]${COLORS.reset}`,
102
+ watch: `${COLORS.yellow}[○]${COLORS.reset}`
103
+ };
104
+
105
+ const icon = icons[type] || icons.info;
106
+ let output = `${time} ${icon} ${message}`;
107
+
108
+ if (details) {
109
+ output += ` ${COLORS.dim}(${details})${COLORS.reset}`;
110
+ }
111
+
112
+ console.log(output);
113
+ }
114
+
115
+ // ─────────────────────────────────────────────────────────────────────────
116
+ // Build Process
117
+ // ─────────────────────────────────────────────────────────────────────────
118
+
119
+ async #runBuild(buildArg = null) {
120
+ if (this.#isBuilding) {
121
+ this.#pendingBuild = true;
122
+ return false;
123
+ }
124
+
125
+ this.#isBuilding = true;
126
+ const startTime = Date.now();
127
+ this.#log("build", "Building...");
128
+
129
+ return new Promise((resolve) => {
130
+ const buildScript = path.join(FRAMEWORK_DIR, "lib/Console/Commands/BuildCommand.js");
131
+ const args = [buildScript];
132
+ if (buildArg) args.push(buildArg);
133
+
134
+ const buildProcess = spawn(process.execPath, args, {
135
+ cwd: PROJECT_DIR,
136
+ stdio: ["inherit", "pipe", "pipe"]
137
+ });
138
+
139
+ let output = "";
140
+
141
+ buildProcess.stdout.on("data", (data) => {
142
+ output += data.toString();
143
+ });
144
+
145
+ buildProcess.stderr.on("data", (data) => {
146
+ output += data.toString();
147
+ });
148
+
149
+ buildProcess.on("close", (code) => {
150
+ this.#isBuilding = false;
151
+ const duration = Date.now() - startTime;
152
+
153
+ if (code === 0) {
154
+ this.#log("success", `Build completed`, `${duration}ms`);
155
+
156
+ if (this.#pendingBuild) {
157
+ this.#pendingBuild = false;
158
+ this.#runBuild().then(resolve);
159
+ }
160
+ else {
161
+ resolve(true);
162
+ }
163
+ }
164
+ else {
165
+ this.#log("error", "Build failed");
166
+ console.log(output);
167
+ resolve(false);
168
+ }
169
+ });
170
+ });
171
+ }
172
+
173
+ // ─────────────────────────────────────────────────────────────────────────
174
+ // Server Process Management
175
+ // ─────────────────────────────────────────────────────────────────────────
176
+
177
+ async #startServer() {
178
+ return new Promise((resolve) => {
179
+ const entryScript = path.join(FRAMEWORK_DIR, "lib/Runtime/Entry.js");
180
+
181
+ this.#serverProcess = spawn(process.execPath, [entryScript], {
182
+ cwd: PROJECT_DIR,
183
+ stdio: ["inherit", "inherit", "inherit"],
184
+ env: { ...process.env, APP_DEV: "true" }
185
+ });
186
+
187
+ this.#serverProcess.on("error", (err) => {
188
+ this.#log("error", `Server error: ${err.message}`);
189
+ });
190
+
191
+ this.#serverProcess.on("close", (code) => {
192
+ if (code !== 0 && code !== null && !this.#pendingRestart) {
193
+ this.#log("error", `Server exited with code ${code}`);
194
+ }
195
+ this.#serverProcess = null;
196
+ });
197
+
198
+ setTimeout(() => resolve(), 500);
199
+ });
200
+ }
201
+
202
+ async #stopServer() {
203
+ if (!this.#serverProcess) return;
204
+
205
+ return new Promise((resolve) => {
206
+
207
+ const forceKillTimer = setTimeout(() => {
208
+ if (this.#serverProcess) {
209
+ this.#serverProcess.kill("SIGKILL");
210
+ }
211
+ resolve();
212
+ }, 5000);
213
+
214
+ this.#serverProcess.once("close", () => {
215
+ clearTimeout(forceKillTimer);
216
+ resolve();
217
+ });
218
+
219
+ this.#serverProcess.kill("SIGTERM");
220
+ });
221
+ }
222
+
223
+ async #restartServer() {
224
+ if (this.#pendingRestart) return;
225
+
226
+ this.#pendingRestart = true;
227
+ await this.#stopServer();
228
+ await this.#startServer();
229
+ this.#pendingRestart = false;
230
+ }
231
+
232
+ #getRelativePath(filePath, baseDir = PROJECT_DIR) {
233
+ return path.relative(baseDir, filePath).replace(/\\/g, "/");
234
+ }
235
+
236
+ #matchesPattern(filePath, patterns, baseDir = PROJECT_DIR) {
237
+ const relativePath = this.#getRelativePath(filePath, baseDir);
238
+
239
+ return patterns.some(pattern => {
240
+ let regexPattern = pattern
241
+ .replace(/\*\*\//g, "<<ANYDIR>>") // **/ = any directory depth
242
+ .replace(/\*\*/g, "<<ANYCHAR>>") // ** = any characters
243
+ .replace(/\*/g, "[^/]*") // * = any characters except /
244
+ .replace(/\//g, "\\/") // escape forward slashes
245
+ .replace(/<<ANYDIR>>/g, "(?:.*\\/)?") // restore **/ placeholder
246
+ .replace(/<<ANYCHAR>>/g, ".*"); // restore ** placeholder
247
+
248
+ const regex = new RegExp(`^${regexPattern}$`);
249
+ return regex.test(relativePath);
250
+ });
251
+ }
252
+
253
+ async #handleFileChange(filePath, eventType) {
254
+ const relativePath = this.#getRelativePath(filePath);
255
+ const frameworkRelativePath = this.#getRelativePath(filePath, FRAMEWORK_DIR);
256
+
257
+ // Check framework files first (lib/View/Templates)
258
+ if (this.#matchesPattern(filePath, WATCH_CONFIG.frameworkBuildOnly, FRAMEWORK_DIR)) {
259
+ this.#log("reload", `Framework file changed: ${frameworkRelativePath}`);
260
+
261
+ clearTimeout(this.#buildDebounceTimer);
262
+ this.#buildDebounceTimer = setTimeout(async () => {
263
+ await this.#runBuild();
264
+ }, 100);
265
+
266
+ return;
267
+ }
268
+
269
+ // Check project build-only files
270
+ if (this.#matchesPattern(filePath, WATCH_CONFIG.buildOnly)) {
271
+ this.#log("reload", `File changed: ${relativePath}`);
272
+ // CSS only needs CSS rebuild
273
+ // TSX needs views + CSS (Tailwind scans TSX for classes)
274
+ const isCss = filePath.endsWith(".css");
275
+ const buildArg = isCss ? "--only=css" : null; // null = full build for TSX
276
+
277
+ clearTimeout(this.#buildDebounceTimer);
278
+ this.#buildDebounceTimer = setTimeout(async () => {
279
+ await this.#runBuild(buildArg);
280
+ }, 100);
281
+
282
+ return;
283
+ }
284
+
285
+ if (this.#matchesPattern(filePath, WATCH_CONFIG.fullRestart)) {
286
+ this.#log("reload", `File changed: ${relativePath}`, "full restart");
287
+
288
+ clearTimeout(this.#restartDebounceTimer);
289
+ this.#restartDebounceTimer = setTimeout(async () => {
290
+ await this.#runBuild();
291
+ await this.#restartServer();
292
+ }, 200);
293
+
294
+ return;
295
+ }
296
+ }
297
+
298
+ #setupWatcher() {
299
+ const watchPaths = [
300
+ path.join(PROJECT_DIR, "resources/views"),
301
+ path.join(PROJECT_DIR, "resources/css"),
302
+ path.join(FRAMEWORK_DIR, "lib/View/Templates"),
303
+ path.join(PROJECT_DIR, "config"),
304
+ path.join(PROJECT_DIR, "app"),
305
+ path.join(PROJECT_DIR, "routes"),
306
+ path.join(PROJECT_DIR, "app.js")
307
+ ];
308
+
309
+ const watcher = chokidar.watch(watchPaths, {
310
+ ignored: [
311
+ /node_modules/,
312
+ /\.git/,
313
+ /build[\\\/]/,
314
+ /storage[\\\/]app[\\\/]public/,
315
+ /storage[\\\/]framework/,
316
+ /storage[\\\/]logs/,
317
+ /\.nitron/,
318
+ /package-lock\.json$/
319
+ ],
320
+ persistent: true,
321
+ ignoreInitial: true,
322
+ usePolling: false,
323
+ awaitWriteFinish: {
324
+ stabilityThreshold: 100,
325
+ pollInterval: 50
326
+ }
327
+ });
328
+
329
+ watcher.on("change", (filePath) => this.#handleFileChange(filePath, "change"));
330
+ watcher.on("add", (filePath) => this.#handleFileChange(filePath, "add"));
331
+ watcher.on("unlink", (filePath) => this.#handleFileChange(filePath, "unlink"));
332
+
333
+ watcher.on("ready", () => {
334
+ if (!this.#watcherReady) {
335
+ this.#watcherReady = true;
336
+ this.#log("watch", "Watching for file changes...");
337
+ }
338
+ });
339
+
340
+ watcher.on("error", (error) => {
341
+ this.#log("error", `Watcher error: ${error.message}`);
342
+ });
343
+
344
+ return watcher;
345
+ }
346
+
347
+ async start() {
348
+ // Initial build
349
+ const buildSuccess = await this.#runBuild();
350
+ if (!buildSuccess) {
351
+ this.#log("error", "Initial build failed. Watching for changes...");
352
+ }
353
+
354
+ // Start server
355
+ await this.#startServer();
356
+
357
+ // Setup file watcher
358
+ const watcher = this.#setupWatcher();
359
+
360
+ // Graceful shutdown
361
+ const cleanup = async () => {
362
+ console.log();
363
+ watcher.close();
364
+ await this.#stopServer();
365
+ process.exit(0);
366
+ };
367
+
368
+ process.on("SIGINT", cleanup);
369
+ process.on("SIGTERM", cleanup);
370
+ }
371
+ }
372
+
373
+ export default async function Dev() {
374
+ const devServer = new DevServer();
375
+ await devServer.start();
376
+ }
377
+
378
+ // Auto-run when called directly
379
+ const isMain = process.argv[1]?.endsWith("DevCommand.js");
380
+ if (isMain) {
381
+ Dev().catch(err => {
382
+ console.error(err);
383
+ process.exit(1);
384
+ });
385
+ }
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import Paths from '../../Core/Paths.js';
5
+
6
+ const baseDirs = {
7
+ controller: 'app/Controllers',
8
+ middleware: 'app/Middlewares',
9
+ model: 'app/Models',
10
+ migration: 'database/migrations',
11
+ seeder: 'database/seeders',
12
+ };
13
+
14
+ const templates = {
15
+ controller: 'controller.js',
16
+ middleware: 'middleware.js',
17
+ model: 'model.js',
18
+ migration: 'migration.js',
19
+ seeder: 'seeder.js',
20
+ };
21
+
22
+ function toTableName(className) {
23
+ const name = className.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
24
+ return pluralize(name);
25
+ }
26
+
27
+ function pluralize(word) {
28
+ if (/[^aeiou]y$/.test(word)) {
29
+ return word.slice(0, -1) + 'ies';
30
+ }
31
+
32
+ if (/(s|x|z|ch|sh)$/.test(word)) {
33
+ return word + 'es';
34
+ }
35
+
36
+ if (/fe?$/.test(word)) {
37
+ return word.replace(/fe?$/, 'ves');
38
+ }
39
+
40
+ return word.endsWith('s') ? word : word + 's';
41
+ }
42
+
43
+ function toPascalCase(str) {
44
+ return str
45
+ .split(/[-_]/)
46
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
47
+ .join('');
48
+ }
49
+
50
+ export default async function make(type, rawName) {
51
+ if (!type || !rawName) {
52
+ console.error("\x1b[31m%s\x1b[0m", "Usage: njs make:<type> <Name/Path> (e.g., njs make:controller Admin/HomeController)");
53
+ return false;
54
+ }
55
+
56
+ const parts = rawName.split('/');
57
+ const subDirs = parts.slice(0, -1).join('/');
58
+ const outputDir = path.join(Paths.project, baseDirs[type]);
59
+
60
+ let className = parts[parts.length - 1];
61
+ let fileName = `${className}.js`;
62
+
63
+ if (type === 'migration') {
64
+ const now = new Date();
65
+ const pad = (n) => n.toString().padStart(2, '0');
66
+
67
+ const year = now.getFullYear();
68
+ const month = pad(now.getMonth() + 1);
69
+ const day = pad(now.getDate());
70
+ const hour = pad(now.getHours());
71
+ const minute = pad(now.getMinutes());
72
+
73
+ const timestamp = `${year}_${month}_${day}_${hour}_${minute}`;
74
+ fileName = `${timestamp}_${className}.js`;
75
+ }
76
+
77
+ const fullDir = path.join(outputDir, subDirs);
78
+ const fullPath = path.join(fullDir, fileName);
79
+
80
+ const templatePath = path.join(Paths.frameworkTemplates, templates[type]);
81
+ let template = fs.readFileSync(templatePath, 'utf-8');
82
+
83
+ if (type === "model") {
84
+ template = template.replace(/__CLASS__/g, className).replace(/__TABLE__/g, toTableName(className));
85
+ }
86
+ else if (type === "migration") {
87
+ template = template.replace(/__CLASS__/g, toPascalCase(className)).replace(/__TABLE__/g, className);
88
+ }
89
+ else {
90
+ template = template.replace(/__CLASS__/g, className);
91
+ }
92
+
93
+ fs.mkdirSync(fullDir, { recursive: true });
94
+ fs.writeFileSync(fullPath, template);
95
+
96
+ console.log("\x1b[32m%s\x1b[0m", `${type[0].toUpperCase() + type.slice(1)} successfully created: ${fullPath}`);
97
+ return true;
98
+ }
99
+
100
+ // Auto-run when called directly
101
+ const isMain = process.argv[1]?.endsWith("MakeCommand.js");
102
+ if (isMain) {
103
+ const [, , type, rawName] = process.argv;
104
+ make(type, rawName)
105
+ .then(success => process.exit(success ? 0 : 1))
106
+ .catch(err => {
107
+ console.error(err);
108
+ process.exit(1);
109
+ });
110
+ }
@@ -0,0 +1,98 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { pathToFileURL } from 'url';
4
+ import dotenv from 'dotenv';
5
+ import DB from '../../Database/DB.js';
6
+ import Paths from '../../Core/Paths.js';
7
+ import Config from '../../Core/Config.js';
8
+
9
+ class Migrate {
10
+ #files;
11
+ #shouldSeed;
12
+
13
+ constructor(files = null, shouldSeed = false) {
14
+ const migrationsDir = Paths.migrations;
15
+ this.#files = files || fs.readdirSync(migrationsDir);
16
+ this.#shouldSeed = shouldSeed;
17
+ }
18
+
19
+ async run() {
20
+ dotenv.config({ quiet: true });
21
+ await Config.initialize();
22
+
23
+ try {
24
+ await DB.setup();
25
+ } catch (error) {
26
+ console.error('❌ Database setup failed:', error.message);
27
+ console.error('Check your .env file and ensure the database exists and is accessible');
28
+ return false;
29
+ }
30
+
31
+ const migrationsDir = Paths.migrations;
32
+ for (const file of this.#files) {
33
+ const filePath = Paths.migrationUrl(file);
34
+ const module = await import(filePath);
35
+
36
+ console.log(`📦 [\x1b[36m${module.default.name}\x1b[0m] Creating table...`);
37
+ try {
38
+ await module.default.up();
39
+ console.log(`✅ [\x1b[36m${module.default.name}\x1b[0m] Table created successfully\n`);
40
+ } catch (error) {
41
+ if (this.#isTableExistsError(error)) {
42
+ console.log(`⚠️ [\x1b[36m${module.default.name}\x1b[0m] Table already exists, skipping\n`);
43
+ continue;
44
+ }
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ if (this.#shouldSeed) {
50
+ await this.#runSeeders();
51
+ }
52
+
53
+ await DB.close();
54
+ return true;
55
+ }
56
+
57
+ async #runSeeders() {
58
+ const seedersDir = Paths.seeders;
59
+ const seedFiles = fs.readdirSync(seedersDir);
60
+
61
+ for (const file of seedFiles) {
62
+ const filePath = pathToFileURL(path.join(seedersDir, file)).href;
63
+ const seeder = await import(filePath);
64
+ console.log(`🌱 [\x1b[36m${seeder.default.name}\x1b[0m] Seeding...`);
65
+ await seeder.default.run();
66
+ console.log(`✅ [\x1b[36m${seeder.default.name}\x1b[0m] Seeding completed\n`);
67
+ }
68
+ }
69
+
70
+ #isTableExistsError(error) {
71
+ const message = String(error?.message || "").toLowerCase();
72
+
73
+ return message.includes("already exists") ||
74
+ message.includes("table exists") ||
75
+ message.includes("errno 1050") ||
76
+ message.includes("er_table_exists_error") ||
77
+ message.includes("duplicate table");
78
+ }
79
+ }
80
+
81
+ export default async function migrate(shouldSeed = false, files = null) {
82
+ const instance = new Migrate(files, shouldSeed);
83
+ return instance.run();
84
+ }
85
+
86
+ // Auto-run when called directly
87
+ const isMain = process.argv[1]?.endsWith("MigrateCommand.js");
88
+ if (isMain) {
89
+ const args = process.argv.slice(2);
90
+ const shouldSeed = args.includes("--seed");
91
+ const files = args.filter(arg => arg !== "--seed");
92
+ migrate(shouldSeed, files.length > 0 ? files : null)
93
+ .then(success => process.exit(success ? 0 : 1))
94
+ .catch(err => {
95
+ console.error(err);
96
+ process.exit(1);
97
+ });
98
+ }