@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.
- package/lib/Build/CssBuilder.js +129 -0
- package/lib/Build/FileAnalyzer.js +395 -0
- package/lib/Build/HydrationBuilder.js +173 -0
- package/lib/Build/Manager.js +290 -936
- package/lib/Build/colors.js +10 -0
- package/lib/Build/jsxRuntime.js +116 -0
- package/lib/Build/plugins.js +264 -0
- package/lib/Console/Commands/BuildCommand.js +6 -5
- package/lib/Console/Commands/DevCommand.js +151 -311
- package/lib/Console/Stubs/page-hydration-dev.tsx +72 -0
- package/lib/Console/Stubs/page-hydration.tsx +15 -16
- package/lib/Console/Stubs/vendor-dev.tsx +50 -0
- package/lib/Core/Environment.js +29 -2
- package/lib/Core/Paths.js +12 -4
- package/lib/Database/Drivers/MySQLDriver.js +5 -4
- package/lib/Database/QueryBuilder.js +2 -3
- package/lib/Filesystem/Manager.js +32 -7
- package/lib/HMR/Server.js +87 -0
- package/lib/Http/Server.js +9 -5
- package/lib/Logging/Manager.js +68 -18
- package/lib/Route/Loader.js +3 -4
- package/lib/Route/Manager.js +24 -3
- package/lib/Runtime/Entry.js +26 -1
- package/lib/Session/File.js +18 -7
- package/lib/View/Client/hmr-client.js +166 -0
- package/lib/View/Client/spa.js +142 -0
- package/lib/View/Layout.js +94 -0
- package/lib/View/Manager.js +390 -46
- package/lib/index.d.ts +55 -0
- package/package.json +2 -1
- package/skeleton/.env.example +0 -2
- package/skeleton/app/Controllers/HomeController.js +27 -3
- package/skeleton/config/app.js +15 -14
- package/skeleton/config/session.js +1 -1
- package/skeleton/globals.d.ts +3 -63
- package/skeleton/resources/views/Site/Home.tsx +274 -50
- package/skeleton/tsconfig.json +5 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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",
|
|
49
|
+
document.addEventListener("DOMContentLoaded", mount);
|
|
52
50
|
} else {
|
|
53
|
-
|
|
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
|
+
};
|
package/lib/Core/Environment.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
package/lib/Http/Server.js
CHANGED
|
@@ -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: !
|
|
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:
|
|
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} ${
|
|
299
|
+
`${bold}Mode:${reset} ${Environment.isDev ? "development" : "production"}`
|
|
296
300
|
]
|
|
297
301
|
: [
|
|
298
302
|
`${color}${bold}✕${reset} ${bold}Server failed to start${reset}`,
|
package/lib/Logging/Manager.js
CHANGED
|
@@ -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
|
|
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(
|
|
68
|
+
this.#logToConsole(level, message, context, timestamp);
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
if (channel === "file") {
|
|
70
|
-
this.#logToFile(
|
|
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 #
|
|
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
|
|
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}${
|
|
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 #
|
|
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
|
|
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,
|
|
192
|
+
fs.appendFileSync(filePath, logEntry);
|
|
143
193
|
} else {
|
|
144
|
-
fs.appendFile(filePath,
|
|
194
|
+
fs.appendFile(filePath, logEntry, (err) => {
|
|
145
195
|
if (err) {
|
|
146
196
|
console.error("Failed to write log:", err.message);
|
|
147
197
|
}
|
package/lib/Route/Loader.js
CHANGED
|
@@ -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 || !
|
|
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 (!
|
|
26
|
+
if (!Environment.isDev) return handler;
|
|
28
27
|
|
|
29
28
|
const info = this.#registry.get(handler);
|
|
30
29
|
if (!info) return handler;
|
package/lib/Route/Manager.js
CHANGED
|
@@ -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
|
}
|