@nitronjs/framework 0.1.23 → 0.2.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 (37) hide show
  1. package/lib/Build/CssBuilder.js +129 -0
  2. package/lib/Build/FileAnalyzer.js +395 -0
  3. package/lib/Build/HydrationBuilder.js +173 -0
  4. package/lib/Build/Manager.js +290 -936
  5. package/lib/Build/colors.js +10 -0
  6. package/lib/Build/jsxRuntime.js +116 -0
  7. package/lib/Build/plugins.js +264 -0
  8. package/lib/Console/Commands/BuildCommand.js +6 -5
  9. package/lib/Console/Commands/DevCommand.js +151 -311
  10. package/lib/Console/Stubs/page-hydration-dev.tsx +72 -0
  11. package/lib/Console/Stubs/page-hydration.tsx +15 -16
  12. package/lib/Console/Stubs/vendor-dev.tsx +50 -0
  13. package/lib/Core/Environment.js +29 -2
  14. package/lib/Core/Paths.js +12 -4
  15. package/lib/Database/Drivers/MySQLDriver.js +5 -4
  16. package/lib/Database/QueryBuilder.js +2 -3
  17. package/lib/Filesystem/Manager.js +32 -7
  18. package/lib/HMR/Server.js +87 -0
  19. package/lib/Http/Server.js +9 -5
  20. package/lib/Logging/Manager.js +68 -18
  21. package/lib/Route/Loader.js +3 -4
  22. package/lib/Route/Manager.js +24 -3
  23. package/lib/Runtime/Entry.js +26 -1
  24. package/lib/Session/File.js +18 -7
  25. package/lib/View/Client/hmr-client.js +166 -0
  26. package/lib/View/Client/spa.js +142 -0
  27. package/lib/View/Layout.js +94 -0
  28. package/lib/View/Manager.js +390 -46
  29. package/lib/index.d.ts +55 -0
  30. package/package.json +2 -1
  31. package/skeleton/.env.example +0 -2
  32. package/skeleton/app/Controllers/HomeController.js +27 -3
  33. package/skeleton/config/app.js +15 -14
  34. package/skeleton/config/session.js +1 -1
  35. package/skeleton/globals.d.ts +3 -63
  36. package/skeleton/resources/views/Site/Home.tsx +274 -50
  37. package/skeleton/tsconfig.json +5 -1
@@ -1,11 +1,12 @@
1
1
  import React from "react";
2
- import { hydrateRoot } from "react-dom/client";
2
+ import { createRoot } from "react-dom/client";
3
3
 
4
4
  // __COMPONENT_IMPORTS__
5
5
 
6
6
  declare global {
7
7
  interface Window {
8
8
  __NITRON_PROPS__?: Record<string, any>;
9
+ __NITRON_ROOTS__?: Map<HTMLElement, any>;
9
10
  }
10
11
  }
11
12
 
@@ -13,8 +14,9 @@ const componentManifest: Record<string, React.ComponentType<any>> = {};
13
14
 
14
15
  // __COMPONENT_MANIFEST__
15
16
 
16
- function hydrate() {
17
+ function mount() {
17
18
  const props = window.__NITRON_PROPS__ || {};
19
+ if (!window.__NITRON_ROOTS__) window.__NITRON_ROOTS__ = new Map();
18
20
 
19
21
  const islands = document.querySelectorAll<HTMLElement>("[data-cid]");
20
22
 
@@ -22,24 +24,20 @@ function hydrate() {
22
24
  const componentName = element.dataset.island;
23
25
  const componentId = element.dataset.cid;
24
26
 
25
- if (!componentName || !componentId) {
26
- return;
27
- }
27
+ if (!componentName || !componentId) return;
28
28
 
29
29
  const Component = componentManifest[componentName];
30
-
31
- if (!Component) {
32
- return;
33
- }
30
+ if (!Component) return;
34
31
 
35
32
  const componentProps = props[componentId] || {};
36
33
 
37
34
  try {
38
- hydrateRoot(
39
- element,
40
- React.createElement(Component, componentProps),
41
- { identifierPrefix: componentId }
42
- );
35
+ let root = window.__NITRON_ROOTS__.get(element);
36
+ if (!root) {
37
+ root = createRoot(element);
38
+ window.__NITRON_ROOTS__.set(element, root);
39
+ }
40
+ root.render(React.createElement(Component, componentProps));
43
41
  } catch {
44
42
  }
45
43
  });
@@ -48,7 +46,8 @@ function hydrate() {
48
46
  }
49
47
 
50
48
  if (document.readyState === "loading") {
51
- document.addEventListener("DOMContentLoaded", hydrate);
49
+ document.addEventListener("DOMContentLoaded", mount);
52
50
  } else {
53
- hydrate();
51
+ mount();
54
52
  }
53
+
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import * as ReactDOM from 'react-dom';
3
+ import * as ReactDOMClient from 'react-dom/client';
4
+ import * as ReactJSXRuntime from 'react/jsx-runtime';
5
+ import RefreshRuntime from 'react-refresh/runtime';
6
+
7
+ Object.assign(window, {
8
+ __NITRON_REACT__: React,
9
+ __NITRON_REACT_DOM__: ReactDOM,
10
+ __NITRON_REACT_DOM_CLIENT__: ReactDOMClient,
11
+ __NITRON_JSX_RUNTIME__: ReactJSXRuntime
12
+ });
13
+
14
+ RefreshRuntime.injectIntoGlobalHook(window);
15
+
16
+ interface RefreshModule {
17
+ performReactRefresh: () => void;
18
+ register: (type: any, id: string) => void;
19
+ createSignatureFunctionForTransform: () => (type: any, key: string, forceReset?: boolean, getCustomHooks?: () => any[]) => any;
20
+ isLikelyComponentType: (type: any) => boolean;
21
+ }
22
+
23
+ const NitronRefresh: RefreshModule = {
24
+ performReactRefresh: () => {
25
+ if ((RefreshRuntime as any).hasUnrecoverableErrors?.()) {
26
+ window.location.reload();
27
+ return;
28
+ }
29
+ RefreshRuntime.performReactRefresh();
30
+ },
31
+ register: (type: any, id: string) => {
32
+ RefreshRuntime.register(type, id);
33
+ },
34
+ createSignatureFunctionForTransform: () => {
35
+ return RefreshRuntime.createSignatureFunctionForTransform();
36
+ },
37
+ isLikelyComponentType: (type: any) => {
38
+ return RefreshRuntime.isLikelyComponentType(type);
39
+ }
40
+ };
41
+
42
+ (window as any).__NITRON_REFRESH__ = NitronRefresh;
43
+
44
+ (window as any).$RefreshReg$ = (type: any, id: string) => {
45
+ RefreshRuntime.register(type, id);
46
+ };
47
+
48
+ (window as any).$RefreshSig$ = () => {
49
+ return RefreshRuntime.createSignatureFunctionForTransform();
50
+ };
@@ -1,8 +1,36 @@
1
1
  import fs from "node:fs";
2
2
 
3
+ /**
4
+ * Environment Manager
5
+ *
6
+ * Handles environment detection and .env file operations.
7
+ * Development mode is detected automatically based on how the app was started.
8
+ */
3
9
  class Environment {
10
+ static #isDev = false;
4
11
 
5
- static update (key, value) {
12
+ /**
13
+ * Set development mode (called by DevCommand/BuildCommand)
14
+ */
15
+ static setDev(value) {
16
+ this.#isDev = value;
17
+ }
18
+
19
+ /**
20
+ * Check if running in development mode
21
+ */
22
+ static get isDev() {
23
+ return this.#isDev;
24
+ }
25
+
26
+ /**
27
+ * Check if running in production mode
28
+ */
29
+ static get isProd() {
30
+ return !this.#isDev;
31
+ }
32
+
33
+ static update(key, value) {
6
34
  let content = fs.readFileSync(".env", 'utf-8');
7
35
  const regex = new RegExp(`^${key}=.*`, 'm');
8
36
 
@@ -15,7 +43,6 @@ class Environment {
15
43
 
16
44
  fs.writeFileSync(".env", content.trim() + '\n');
17
45
  }
18
-
19
46
  }
20
47
 
21
48
  export default Environment;
package/lib/Core/Paths.js CHANGED
@@ -132,20 +132,28 @@ class Paths {
132
132
  return path.join(this.#project, "storage/app/public/js");
133
133
  }
134
134
 
135
+ // ─────────────────────────────────────────────────────────────────────────
136
+ // Build Output (.nitron - single hidden folder for all framework artifacts)
137
+ // ─────────────────────────────────────────────────────────────────────────
138
+
139
+ static get nitron() {
140
+ return path.join(this.#project, ".nitron");
141
+ }
142
+
135
143
  static get build() {
136
- return path.join(this.#project, "build");
144
+ return path.join(this.#project, ".nitron/build");
137
145
  }
138
146
 
139
147
  static get buildViews() {
140
- return path.join(this.#project, "build/views");
148
+ return path.join(this.#project, ".nitron/build/views");
141
149
  }
142
150
 
143
151
  static get buildFrameworkViews() {
144
- return path.join(this.#project, "build/framework/views");
152
+ return path.join(this.#project, ".nitron/build/framework/views");
145
153
  }
146
154
 
147
155
  static get nitronTemp() {
148
- return path.join(this.#project, ".nitron");
156
+ return path.join(this.#project, ".nitron/temp");
149
157
  }
150
158
 
151
159
  static get jsxRuntime() {
@@ -1,4 +1,5 @@
1
1
  import mysql from 'mysql2/promise';
2
+ import Environment from '../../Core/Environment.js';
2
3
 
3
4
  class MySQLDriver {
4
5
  #pool = null;
@@ -35,7 +36,7 @@ class MySQLDriver {
35
36
  }
36
37
 
37
38
  async query(sql, bindings = []) {
38
- const isProduction = process.env.APP_DEV === 'false';
39
+ const isProduction = Environment.isProd;
39
40
 
40
41
  try {
41
42
  const connection = await this.#pool.getConnection();
@@ -73,7 +74,7 @@ class MySQLDriver {
73
74
  }
74
75
 
75
76
  async raw(sql) {
76
- const isProduction = process.env.APP_DEV === 'false';
77
+ const isProduction = Environment.isProd;
77
78
 
78
79
  try {
79
80
  const [rows, fields] = await this.#pool.query(sql);
@@ -101,7 +102,7 @@ class MySQLDriver {
101
102
  }
102
103
 
103
104
  #maskSensitiveData(bindings) {
104
- if (process.env.APP_DEV === 'true') {
105
+ if (Environment.isDev) {
105
106
  return bindings; // Show all in development
106
107
  }
107
108
 
@@ -161,7 +162,7 @@ class MySQLDriver {
161
162
  }
162
163
 
163
164
  catch (error) {
164
- const isProduction = process.env.APP_DEV === 'false';
165
+ const isProduction = Environment.isProd;
165
166
 
166
167
  if (isProduction) {
167
168
  console.error('[MySQLDriver] Transaction query failed', {
@@ -1,4 +1,5 @@
1
1
  import { validateDirection, validateIdentifier, validateWhereOperator } from "./QueryValidation.js";
2
+ import Environment from "../Core/Environment.js";
2
3
 
3
4
  class QueryBuilder {
4
5
  #table = null;
@@ -104,9 +105,7 @@ class QueryBuilder {
104
105
  }
105
106
 
106
107
  #sanitizeError(error) {
107
- const isProduction = process.env.APP_DEV === 'false';
108
-
109
- if (isProduction) {
108
+ if (Environment.isProd) {
110
109
  console.error('[QueryBuilder] Database error:', {
111
110
  message: error.message,
112
111
  code: error.code,
@@ -6,21 +6,39 @@ class FilesystemManager {
6
6
  static #publicRoot = Paths.storagePublic;
7
7
  static #privateRoot = Paths.storagePrivate;
8
8
 
9
+ /**
10
+ * Validate path to prevent directory traversal attacks
11
+ * @throws Error if path escapes base directory
12
+ */
13
+ static #validatePath(base, filePath) {
14
+ const normalizedBase = path.normalize(base) + path.sep;
15
+ const fullPath = path.normalize(path.join(base, filePath));
16
+
17
+ if (!fullPath.startsWith(normalizedBase)) {
18
+ throw new Error("Invalid file path: directory traversal detected");
19
+ }
20
+
21
+ return fullPath;
22
+ }
23
+
9
24
  static async get(filePath, isPrivate = false) {
10
25
  const base = isPrivate ? this.#privateRoot : this.#publicRoot;
11
- const fullPath = path.join(base, filePath);
12
-
26
+
13
27
  try {
28
+ const fullPath = this.#validatePath(base, filePath);
14
29
  return await fs.promises.readFile(fullPath);
15
- } catch {
30
+ } catch (err) {
31
+ if (err.message.includes("directory traversal")) throw err;
16
32
  return null;
17
33
  }
18
34
  }
19
35
 
20
36
  static async put(file, dir, fileName, isPrivate = false) {
21
37
  const base = isPrivate ? this.#privateRoot : this.#publicRoot;
22
- const folderPath = path.join(base, dir);
23
- const fullPath = path.join(folderPath, fileName);
38
+
39
+ // Validate both dir and fileName
40
+ const folderPath = this.#validatePath(base, dir);
41
+ const fullPath = this.#validatePath(base, path.join(dir, fileName));
24
42
 
25
43
  await fs.promises.mkdir(folderPath, { recursive: true });
26
44
  await fs.promises.writeFile(fullPath, file._buf);
@@ -30,12 +48,19 @@ class FilesystemManager {
30
48
 
31
49
  static async delete(filePath, isPrivate = false) {
32
50
  const base = isPrivate ? this.#privateRoot : this.#publicRoot;
33
- await fs.promises.unlink(path.join(base, filePath));
51
+ const fullPath = this.#validatePath(base, filePath);
52
+ await fs.promises.unlink(fullPath);
34
53
  }
35
54
 
36
55
  static exists(filePath, isPrivate = false) {
37
56
  const base = isPrivate ? this.#privateRoot : this.#publicRoot;
38
- return fs.existsSync(path.join(base, filePath));
57
+
58
+ try {
59
+ const fullPath = this.#validatePath(base, filePath);
60
+ return fs.existsSync(fullPath);
61
+ } catch {
62
+ return false;
63
+ }
39
64
  }
40
65
 
41
66
  static url(filePath) {
@@ -0,0 +1,87 @@
1
+ import { Server as SocketServer } from "socket.io";
2
+ import path from "path";
3
+
4
+ class HMRServer {
5
+ #io = null;
6
+ #ready = false;
7
+ #connections = 0;
8
+
9
+ setup(httpServer) {
10
+ if (this.#io) return;
11
+
12
+ this.#io = new SocketServer(httpServer, {
13
+ path: "/__nitron_hmr",
14
+ transports: ["websocket", "polling"],
15
+ cors: { origin: "*" },
16
+ pingTimeout: 60000,
17
+ pingInterval: 25000,
18
+ serveClient: true
19
+ });
20
+
21
+ this.#io.on("connection", (socket) => {
22
+ this.#connections++;
23
+ socket.on("disconnect", () => { this.#connections--; });
24
+ });
25
+
26
+ this.#ready = true;
27
+ }
28
+
29
+ get isReady() {
30
+ return this.#ready && this.#io !== null;
31
+ }
32
+
33
+ get connectionCount() {
34
+ return this.#connections;
35
+ }
36
+
37
+ emitViewUpdate(filePath) {
38
+ if (!this.#io) return;
39
+
40
+ const normalized = filePath.replace(/\\/g, "/");
41
+ const viewsMatch = normalized.match(/resources\/views\/(.+)\.tsx$/);
42
+ const viewPath = viewsMatch ? viewsMatch[1].toLowerCase() : path.basename(filePath, ".tsx").toLowerCase();
43
+
44
+ this.#io.emit("hmr:update", {
45
+ type: "view",
46
+ file: viewPath,
47
+ url: `/js/${viewPath}.js`,
48
+ timestamp: Date.now()
49
+ });
50
+ }
51
+
52
+ emitCss(filePath) {
53
+ if (!this.#io) return;
54
+ this.#io.emit("hmr:css", {
55
+ file: filePath ? path.basename(filePath) : null,
56
+ timestamp: Date.now()
57
+ });
58
+ }
59
+
60
+ emitReload(reason) {
61
+ if (!this.#io) return;
62
+ this.#io.emit("hmr:reload", {
63
+ reason,
64
+ timestamp: Date.now()
65
+ });
66
+ }
67
+
68
+ emitError(error, filePath) {
69
+ if (!this.#io) return;
70
+ this.#io.emit("hmr:error", {
71
+ file: filePath,
72
+ message: String(error?.message || error),
73
+ timestamp: Date.now()
74
+ });
75
+ }
76
+
77
+ close() {
78
+ if (this.#io) {
79
+ this.#io.close();
80
+ this.#io = null;
81
+ }
82
+ this.#ready = false;
83
+ this.#connections = 0;
84
+ }
85
+ }
86
+
87
+ export default new HMRServer();
@@ -9,6 +9,7 @@ import fastifyHelmet from "@fastify/helmet";
9
9
  import fastifyMultipart from "@fastify/multipart";
10
10
  import Paths from "../Core/Paths.js";
11
11
  import Config from "../Core/Config.js";
12
+ import Environment from "../Core/Environment.js";
12
13
  import Route from "../Route/Manager.js";
13
14
  import View from "../View/Manager.js";
14
15
  import Auth from "../Auth/Manager.js";
@@ -16,8 +17,7 @@ import SessionManager from "../Session/Manager.js";
16
17
  import DB from "../Database/DB.js";
17
18
  import Log from "../Logging/Manager.js";
18
19
  import Loader from "../Route/Loader.js";
19
-
20
- const IS_DEV = process.env.APP_DEV === "true";
20
+ import HMRServer from "../HMR/Server.js";
21
21
 
22
22
  class Server {
23
23
  static #server;
@@ -91,7 +91,7 @@ class Server {
91
91
  dnsPrefetchControl: { allow: false },
92
92
  frameguard: { action: "deny" },
93
93
  hidePoweredBy: true,
94
- hsts: !IS_DEV,
94
+ hsts: !Environment.isDev,
95
95
  ieNoOpen: true,
96
96
  noSniff: true,
97
97
  originAgentCluster: true,
@@ -239,13 +239,17 @@ class Server {
239
239
  const port = Number(process.env.APP_PORT) || 3000;
240
240
  const address = await this.#server.listen({ host, port });
241
241
 
242
+ if (Environment.isDev) {
243
+ HMRServer.setup(this.#server.server);
244
+ }
245
+
242
246
  this.#printBanner({ success: true, address, host, port });
243
247
 
244
248
  Log.info("Server started successfully!", {
245
249
  address,
246
250
  host,
247
251
  port,
248
- environment: IS_DEV ? "development" : "production"
252
+ environment: Environment.isDev ? "development" : "production"
249
253
  });
250
254
  }
251
255
  catch (err) {
@@ -292,7 +296,7 @@ ${color}███╗ ██╗██╗████████╗████
292
296
  `${bold}Address:${reset} ${address}`,
293
297
  `${bold}Host:${reset} ${host}`,
294
298
  `${bold}Port:${reset} ${port}`,
295
- `${bold}Mode:${reset} ${IS_DEV ? "development" : "production"}`
299
+ `${bold}Mode:${reset} ${Environment.isDev ? "development" : "production"}`
296
300
  ]
297
301
  : [
298
302
  `${color}${bold}✕${reset} ${bold}Server failed to start${reset}`,
@@ -11,6 +11,14 @@ class LogManager {
11
11
  fatal: 4
12
12
  };
13
13
 
14
+ static #levelLabels = {
15
+ debug: "DEBUG",
16
+ info: "INFO ",
17
+ warn: "WARN ",
18
+ error: "ERROR",
19
+ fatal: "FATAL"
20
+ };
21
+
14
22
  static debug(message, context = {}) {
15
23
  this.#log("debug", message, context);
16
24
  }
@@ -48,13 +56,7 @@ class LogManager {
48
56
  return;
49
57
  }
50
58
 
51
- const payload = {
52
- level,
53
- message,
54
- context,
55
- timestamp: new Date().toISOString(),
56
- pid: process.pid
57
- };
59
+ const timestamp = new Date();
58
60
 
59
61
  const channel = config.channel;
60
62
 
@@ -63,11 +65,11 @@ class LogManager {
63
65
  }
64
66
 
65
67
  if (channel === "console") {
66
- this.#logToConsole(payload);
68
+ this.#logToConsole(level, message, context, timestamp);
67
69
  }
68
70
 
69
71
  if (channel === "file") {
70
- this.#logToFile(payload, config);
72
+ this.#logToFile(level, message, context, timestamp, config);
71
73
  }
72
74
  } catch (e) {
73
75
  console.error("Logger failure:", e.message);
@@ -79,7 +81,18 @@ class LogManager {
79
81
  return this.#levels[level] >= this.#levels[configLevel];
80
82
  }
81
83
 
82
- static #logToConsole({ level, message, context, timestamp }) {
84
+ static #formatTimestamp(date) {
85
+ const year = date.getFullYear();
86
+ const month = String(date.getMonth() + 1).padStart(2, "0");
87
+ const day = String(date.getDate()).padStart(2, "0");
88
+ const hours = String(date.getHours()).padStart(2, "0");
89
+ const minutes = String(date.getMinutes()).padStart(2, "0");
90
+ const seconds = String(date.getSeconds()).padStart(2, "0");
91
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
92
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
93
+ }
94
+
95
+ static #logToConsole(level, message, context, timestamp) {
83
96
  const colors = {
84
97
  debug: "\x1b[90m",
85
98
  info: "\x1b[36m",
@@ -102,14 +115,12 @@ class LogManager {
102
115
  const bold = "\x1b[1m";
103
116
  const dim = "\x1b[2m";
104
117
 
105
- const date = new Date(timestamp);
106
- const time = date.toLocaleTimeString("en-US", { hour12: false });
107
- const dateStr = date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
118
+ const time = this.#formatTimestamp(timestamp);
108
119
  const width = process.stdout.columns || 80;
109
120
 
110
121
  console.log(`${color}┌${"─".repeat(width - 2)}${reset}`);
111
122
  console.log(`${color}│${reset} ${color}${icon}${reset} ${bold}${level.toUpperCase()}${reset}`);
112
- console.log(`${color}│${reset} ${dim}${dateStr} at ${time}${reset}`);
123
+ console.log(`${color}│${reset} ${dim}${time}${reset}`);
113
124
  console.log(`${color}├${"─".repeat(width - 2)}${reset}`);
114
125
  console.log(`${color}│${reset}`);
115
126
  console.log(`${color}│${reset} ${bold}${message}${reset}`);
@@ -128,7 +139,32 @@ class LogManager {
128
139
  console.log();
129
140
  }
130
141
 
131
- static #logToFile(payload, config) {
142
+ static #formatContext(context, indent = "") {
143
+ const lines = [];
144
+
145
+ for (const [key, value] of Object.entries(context)) {
146
+ if (value === null || value === undefined) continue;
147
+
148
+ if (typeof value === "object" && !Array.isArray(value)) {
149
+ lines.push(`${indent}${key}:`);
150
+ lines.push(...this.#formatContext(value, indent + " "));
151
+ }
152
+ else if (key === "stack" && typeof value === "string") {
153
+ // Format stack trace nicely
154
+ lines.push(`${indent}${key}:`);
155
+ value.split("\n").forEach(line => {
156
+ lines.push(`${indent} ${line.trim()}`);
157
+ });
158
+ }
159
+ else {
160
+ lines.push(`${indent}${key}: ${value}`);
161
+ }
162
+ }
163
+
164
+ return lines;
165
+ }
166
+
167
+ static #logToFile(level, message, context, timestamp, config) {
132
168
  const filePath = path.resolve(process.cwd(), config.file);
133
169
  const dir = path.dirname(filePath);
134
170
 
@@ -136,12 +172,26 @@ class LogManager {
136
172
  fs.mkdirSync(dir, { recursive: true });
137
173
  }
138
174
 
139
- const logLine = JSON.stringify(payload) + "\n";
175
+ const time = this.#formatTimestamp(timestamp);
176
+ const label = this.#levelLabels[level];
177
+
178
+ // Build readable log entry
179
+ const lines = [];
180
+ lines.push(`[${time}] [${label}] ${message}`);
181
+
182
+ if (Object.keys(context).length > 0) {
183
+ const contextLines = this.#formatContext(context, " ");
184
+ lines.push(...contextLines);
185
+ }
186
+
187
+ lines.push(""); // Empty line between entries
188
+
189
+ const logEntry = lines.join("\n") + "\n";
140
190
 
141
191
  if (config.sync) {
142
- fs.appendFileSync(filePath, logLine);
192
+ fs.appendFileSync(filePath, logEntry);
143
193
  } else {
144
- fs.appendFile(filePath, logLine, (err) => {
194
+ fs.appendFile(filePath, logEntry, (err) => {
145
195
  if (err) {
146
196
  console.error("Failed to write log:", err.message);
147
197
  }
@@ -1,8 +1,7 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
3
  import Paths from "../Core/Paths.js";
4
-
5
- const IS_DEV = process.env.APP_DEV === "true";
4
+ import Environment from "../Core/Environment.js";
6
5
 
7
6
  const DIRECTORIES = [
8
7
  Paths.controllers,
@@ -14,7 +13,7 @@ class Loader {
14
13
  #initialized = false;
15
14
 
16
15
  async initialize() {
17
- if (this.#initialized || !IS_DEV) return;
16
+ if (this.#initialized || !Environment.isDev) return;
18
17
 
19
18
  for (const dir of DIRECTORIES) {
20
19
  await this.#loadDirectory(dir);
@@ -24,7 +23,7 @@ class Loader {
24
23
  }
25
24
 
26
25
  wrapHandler(handler) {
27
- if (!IS_DEV) return handler;
26
+ if (!Environment.isDev) return handler;
28
27
 
29
28
  const info = this.#registry.get(handler);
30
29
  if (!info) return handler;
@@ -19,7 +19,6 @@ class Route {
19
19
  }
20
20
 
21
21
  static async setup(server) {
22
- // Setup global route helper for SSR
23
22
  globalThis.route = (name, params, query) => this.route(name, params, query);
24
23
 
25
24
  const Kernel = await this.getKernel();
@@ -56,7 +55,6 @@ class Route {
56
55
  }
57
56
  }
58
57
 
59
- // Get route manifest for client-side routing
60
58
  static getClientManifest() {
61
59
  const manifest = {};
62
60
  for (const route of this.#routes) {
@@ -66,7 +64,30 @@ class Route {
66
64
  }
67
65
  return manifest;
68
66
  }
69
-
67
+
68
+ static match(pathname, method = "GET") {
69
+ for (const route of this.#routes) {
70
+ if (route.method !== method) continue;
71
+
72
+ const pattern = route.url
73
+ .replace(/:[^/]+/g, "([^/]+)")
74
+ .replace(/\//g, "\\/");
75
+ const regex = new RegExp(`^${pattern}$`);
76
+ const match = pathname.match(regex);
77
+
78
+ if (match) {
79
+ const paramNames = (route.url.match(/:[^/]+/g) || [])
80
+ .map(p => p.slice(1));
81
+ const params = {};
82
+ paramNames.forEach((name, i) => {
83
+ params[name] = match[i + 1];
84
+ });
85
+ return { handler: route.handler, params, route };
86
+ }
87
+ }
88
+ return null;
89
+ }
90
+
70
91
  static get (url, handler) {
71
92
  return this.#add("GET", url, handler);
72
93
  }