@nitronjs/framework 0.3.2 → 0.3.4

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.
@@ -1,258 +1,301 @@
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 fs from "fs";
7
- import Paths from "../../Core/Paths.js";
8
- import Environment from "../../Core/Environment.js";
9
- import Builder from "../../Build/Manager.js";
10
- import Layout from "../../View/Layout.js";
11
-
12
- dotenv.config({ quiet: true });
13
- Environment.setDev(true);
14
-
15
- const C = { r: "\x1b[0m", d: "\x1b[2m", bold: "\x1b[1m", red: "\x1b[31m", g: "\x1b[32m", y: "\x1b[33m", b: "\x1b[34m", m: "\x1b[35m", c: "\x1b[36m" };
16
-
17
- // Using consistent-width characters for alignment
18
- const ICONS = {
19
- info: `${C.b}i${C.r}`,
20
- ok: `${C.g}✓${C.r}`,
21
- err: `${C.red}✗${C.r}`,
22
- build: `${C.y}○${C.r}`,
23
- hmr: `${C.m}●${C.r}`,
24
- watch: `${C.c}◐${C.r}`
25
- };
26
-
27
- const PATTERNS = {
28
- views: ["resources/views/**/*.tsx"],
29
- css: ["resources/css/**/*.css"],
30
- restart: ["config/**/*.js", "app/Kernel.js", "app/Models/**/*.js", "routes/**/*.js", "app.js", "app/Controllers/**/*.js", "app/Middlewares/**/*.js"],
31
- framework: ["lib/View/Templates/**/*.tsx"]
32
- };
33
-
34
- class DevServer {
35
- #proc = null;
36
- #building = false;
37
- #builder = new Builder();
38
- #debounce = { build: null, restart: null };
39
- #banner = null;
40
-
41
- #log(icon, msg, extra) {
42
- const time = new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
43
- const iconStr = ICONS[icon] || ICONS.info;
44
- const extraStr = extra ? ` ${C.d}(${extra})${C.r}` : "";
45
- console.log(`${C.d}${time}${C.r} ${iconStr} ${msg}${extraStr}`);
46
- }
47
-
48
- async #build(only = null) {
49
- if (this.#building) return { success: false, skipped: true };
50
- this.#building = true;
51
- this.#log("build", only ? `Building ${only}...` : "Building...");
52
-
53
- try {
54
- const result = await this.#builder.run(only, true, true);
55
- this.#building = false;
56
-
57
- if (result.success) {
58
- this.#log("ok", "Build completed", `${result.time}ms`);
59
- } else {
60
- this.#log("err", "Build failed");
61
- if (result.error) this.#send("error", { message: result.error });
62
- }
63
- return result;
64
- } catch (e) {
65
- this.#building = false;
66
- this.#log("err", `Build error: ${e.message}`);
67
- this.#send("error", { message: e.message });
68
- return { success: false, error: e.message };
69
- }
70
- }
71
-
72
- async #start() {
73
- return new Promise(resolve => {
74
- this.#proc = spawn(process.execPath, [path.join(Paths.framework, "lib/Runtime/Entry.js")], {
75
- cwd: process.cwd(),
76
- stdio: ["inherit", "inherit", "inherit", "ipc"],
77
- env: { ...process.env, __NITRON_DEV__: "true" }
78
- });
79
- this.#proc.on("error", e => this.#log("err", e.message));
80
- this.#proc.on("close", code => {
81
- if (code && code !== 0) this.#log("err", `Exit code ${code}`);
82
- this.#proc = null;
83
- });
84
- this.#proc.on("message", msg => {
85
- if (msg?.type === "banner") this.#banner = msg.text;
86
- if (msg?.type === "ready") resolve();
87
- });
88
- setTimeout(resolve, 5000);
89
- });
90
- }
91
-
92
- #send(type, data) {
93
- if (this.#proc?.connected) {
94
- this.#proc.send({ type, ...data });
95
- }
96
- }
97
-
98
- async #stop() {
99
- if (!this.#proc) return;
100
- return new Promise(resolve => {
101
- const timeout = setTimeout(() => { this.#proc?.kill("SIGKILL"); resolve(); }, 5000);
102
- this.#proc.once("close", () => { clearTimeout(timeout); resolve(); });
103
- this.#proc.kill("SIGTERM");
104
- });
105
- }
106
-
107
- #rel(p, base = Paths.project) {
108
- return path.relative(base, p).replace(/\\/g, "/");
109
- }
110
-
111
- #match(p, patterns, base = Paths.project) {
112
- const rel = this.#rel(p, base);
113
- return patterns.some(pat => {
114
- if (pat.includes("**")) {
115
- const [prefix, suffix] = pat.split("**");
116
- const dir = prefix.replace(/\/$/, "");
117
- const ext = suffix.replace(/^\/?\*/, "");
118
- return rel.startsWith(dir) && rel.endsWith(ext);
119
- }
120
- return rel === pat;
121
- });
122
- }
123
-
124
- async #onChange(filePath) {
125
- const rel = this.#rel(filePath);
126
-
127
- if (this.#match(filePath, PATTERNS.framework, Paths.framework)) {
128
- this.#log("build", `Framework: ${this.#rel(filePath, Paths.framework)}`);
129
- clearTimeout(this.#debounce.build);
130
- this.#debounce.build = setTimeout(async () => {
131
- const r = await this.#build();
132
- if (r.success) this.#send("reload", { reason: "Framework template changed" });
133
- }, 100);
134
- return;
135
- }
136
-
137
- if (this.#match(filePath, PATTERNS.css)) {
138
- this.#log("hmr", rel);
139
- clearTimeout(this.#debounce.build);
140
- this.#debounce.build = setTimeout(async () => {
141
- const r = await this.#build("css");
142
- if (r.success) this.#send("change", { changeType: "css", file: rel });
143
- }, 100);
144
- return;
145
- }
146
-
147
- if (this.#match(filePath, PATTERNS.views)) {
148
- this.#log("hmr", rel);
149
- clearTimeout(this.#debounce.build);
150
- this.#debounce.build = setTimeout(async () => {
151
- const r = await this.#build();
152
- if (r.success) {
153
- // When client component JS chunks are rebuilt, the browser
154
- // still has the old JS modules loaded in memory. RSC refetch
155
- // only updates the React tree data, not the actual JS code.
156
- // A full page reload is needed to load the new JS chunks.
157
- if (r.viewsChanged) {
158
- this.#send("reload", { reason: "Client component changed" });
159
- }
160
- else {
161
- const changeType = detectChangeType(filePath);
162
- this.#send("change", {
163
- changeType,
164
- cssChanged: r.cssChanged || false,
165
- file: rel
166
- });
167
- }
168
- }
169
- }, 100);
170
- return;
171
- }
172
-
173
- if (this.#match(filePath, PATTERNS.restart)) {
174
- this.#log("build", rel, "restart required");
175
- clearTimeout(this.#debounce.restart);
176
- this.#debounce.restart = setTimeout(async () => {
177
- await this.#build();
178
- await this.#stop();
179
- await this.#start();
180
- this.#send("reload", { reason: "Server restarted" });
181
- }, 200);
182
- return;
183
- }
184
-
185
- this.#log("info", `Unmatched: ${rel}`);
186
- }
187
-
188
- async #watch() {
189
- const candidates = [
190
- path.join(Paths.project, "resources/views"),
191
- path.join(Paths.project, "resources/css"),
192
- path.join(Paths.project, "config"),
193
- path.join(Paths.project, "app"),
194
- path.join(Paths.project, "routes"),
195
- path.join(Paths.framework, "lib/View/Templates")
196
- ];
197
-
198
- const watchPaths = candidates.filter(p => fs.existsSync(p));
199
-
200
- if (!watchPaths.length) {
201
- this.#log("err", "No watch paths found");
202
- return { close: () => {} };
203
- }
204
-
205
- const watcher = chokidar.watch(watchPaths, {
206
- ignored: [/node_modules/, /\.git/, /build[\\/]/, /storage[\\/]/, /\.nitron/],
207
- persistent: true,
208
- ignoreInitial: true,
209
- usePolling: true,
210
- interval: 100,
211
- binaryInterval: 100,
212
- awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 20 }
213
- });
214
-
215
- watcher.on("change", p => this.#onChange(p));
216
- watcher.on("add", p => this.#onChange(p));
217
- watcher.on("error", e => this.#log("err", `Watch error: ${e.message}`));
218
-
219
- return new Promise(resolve => {
220
- watcher.on("ready", () => {
221
- this.#log("watch", `Watching ${watchPaths.length} directories`);
222
- resolve(watcher);
223
- });
224
- });
225
- }
226
-
227
- async start() {
228
- await this.#build();
229
- await this.#start();
230
- const watcher = await this.#watch();
231
-
232
- if (this.#banner) console.log(this.#banner);
233
-
234
- const exit = async () => {
235
- console.log();
236
- await watcher.close();
237
- await this.#stop();
238
- process.exitCode = 0;
239
- };
240
-
241
- process.on("SIGINT", exit);
242
- process.on("SIGTERM", exit);
243
- }
244
- }
245
-
246
- function detectChangeType(filePath) {
247
- if (Layout.isLayout(filePath)) return "layout";
248
-
249
- return "page";
250
- }
251
-
252
- export default async function Dev() {
253
- await new DevServer().start();
254
- }
255
-
256
- if (process.argv[1]?.endsWith("DevCommand.js")) {
257
- Dev().catch(e => { console.error(e); process.exitCode = 1; });
258
- }
1
+ #!/usr/bin/env node
2
+ import dotenv from "dotenv";
3
+ import { spawn } from "child_process";
4
+ import watcher from "@parcel/watcher";
5
+ import path from "path";
6
+ import fs from "fs";
7
+ import Paths from "../../Core/Paths.js";
8
+ import Environment from "../../Core/Environment.js";
9
+ import Builder from "../../Build/Manager.js";
10
+ import Layout from "../../View/Layout.js";
11
+
12
+ dotenv.config({ quiet: true });
13
+ Environment.setDev(true);
14
+
15
+ const C = { r: "\x1b[0m", d: "\x1b[2m", bold: "\x1b[1m", red: "\x1b[31m", g: "\x1b[32m", y: "\x1b[33m", b: "\x1b[34m", m: "\x1b[35m", c: "\x1b[36m" };
16
+
17
+ const ICONS = {
18
+ info: `${C.b}i${C.r}`,
19
+ ok: `${C.g}✓${C.r}`,
20
+ err: `${C.red}✗${C.r}`,
21
+ build: `${C.y}○${C.r}`,
22
+ hmr: `${C.m}●${C.r}`,
23
+ watch: `${C.c}◐${C.r}`
24
+ };
25
+
26
+ const PATTERNS = {
27
+ views: ["resources/views/**/*.tsx", "resources/views/**/*.ts"],
28
+ css: ["resources/css/**/*.css"],
29
+ restart: ["config/**/*.js", "app/Kernel.js", "app/Models/**/*.js", "routes/**/*.js", "app.js", "app/Controllers/**/*.js", "app/Middlewares/**/*.js"],
30
+ framework: ["lib/View/Templates/**/*.tsx"]
31
+ };
32
+
33
+ const IGNORE_PATTERN = /node_modules|\.git|build[/\\]|storage[/\\]|\.nitron/;
34
+
35
+ class DevServer {
36
+ #proc = null;
37
+ #building = false;
38
+ #builder = new Builder();
39
+ #debounce = { build: null, restart: null };
40
+ #banner = null;
41
+ #subscriptions = [];
42
+ #moduleGraph = null;
43
+
44
+ #log(icon, msg, extra) {
45
+ const time = new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
46
+ const iconStr = ICONS[icon] || ICONS.info;
47
+ const extraStr = extra ? ` ${C.d}(${extra})${C.r}` : "";
48
+ console.log(`${C.d}${time}${C.r} ${iconStr} ${msg}${extraStr}`);
49
+ }
50
+
51
+ async #build(only = null) {
52
+ if (this.#building) return { success: false, skipped: true };
53
+ this.#building = true;
54
+ this.#log("build", only ? `Building ${only}...` : "Building...");
55
+
56
+ try {
57
+ const result = await this.#builder.run(only, true, true);
58
+ this.#building = false;
59
+
60
+ if (result.success) {
61
+ this.#log("ok", "Build completed", `${result.time}ms`);
62
+
63
+ if (result.moduleGraph) {
64
+ this.#moduleGraph = result.moduleGraph;
65
+ }
66
+ }
67
+ else {
68
+ this.#log("err", "Build failed");
69
+ if (result.error) this.#send("error", { message: result.error });
70
+ }
71
+
72
+ return result;
73
+ }
74
+ catch (e) {
75
+ this.#building = false;
76
+ this.#log("err", `Build error: ${e.message}`);
77
+ this.#send("error", { message: e.message });
78
+ return { success: false, error: e.message };
79
+ }
80
+ }
81
+
82
+ async #start() {
83
+ return new Promise(resolve => {
84
+ this.#proc = spawn(process.execPath, [path.join(Paths.framework, "lib/Runtime/Entry.js")], {
85
+ cwd: process.cwd(),
86
+ stdio: ["inherit", "inherit", "inherit", "ipc"],
87
+ env: { ...process.env, __NITRON_DEV__: "true" }
88
+ });
89
+
90
+ this.#proc.on("error", e => this.#log("err", e.message));
91
+
92
+ this.#proc.on("close", code => {
93
+ if (code && code !== 0) this.#log("err", `Exit code ${code}`);
94
+ this.#proc = null;
95
+ });
96
+
97
+ this.#proc.on("message", msg => {
98
+ if (msg?.type === "banner") this.#banner = msg.text;
99
+ if (msg?.type === "ready") resolve();
100
+ });
101
+
102
+ setTimeout(resolve, 5000);
103
+ });
104
+ }
105
+
106
+ #send(type, data) {
107
+ if (this.#proc?.connected) {
108
+ this.#proc.send({ type, ...data });
109
+ }
110
+ }
111
+
112
+ async #stop() {
113
+ if (!this.#proc) return;
114
+
115
+ return new Promise(resolve => {
116
+ const timeout = setTimeout(() => { this.#proc?.kill("SIGKILL"); resolve(); }, 5000);
117
+ this.#proc.once("close", () => { clearTimeout(timeout); resolve(); });
118
+ this.#proc.kill("SIGTERM");
119
+ });
120
+ }
121
+
122
+ #rel(p, base = Paths.project) {
123
+ return path.relative(base, p).replace(/\\/g, "/");
124
+ }
125
+
126
+ #match(p, patterns, base = Paths.project) {
127
+ const rel = this.#rel(p, base);
128
+
129
+ return patterns.some(pat => {
130
+ if (pat.includes("**")) {
131
+ const [prefix, suffix] = pat.split("**");
132
+ const dir = prefix.replace(/\/$/, "");
133
+ const ext = suffix.replace(/^\/?\*/, "");
134
+ return rel.startsWith(dir) && rel.endsWith(ext);
135
+ }
136
+
137
+ return rel === pat;
138
+ });
139
+ }
140
+
141
+ async #onChange(filePath) {
142
+ const rel = this.#rel(filePath);
143
+
144
+ if (this.#match(filePath, PATTERNS.framework, Paths.framework)) {
145
+ this.#log("build", `Framework: ${this.#rel(filePath, Paths.framework)}`);
146
+ clearTimeout(this.#debounce.build);
147
+ this.#debounce.build = setTimeout(async () => {
148
+ const r = await this.#build();
149
+ if (r.success) this.#send("reload", { reason: "Framework template changed" });
150
+ }, 50);
151
+ return;
152
+ }
153
+
154
+ if (this.#match(filePath, PATTERNS.css)) {
155
+ this.#log("hmr", rel);
156
+ clearTimeout(this.#debounce.build);
157
+ this.#debounce.build = setTimeout(async () => {
158
+ const r = await this.#build("css");
159
+ if (r.success) this.#send("change", { changeType: "css", file: rel });
160
+ }, 50);
161
+ return;
162
+ }
163
+
164
+ if (this.#match(filePath, PATTERNS.views)) {
165
+ this.#log("hmr", rel);
166
+ clearTimeout(this.#debounce.build);
167
+ this.#debounce.build = setTimeout(async () => {
168
+ const r = await this.#build();
169
+
170
+ if (!r.success) return;
171
+
172
+ if (this.#moduleGraph && r.viewsChanged) {
173
+ const boundaries = this.#moduleGraph.getAffectedBoundaries(filePath);
174
+ const clientBoundaries = [...boundaries].filter(b => this.#moduleGraph.isClientComponent(b));
175
+
176
+ if (clientBoundaries.length > 0 && r.rebuiltChunks?.length) {
177
+ this.#send("fast-refresh", {
178
+ chunks: r.rebuiltChunks,
179
+ cssChanged: r.cssChanged || false,
180
+ timestamp: Date.now()
181
+ });
182
+ }
183
+ else {
184
+ this.#send("change", {
185
+ changeType: detectChangeType(filePath),
186
+ cssChanged: r.cssChanged || false,
187
+ file: rel
188
+ });
189
+ }
190
+ }
191
+ else if (r.viewsChanged && r.rebuiltChunks?.length) {
192
+ this.#send("fast-refresh", {
193
+ chunks: r.rebuiltChunks,
194
+ cssChanged: r.cssChanged || false,
195
+ timestamp: Date.now()
196
+ });
197
+ }
198
+ else if (r.viewsChanged) {
199
+ this.#send("reload", { reason: "Client component changed" });
200
+ }
201
+ else {
202
+ this.#send("change", {
203
+ changeType: detectChangeType(filePath),
204
+ cssChanged: r.cssChanged || false,
205
+ file: rel
206
+ });
207
+ }
208
+ }, 50);
209
+ return;
210
+ }
211
+
212
+ if (this.#match(filePath, PATTERNS.restart)) {
213
+ this.#log("build", rel, "restart required");
214
+ clearTimeout(this.#debounce.restart);
215
+ this.#debounce.restart = setTimeout(async () => {
216
+ await this.#build();
217
+ await this.#stop();
218
+ await this.#start();
219
+ this.#send("reload", { reason: "Server restarted" });
220
+ }, 100);
221
+ return;
222
+ }
223
+
224
+ this.#log("info", `Unmatched: ${rel}`);
225
+ }
226
+
227
+ async #watch() {
228
+ const candidates = [
229
+ path.join(Paths.project, "resources/views"),
230
+ path.join(Paths.project, "resources/css"),
231
+ path.join(Paths.project, "config"),
232
+ path.join(Paths.project, "app"),
233
+ path.join(Paths.project, "routes"),
234
+ path.join(Paths.framework, "lib/View/Templates")
235
+ ];
236
+
237
+ const watchPaths = candidates.filter(p => fs.existsSync(p));
238
+
239
+ if (!watchPaths.length) {
240
+ this.#log("err", "No watch paths found");
241
+ return;
242
+ }
243
+
244
+ for (const dir of watchPaths) {
245
+ const sub = await watcher.subscribe(dir, (err, events) => {
246
+ if (err) {
247
+ this.#log("err", `Watch error: ${err.message}`);
248
+ return;
249
+ }
250
+
251
+ for (const event of events) {
252
+ if (IGNORE_PATTERN.test(event.path)) continue;
253
+ if (event.type === "delete") continue;
254
+
255
+ this.#onChange(event.path);
256
+ }
257
+ });
258
+
259
+ this.#subscriptions.push(sub);
260
+ }
261
+
262
+ this.#log("watch", `Watching ${watchPaths.length} directories`);
263
+ }
264
+
265
+ async start() {
266
+ await this.#build();
267
+ await this.#start();
268
+ await this.#watch();
269
+
270
+ if (this.#banner) console.log(this.#banner);
271
+
272
+ const exit = async () => {
273
+ console.log();
274
+
275
+ for (const sub of this.#subscriptions) {
276
+ await sub.unsubscribe();
277
+ }
278
+
279
+ await this.#builder.dispose();
280
+ await this.#stop();
281
+ process.exitCode = 0;
282
+ };
283
+
284
+ process.on("SIGINT", exit);
285
+ process.on("SIGTERM", exit);
286
+ }
287
+ }
288
+
289
+ function detectChangeType(filePath) {
290
+ if (Layout.isLayout(filePath)) return "layout";
291
+
292
+ return "page";
293
+ }
294
+
295
+ export default async function Dev() {
296
+ await new DevServer().start();
297
+ }
298
+
299
+ if (process.argv[1]?.endsWith("DevCommand.js")) {
300
+ Dev().catch(e => { console.error(e); process.exitCode = 1; });
301
+ }
@@ -111,7 +111,18 @@ class DateTime {
111
111
  date.setHours(date.getHours() + hours);
112
112
  return date.toISOString().slice(0, 19).replace('T', ' ');
113
113
  }
114
-
114
+
115
+ /**
116
+ * Add minutes to current date
117
+ * @param {number} minutes - Number of minutes to add
118
+ * @returns {string} SQL formatted datetime
119
+ */
120
+ static addMinutes(minutes) {
121
+ const date = this.#getDate();
122
+ date.setMinutes(date.getMinutes() + minutes);
123
+ return date.toISOString().slice(0, 19).replace('T', ' ');
124
+ }
125
+
115
126
  /**
116
127
  * Subtract days from current date
117
128
  * @param {number} days - Number of days to subtract
@@ -120,7 +131,7 @@ class DateTime {
120
131
  static subDays(days) {
121
132
  return this.addDays(-days);
122
133
  }
123
-
134
+
124
135
  /**
125
136
  * Subtract hours from current date
126
137
  * @param {number} hours - Number of hours to subtract
@@ -129,6 +140,15 @@ class DateTime {
129
140
  static subHours(hours) {
130
141
  return this.addHours(-hours);
131
142
  }
143
+
144
+ /**
145
+ * Subtract minutes from current date
146
+ * @param {number} minutes - Number of minutes to subtract
147
+ * @returns {string} SQL formatted datetime
148
+ */
149
+ static subMinutes(minutes) {
150
+ return this.addMinutes(-minutes);
151
+ }
132
152
  }
133
153
 
134
154
  export default DateTime;
@@ -29,7 +29,8 @@ class DevIndicator {
29
29
  const route = devData?.route;
30
30
  const timing = devData ? buildTimingSummary(devData) : null;
31
31
  const mwCount = devData?.middlewareTiming?.length || 0;
32
- const propsInfo = devData?.rawProps ? buildPropsSummary(devData) : null;
32
+ const hasProps = devData?.rawProps && Object.keys(devData.rawProps).length > 0;
33
+ const propsInfo = hasProps ? buildPropsSummary(devData) : null;
33
34
 
34
35
  const totalMs = timing ? parseFloat(timing.total) : 0;
35
36
  const timingWarnClass = totalMs >= 300 ? " ndi-critical" : totalMs >= 100 ? " ndi-warn" : "";