@nitronjs/framework 0.3.3 → 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", "resources/views/**/*.ts"],
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
+ }