@nitronjs/framework 0.2.2 → 0.2.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.
- package/README.md +3 -1
- package/cli/create.js +88 -72
- package/cli/njs.js +17 -19
- package/lib/Auth/Auth.js +167 -0
- package/lib/Build/CssBuilder.js +9 -0
- package/lib/Build/FileAnalyzer.js +16 -0
- package/lib/Build/HydrationBuilder.js +17 -0
- package/lib/Build/Manager.js +15 -0
- package/lib/Build/colors.js +4 -0
- package/lib/Build/plugins.js +84 -20
- package/lib/Console/Commands/DevCommand.js +13 -9
- package/lib/Console/Commands/MakeCommand.js +24 -10
- package/lib/Console/Commands/MigrateCommand.js +4 -3
- package/lib/Console/Commands/MigrateFreshCommand.js +22 -27
- package/lib/Console/Commands/MigrateRollbackCommand.js +8 -4
- package/lib/Console/Commands/MigrateStatusCommand.js +8 -4
- package/lib/Console/Commands/SeedCommand.js +8 -28
- package/lib/Console/Commands/StorageLinkCommand.js +20 -5
- package/lib/Console/Output.js +143 -0
- package/lib/Core/Config.js +2 -1
- package/lib/Core/Paths.js +8 -8
- package/lib/Database/DB.js +141 -51
- package/lib/Database/Drivers/MySQLDriver.js +102 -157
- package/lib/Database/Migration/Checksum.js +3 -8
- package/lib/Database/Migration/MigrationRepository.js +25 -35
- package/lib/Database/Migration/MigrationRunner.js +59 -67
- package/lib/Database/Model.js +165 -75
- package/lib/Database/QueryBuilder.js +43 -0
- package/lib/Database/QueryValidation.js +51 -30
- package/lib/Database/Schema/Blueprint.js +25 -36
- package/lib/Database/Schema/Manager.js +31 -68
- package/lib/Database/Seeder/SeederRunner.js +24 -145
- package/lib/Date/DateTime.js +9 -0
- package/lib/Encryption/Encryption.js +52 -0
- package/lib/Faker/Faker.js +11 -0
- package/lib/Filesystem/Storage.js +120 -0
- package/lib/HMR/Server.js +79 -9
- package/lib/Hashing/Hash.js +41 -0
- package/lib/Http/Server.js +179 -151
- package/lib/Logging/{Manager.js → Log.js} +68 -80
- package/lib/Mail/Mail.js +187 -0
- package/lib/Route/Router.js +416 -0
- package/lib/Session/File.js +135 -233
- package/lib/Session/Manager.js +117 -171
- package/lib/Session/Memory.js +28 -38
- package/lib/Session/Session.js +71 -107
- package/lib/Support/Str.js +103 -0
- package/lib/Translation/Lang.js +54 -0
- package/lib/View/Client/hmr-client.js +87 -51
- package/lib/View/Client/nitronjs-icon.png +0 -0
- package/lib/View/{Manager.js → View.js} +44 -29
- package/lib/index.d.ts +49 -27
- package/lib/index.js +19 -13
- package/package.json +1 -1
- package/skeleton/app/Controllers/HomeController.js +7 -1
- package/skeleton/package.json +2 -0
- package/skeleton/resources/css/global.css +1 -0
- package/skeleton/resources/views/Site/Home.tsx +456 -79
- package/skeleton/tsconfig.json +6 -1
- package/lib/Auth/Manager.js +0 -111
- package/lib/Database/Connection.js +0 -61
- package/lib/Database/Manager.js +0 -162
- package/lib/Database/Migration/migrations/0000_00_00_00_01_create_seeders_table.js +0 -20
- package/lib/Database/Seeder/SeederRepository.js +0 -45
- package/lib/Encryption/Manager.js +0 -47
- package/lib/Filesystem/Manager.js +0 -74
- package/lib/Hashing/Manager.js +0 -25
- package/lib/Mail/Manager.js +0 -120
- package/lib/Route/Loader.js +0 -80
- package/lib/Route/Manager.js +0 -286
- package/lib/Translation/Manager.js +0 -49
package/lib/Faker/Faker.js
CHANGED
|
@@ -8,6 +8,17 @@ import * as ColorData from './Data/Color.js';
|
|
|
8
8
|
import * as DateData from './Data/Date.js';
|
|
9
9
|
import * as PhoneData from './Data/Phone.js';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Fake data generator for testing and seeding.
|
|
13
|
+
* Generates unique values by default to prevent duplicates.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* import { Faker } from "@nitronjs/framework";
|
|
17
|
+
*
|
|
18
|
+
* const name = Faker.fullName();
|
|
19
|
+
* const email = Faker.email();
|
|
20
|
+
* const phone = Faker.phoneNumber();
|
|
21
|
+
*/
|
|
11
22
|
class FakerClass {
|
|
12
23
|
|
|
13
24
|
#usedValues = new Map();
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import Paths from "../Core/Paths.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* File storage manager for public and private file operations.
|
|
7
|
+
* Includes security measures against directory traversal attacks.
|
|
8
|
+
*/
|
|
9
|
+
class Storage {
|
|
10
|
+
static #publicRoot = Paths.storagePublic;
|
|
11
|
+
static #privateRoot = Paths.storagePrivate;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reads a file from storage.
|
|
15
|
+
* @param {string} filePath - Relative path to the file
|
|
16
|
+
* @param {boolean} isPrivate - Whether to read from private storage
|
|
17
|
+
* @returns {Promise<Buffer|null>} File contents or null if not found
|
|
18
|
+
*/
|
|
19
|
+
static async get(filePath, isPrivate = false) {
|
|
20
|
+
const base = isPrivate ? this.#privateRoot : this.#publicRoot;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const fullPath = this.#validatePath(base, filePath);
|
|
24
|
+
|
|
25
|
+
return await fs.promises.readFile(fullPath);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
if (err.message.includes("directory traversal")) {
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Saves a file to storage.
|
|
38
|
+
* @param {Object} file - File object with _buf property containing file data
|
|
39
|
+
* @param {string} dir - Directory path within storage
|
|
40
|
+
* @param {string} fileName - Name for the saved file
|
|
41
|
+
* @param {boolean} isPrivate - Whether to save to private storage
|
|
42
|
+
* @returns {Promise<boolean>} True if save successful
|
|
43
|
+
*/
|
|
44
|
+
static async put(file, dir, fileName, isPrivate = false) {
|
|
45
|
+
const base = isPrivate ? this.#privateRoot : this.#publicRoot;
|
|
46
|
+
const folderPath = this.#validatePath(base, dir);
|
|
47
|
+
const fullPath = this.#validatePath(base, path.join(dir, fileName));
|
|
48
|
+
|
|
49
|
+
await fs.promises.mkdir(folderPath, { recursive: true });
|
|
50
|
+
await fs.promises.writeFile(fullPath, file._buf);
|
|
51
|
+
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Deletes a file from storage.
|
|
57
|
+
* @param {string} filePath - Relative path to the file
|
|
58
|
+
* @param {boolean} isPrivate - Whether to delete from private storage
|
|
59
|
+
* @returns {Promise<void>}
|
|
60
|
+
*/
|
|
61
|
+
static async delete(filePath, isPrivate = false) {
|
|
62
|
+
const base = isPrivate ? this.#privateRoot : this.#publicRoot;
|
|
63
|
+
const fullPath = this.#validatePath(base, filePath);
|
|
64
|
+
|
|
65
|
+
await fs.promises.unlink(fullPath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Checks if a file exists in storage.
|
|
70
|
+
* @param {string} filePath - Relative path to the file
|
|
71
|
+
* @param {boolean} isPrivate - Whether to check private storage
|
|
72
|
+
* @returns {boolean} True if file exists
|
|
73
|
+
*/
|
|
74
|
+
static exists(filePath, isPrivate = false) {
|
|
75
|
+
const base = isPrivate ? this.#privateRoot : this.#publicRoot;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const fullPath = this.#validatePath(base, filePath);
|
|
79
|
+
|
|
80
|
+
return fs.existsSync(fullPath);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Gets the public URL for a file in public storage.
|
|
89
|
+
* @param {string} filePath - Relative path to the file
|
|
90
|
+
* @returns {string} Public URL path
|
|
91
|
+
*/
|
|
92
|
+
static url(filePath) {
|
|
93
|
+
if (filePath.startsWith("/")) {
|
|
94
|
+
filePath = filePath.substring(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return `/storage/${filePath}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validates a file path to prevent directory traversal attacks.
|
|
102
|
+
* @param {string} base - Base directory path
|
|
103
|
+
* @param {string} filePath - Relative file path to validate
|
|
104
|
+
* @returns {string} Full validated path
|
|
105
|
+
* @throws {Error} If path escapes base directory
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
static #validatePath(base, filePath) {
|
|
109
|
+
const normalizedBase = path.normalize(base) + path.sep;
|
|
110
|
+
const fullPath = path.normalize(path.join(base, filePath));
|
|
111
|
+
|
|
112
|
+
if (!fullPath.startsWith(normalizedBase)) {
|
|
113
|
+
throw new Error("Invalid file path: directory traversal detected");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return fullPath;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default Storage;
|
package/lib/HMR/Server.js
CHANGED
|
@@ -1,11 +1,52 @@
|
|
|
1
1
|
import { Server as SocketServer } from "socket.io";
|
|
2
|
+
import { createRequire } from "module";
|
|
2
3
|
import path from "path";
|
|
4
|
+
import fs from "fs";
|
|
3
5
|
|
|
6
|
+
/**
|
|
7
|
+
* HMR (Hot Module Replacement) server for development mode.
|
|
8
|
+
* Manages WebSocket connections and broadcasts file change events to connected clients.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // In HTTP Server
|
|
12
|
+
* HMR.registerRoutes(fastify);
|
|
13
|
+
* HMR.setup(httpServer);
|
|
14
|
+
*
|
|
15
|
+
* // Emit updates
|
|
16
|
+
* HMR.emitViewUpdate("resources/views/Site/Home.tsx");
|
|
17
|
+
* HMR.emitCss("site_style.css");
|
|
18
|
+
*/
|
|
4
19
|
class HMRServer {
|
|
5
20
|
#io = null;
|
|
6
|
-
#ready = false;
|
|
7
21
|
#connections = 0;
|
|
22
|
+
#clientScript = null;
|
|
8
23
|
|
|
24
|
+
// Public Methods
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Registers socket.io client script route in Fastify.
|
|
28
|
+
* Must be called before Router.setup() to ensure the route is registered.
|
|
29
|
+
* @param {import("fastify").FastifyInstance} fastify
|
|
30
|
+
*/
|
|
31
|
+
registerRoutes(fastify) {
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
const socketIoDir = path.dirname(require.resolve("socket.io/package.json"));
|
|
34
|
+
|
|
35
|
+
this.#clientScript = path.join(socketIoDir, "client-dist", "socket.io.min.js");
|
|
36
|
+
|
|
37
|
+
fastify.get("/__nitron_hmr/socket.io.js", (req, reply) => {
|
|
38
|
+
if (!this.#clientScript || !fs.existsSync(this.#clientScript)) {
|
|
39
|
+
return reply.code(404).send("Socket.io client not found");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
reply.type("application/javascript").send(fs.readFileSync(this.#clientScript, "utf-8"));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initializes the WebSocket server.
|
|
48
|
+
* @param {import("http").Server} httpServer - Node.js HTTP server instance.
|
|
49
|
+
*/
|
|
9
50
|
setup(httpServer) {
|
|
10
51
|
if (this.#io) return;
|
|
11
52
|
|
|
@@ -15,31 +56,41 @@ class HMRServer {
|
|
|
15
56
|
cors: { origin: "*" },
|
|
16
57
|
pingTimeout: 60000,
|
|
17
58
|
pingInterval: 25000,
|
|
18
|
-
serveClient:
|
|
59
|
+
serveClient: false
|
|
19
60
|
});
|
|
20
61
|
|
|
21
62
|
this.#io.on("connection", (socket) => {
|
|
22
63
|
this.#connections++;
|
|
23
|
-
socket.on("disconnect", () =>
|
|
64
|
+
socket.on("disconnect", () => this.#connections--);
|
|
24
65
|
});
|
|
25
|
-
|
|
26
|
-
this.#ready = true;
|
|
27
66
|
}
|
|
28
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Whether the HMR server is ready and accepting connections.
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
29
72
|
get isReady() {
|
|
30
|
-
return this.#
|
|
73
|
+
return this.#io !== null;
|
|
31
74
|
}
|
|
32
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Number of currently connected clients.
|
|
78
|
+
* @returns {number}
|
|
79
|
+
*/
|
|
33
80
|
get connectionCount() {
|
|
34
81
|
return this.#connections;
|
|
35
82
|
}
|
|
36
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Emits a view update event to trigger hot reload of a React component.
|
|
86
|
+
* @param {string} filePath - Absolute path to the changed view file.
|
|
87
|
+
*/
|
|
37
88
|
emitViewUpdate(filePath) {
|
|
38
89
|
if (!this.#io) return;
|
|
39
90
|
|
|
40
91
|
const normalized = filePath.replace(/\\/g, "/");
|
|
41
|
-
const
|
|
42
|
-
const viewPath =
|
|
92
|
+
const match = normalized.match(/resources\/views\/(.+)\.tsx$/);
|
|
93
|
+
const viewPath = match ? match[1].toLowerCase() : path.basename(filePath, ".tsx").toLowerCase();
|
|
43
94
|
|
|
44
95
|
this.#io.emit("hmr:update", {
|
|
45
96
|
type: "view",
|
|
@@ -49,24 +100,40 @@ class HMRServer {
|
|
|
49
100
|
});
|
|
50
101
|
}
|
|
51
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Emits a CSS update event to refresh stylesheets without page reload.
|
|
105
|
+
* @param {string} [filePath] - Path to the changed CSS file. If null, refreshes all CSS.
|
|
106
|
+
*/
|
|
52
107
|
emitCss(filePath) {
|
|
53
108
|
if (!this.#io) return;
|
|
109
|
+
|
|
54
110
|
this.#io.emit("hmr:css", {
|
|
55
111
|
file: filePath ? path.basename(filePath) : null,
|
|
56
112
|
timestamp: Date.now()
|
|
57
113
|
});
|
|
58
114
|
}
|
|
59
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Emits a full page reload event.
|
|
118
|
+
* @param {string} reason - Reason for the reload (shown in dev tools).
|
|
119
|
+
*/
|
|
60
120
|
emitReload(reason) {
|
|
61
121
|
if (!this.#io) return;
|
|
122
|
+
|
|
62
123
|
this.#io.emit("hmr:reload", {
|
|
63
124
|
reason,
|
|
64
125
|
timestamp: Date.now()
|
|
65
126
|
});
|
|
66
127
|
}
|
|
67
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Emits a build error event to show error overlay in browser.
|
|
131
|
+
* @param {Error|string} error - The error that occurred.
|
|
132
|
+
* @param {string} [filePath] - Path to the file that caused the error.
|
|
133
|
+
*/
|
|
68
134
|
emitError(error, filePath) {
|
|
69
135
|
if (!this.#io) return;
|
|
136
|
+
|
|
70
137
|
this.#io.emit("hmr:error", {
|
|
71
138
|
file: filePath,
|
|
72
139
|
message: String(error?.message || error),
|
|
@@ -74,12 +141,15 @@ class HMRServer {
|
|
|
74
141
|
});
|
|
75
142
|
}
|
|
76
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Closes the WebSocket server and cleans up resources.
|
|
146
|
+
*/
|
|
77
147
|
close() {
|
|
78
148
|
if (this.#io) {
|
|
79
149
|
this.#io.close();
|
|
80
150
|
this.#io = null;
|
|
81
151
|
}
|
|
82
|
-
|
|
152
|
+
|
|
83
153
|
this.#connections = 0;
|
|
84
154
|
}
|
|
85
155
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import bcrypt from "bcrypt";
|
|
2
|
+
import Config from "../Core/Config.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Secure password hashing using bcrypt with APP_KEY pepper.
|
|
6
|
+
*/
|
|
7
|
+
class Hash {
|
|
8
|
+
/**
|
|
9
|
+
* Hashes a plain text password.
|
|
10
|
+
* @param {string} textField - Plain text password to hash
|
|
11
|
+
* @returns {Promise<string>} Bcrypt hashed password
|
|
12
|
+
* @throws {Error} If APP_KEY is not set
|
|
13
|
+
*/
|
|
14
|
+
static async make(textField) {
|
|
15
|
+
if (!process.env.APP_KEY) {
|
|
16
|
+
throw new Error("APP_KEY is required for hashing");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const saltRounds = Config.get("hash.salt_rounds", 10);
|
|
20
|
+
const salt = await bcrypt.genSalt(saltRounds);
|
|
21
|
+
|
|
22
|
+
return await bcrypt.hash(textField + process.env.APP_KEY, salt);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Verifies a plain text password against a hash.
|
|
27
|
+
* @param {string} textField - Plain text password to verify
|
|
28
|
+
* @param {string} hashedText - Bcrypt hash to compare against
|
|
29
|
+
* @returns {Promise<boolean>} True if password matches
|
|
30
|
+
* @throws {Error} If APP_KEY is not set
|
|
31
|
+
*/
|
|
32
|
+
static async check(textField, hashedText) {
|
|
33
|
+
if (!process.env.APP_KEY) {
|
|
34
|
+
throw new Error("APP_KEY is required for hashing");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return await bcrypt.compare(textField + process.env.APP_KEY, hashedText);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default Hash;
|