@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.
- package/README.md +429 -0
- package/cli/create.js +260 -0
- package/cli/njs.js +164 -0
- package/lib/Auth/Manager.js +111 -0
- package/lib/Build/Manager.js +1232 -0
- package/lib/Console/Commands/BuildCommand.js +25 -0
- package/lib/Console/Commands/DevCommand.js +385 -0
- package/lib/Console/Commands/MakeCommand.js +110 -0
- package/lib/Console/Commands/MigrateCommand.js +98 -0
- package/lib/Console/Commands/MigrateFreshCommand.js +97 -0
- package/lib/Console/Commands/SeedCommand.js +92 -0
- package/lib/Console/Commands/StorageLinkCommand.js +31 -0
- package/lib/Console/Stubs/controller.js +19 -0
- package/lib/Console/Stubs/middleware.js +9 -0
- package/lib/Console/Stubs/migration.js +23 -0
- package/lib/Console/Stubs/model.js +7 -0
- package/lib/Console/Stubs/page-hydration.tsx +54 -0
- package/lib/Console/Stubs/seeder.js +9 -0
- package/lib/Console/Stubs/vendor.tsx +11 -0
- package/lib/Core/Config.js +86 -0
- package/lib/Core/Environment.js +21 -0
- package/lib/Core/Paths.js +188 -0
- package/lib/Database/Connection.js +61 -0
- package/lib/Database/DB.js +84 -0
- package/lib/Database/Drivers/MySQLDriver.js +234 -0
- package/lib/Database/Manager.js +162 -0
- package/lib/Database/Model.js +161 -0
- package/lib/Database/QueryBuilder.js +714 -0
- package/lib/Database/QueryValidation.js +62 -0
- package/lib/Database/Schema/Blueprint.js +126 -0
- package/lib/Database/Schema/Manager.js +116 -0
- package/lib/Date/DateTime.js +108 -0
- package/lib/Date/Locale.js +68 -0
- package/lib/Encryption/Manager.js +47 -0
- package/lib/Filesystem/Manager.js +49 -0
- package/lib/Hashing/Manager.js +25 -0
- package/lib/Http/Server.js +317 -0
- package/lib/Logging/Manager.js +153 -0
- package/lib/Mail/Manager.js +120 -0
- package/lib/Route/Loader.js +81 -0
- package/lib/Route/Manager.js +265 -0
- package/lib/Runtime/Entry.js +11 -0
- package/lib/Session/File.js +299 -0
- package/lib/Session/Manager.js +259 -0
- package/lib/Session/Memory.js +67 -0
- package/lib/Session/Session.js +196 -0
- package/lib/Support/Str.js +100 -0
- package/lib/Translation/Manager.js +49 -0
- package/lib/Validation/MimeTypes.js +39 -0
- package/lib/Validation/Validator.js +691 -0
- package/lib/View/Manager.js +544 -0
- package/lib/View/Templates/default/Home.tsx +262 -0
- package/lib/View/Templates/default/MainLayout.tsx +44 -0
- package/lib/View/Templates/errors/404.tsx +13 -0
- package/lib/View/Templates/errors/500.tsx +13 -0
- package/lib/View/Templates/errors/ErrorLayout.tsx +112 -0
- package/lib/View/Templates/messages/Maintenance.tsx +17 -0
- package/lib/View/Templates/messages/MessageLayout.tsx +136 -0
- package/lib/index.js +57 -0
- package/package.json +47 -0
- package/skeleton/.env.example +26 -0
- package/skeleton/app/Controllers/HomeController.js +9 -0
- package/skeleton/app/Kernel.js +11 -0
- package/skeleton/app/Middlewares/Authentication.js +9 -0
- package/skeleton/app/Middlewares/Guest.js +9 -0
- package/skeleton/app/Middlewares/VerifyCsrf.js +24 -0
- package/skeleton/app/Models/User.js +7 -0
- package/skeleton/config/app.js +4 -0
- package/skeleton/config/auth.js +16 -0
- package/skeleton/config/database.js +27 -0
- package/skeleton/config/hash.js +3 -0
- package/skeleton/config/server.js +28 -0
- package/skeleton/config/session.js +21 -0
- package/skeleton/database/migrations/2025_01_01_00_00_users.js +20 -0
- package/skeleton/database/seeders/UserSeeder.js +15 -0
- package/skeleton/globals.d.ts +1 -0
- package/skeleton/package.json +24 -0
- package/skeleton/public/.gitkeep +0 -0
- package/skeleton/resources/css/.gitkeep +0 -0
- package/skeleton/resources/langs/.gitkeep +0 -0
- package/skeleton/resources/views/Site/Home.tsx +66 -0
- package/skeleton/routes/web.js +4 -0
- package/skeleton/storage/app/private/.gitkeep +0 -0
- package/skeleton/storage/app/public/.gitkeep +0 -0
- package/skeleton/storage/framework/sessions/.gitkeep +0 -0
- package/skeleton/storage/logs/.gitkeep +0 -0
- 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
|
+
}
|