@nitronjs/framework 0.2.26 → 0.3.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 (59) hide show
  1. package/README.md +260 -170
  2. package/lib/Auth/Auth.js +2 -2
  3. package/lib/Build/CssBuilder.js +5 -7
  4. package/lib/Build/EffectivePropUsage.js +174 -0
  5. package/lib/Build/FactoryTransform.js +1 -21
  6. package/lib/Build/FileAnalyzer.js +2 -33
  7. package/lib/Build/Manager.js +390 -58
  8. package/lib/Build/PropUsageAnalyzer.js +1189 -0
  9. package/lib/Build/jsxRuntime.js +25 -155
  10. package/lib/Build/plugins.js +212 -146
  11. package/lib/Build/propUtils.js +70 -0
  12. package/lib/Console/Commands/DevCommand.js +30 -10
  13. package/lib/Console/Commands/MakeCommand.js +8 -1
  14. package/lib/Console/Output.js +0 -2
  15. package/lib/Console/Stubs/rsc-consumer.tsx +74 -0
  16. package/lib/Console/Stubs/vendor-dev.tsx +30 -41
  17. package/lib/Console/Stubs/vendor.tsx +25 -1
  18. package/lib/Core/Config.js +0 -6
  19. package/lib/Core/Paths.js +0 -19
  20. package/lib/Database/Migration/Checksum.js +0 -3
  21. package/lib/Database/Migration/MigrationRepository.js +0 -8
  22. package/lib/Database/Migration/MigrationRunner.js +1 -2
  23. package/lib/Database/Model.js +19 -11
  24. package/lib/Database/QueryBuilder.js +25 -4
  25. package/lib/Database/Schema/Blueprint.js +10 -0
  26. package/lib/Database/Schema/Manager.js +2 -0
  27. package/lib/Date/DateTime.js +1 -1
  28. package/lib/Dev/DevContext.js +44 -0
  29. package/lib/Dev/DevErrorPage.js +990 -0
  30. package/lib/Dev/DevIndicator.js +836 -0
  31. package/lib/HMR/Server.js +16 -37
  32. package/lib/Http/Server.js +177 -24
  33. package/lib/Logging/Log.js +34 -2
  34. package/lib/Mail/Mail.js +41 -10
  35. package/lib/Route/Router.js +43 -19
  36. package/lib/Runtime/Entry.js +10 -6
  37. package/lib/Session/Manager.js +144 -1
  38. package/lib/Session/Redis.js +117 -0
  39. package/lib/Session/Session.js +0 -4
  40. package/lib/Support/Str.js +6 -4
  41. package/lib/Translation/Lang.js +376 -32
  42. package/lib/Translation/pluralize.js +81 -0
  43. package/lib/Validation/MagicBytes.js +120 -0
  44. package/lib/Validation/Validator.js +46 -29
  45. package/lib/View/Client/hmr-client.js +100 -90
  46. package/lib/View/Client/spa.js +121 -50
  47. package/lib/View/ClientManifest.js +60 -0
  48. package/lib/View/FlightRenderer.js +100 -0
  49. package/lib/View/Layout.js +0 -3
  50. package/lib/View/PropFilter.js +81 -0
  51. package/lib/View/View.js +230 -495
  52. package/lib/index.d.ts +22 -1
  53. package/package.json +3 -2
  54. package/skeleton/config/app.js +1 -0
  55. package/skeleton/config/server.js +13 -0
  56. package/skeleton/config/session.js +4 -0
  57. package/lib/Build/HydrationBuilder.js +0 -190
  58. package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
  59. package/lib/Console/Stubs/page-hydration.tsx +0 -53
@@ -2,6 +2,7 @@ import crypto from "crypto";
2
2
  import Config from "../Core/Config.js";
3
3
  import Memory from "./Memory.js";
4
4
  import File from "./File.js";
5
+ import RedisStore from "./Redis.js";
5
6
  import Session from "./Session.js";
6
7
 
7
8
  /**
@@ -44,7 +45,26 @@ class SessionManager {
44
45
  this.#config = Config.all("session");
45
46
  this.#config.cookie.signed = true;
46
47
 
47
- this.#store = this.#config.driver === "file" ? new File() : new Memory();
48
+ switch (this.#config.driver) {
49
+ case "file":
50
+ this.#store = new File();
51
+ break;
52
+
53
+ case "memory":
54
+ this.#store = new Memory();
55
+ break;
56
+
57
+ case "redis":
58
+ this.#store = new RedisStore(this.#config.lifetime);
59
+ break;
60
+
61
+ case "none":
62
+ this.#store = null;
63
+ return;
64
+
65
+ default:
66
+ throw new Error(`[Session] Unknown driver: "${this.#config.driver}"`);
67
+ }
48
68
 
49
69
  if (this.#store.ready) {
50
70
  await this.#store.ready;
@@ -122,6 +142,10 @@ class SessionManager {
122
142
  get cookieConfig() {
123
143
  const config = { ...this.#config.cookie, signed: true };
124
144
 
145
+ if (!config.sameSite) {
146
+ config.sameSite = "Lax";
147
+ }
148
+
125
149
  if (config.maxAge) {
126
150
  config.maxAge = Math.floor(config.maxAge / 1000);
127
151
  }
@@ -182,6 +206,17 @@ class SessionManager {
182
206
  }
183
207
  }
184
208
 
209
+ /**
210
+ * Closes the session store connection.
211
+ */
212
+ async close() {
213
+ this.stopGC();
214
+
215
+ if (this.#store && typeof this.#store.close === "function") {
216
+ await this.#store.close();
217
+ }
218
+ }
219
+
185
220
  /**
186
221
  * Sets up session middleware for Fastify.
187
222
  * @param {import("fastify").FastifyInstance} server
@@ -189,9 +224,20 @@ class SessionManager {
189
224
  static async setup(server) {
190
225
  const manager = await SessionManager.getInstance();
191
226
 
227
+ if (!manager.#store) {
228
+ return;
229
+ }
230
+
192
231
  server.decorateRequest("session", null);
193
232
 
194
233
  server.addHook("preHandler", async (request, response) => {
234
+ const lastSegment = request.url.split("/").pop().split("?")[0];
235
+ const dotIndex = lastSegment.lastIndexOf(".");
236
+
237
+ if (dotIndex > 0 && STATIC_EXTENSIONS.has(lastSegment.slice(dotIndex + 1).toLowerCase())) {
238
+ return;
239
+ }
240
+
195
241
  request.session = await manager.load(request, response);
196
242
  });
197
243
 
@@ -217,4 +263,101 @@ class SessionManager {
217
263
  }
218
264
  }
219
265
 
266
+ // ─────────────────────────────────────────────────────────────────────────────
267
+ // Private Constants
268
+ // ─────────────────────────────────────────────────────────────────────────────
269
+
270
+ const STATIC_EXTENSIONS = new Set([
271
+ // ── Styles ──────────────────────────────────────────────────────────────
272
+ "css", "less", "scss", "sass", "styl", "stylus", "pcss", "postcss", "sss",
273
+
274
+ // ── Scripts & Source Maps ───────────────────────────────────────────────
275
+ "js", "mjs", "cjs", "ts", "mts", "cts", "jsx", "tsx",
276
+ "map", "coffee", "litcoffee", "elm", "dart", "vue", "svelte",
277
+
278
+ // ── Images (Raster) ────────────────────────────────────────────────────
279
+ "png", "jpg", "jpeg", "jpe", "jfif", "gif", "bmp", "dib",
280
+ "tiff", "tif", "webp", "avif", "heic", "heif", "ico", "cur",
281
+ "psd", "psb", "ai", "eps", "raw", "cr2", "nef", "orf", "sr2", "arw",
282
+ "dng", "raf", "rw2", "pef", "srw", "x3f", "jxl", "qoi", "hdr", "exr",
283
+
284
+ // ── Images (Vector) ────────────────────────────────────────────────────
285
+ "svg", "svgz", "cgm", "emf", "wmf",
286
+
287
+ // ── Fonts ───────────────────────────────────────────────────────────────
288
+ "woff", "woff2", "ttf", "otf", "eot", "pfb", "pfm",
289
+
290
+ // ── Video ───────────────────────────────────────────────────────────────
291
+ "mp4", "m4v", "webm", "ogv", "ogg", "avi", "mov", "mkv", "flv", "wmv",
292
+ "mpg", "mpeg", "mpe", "3gp", "3g2", "ts", "mts", "m2ts", "vob", "divx",
293
+ "asf", "rm", "rmvb", "f4v", "swf", "hevc", "av1",
294
+
295
+ // ── Audio ───────────────────────────────────────────────────────────────
296
+ "mp3", "wav", "flac", "aac", "m4a", "wma", "opus", "oga",
297
+ "aiff", "aif", "aifc", "mid", "midi", "amr", "ape", "dsf", "dff",
298
+ "ac3", "dts", "pcm", "au", "snd", "ra", "tak", "tta", "wv", "caf",
299
+
300
+ // ── Documents & Office ──────────────────────────────────────────────────
301
+ "pdf", "doc", "docx", "docm", "dot", "dotx", "dotm",
302
+ "xls", "xlsx", "xlsm", "xlsb", "xlt", "xltx", "xltm",
303
+ "ppt", "pptx", "pptm", "pot", "potx", "potm", "pps", "ppsx",
304
+ "odt", "ods", "odp", "odg", "odf", "odb", "odc",
305
+ "rtf", "tex", "latex", "bib", "epub", "mobi", "azw", "azw3",
306
+ "djvu", "djv", "xps", "oxps", "pages", "numbers", "key", "keynote",
307
+
308
+ // ── Data & Config ───────────────────────────────────────────────────────
309
+ "xml", "json", "jsonl", "ndjson", "json5", "jsonc",
310
+ "yaml", "yml", "toml", "ini", "cfg", "conf", "properties",
311
+ "csv", "tsv", "dsv", "parquet", "avro", "arrow", "feather",
312
+ "sql", "sqlite", "db", "mdb", "accdb",
313
+
314
+ // ── Text & Markup ───────────────────────────────────────────────────────
315
+ "txt", "text", "md", "markdown", "rst", "adoc", "asciidoc",
316
+ "log", "diff", "patch", "nfo", "diz",
317
+ "htm", "html", "xhtml", "mhtml", "mht", "shtml",
318
+ "ics", "ical", "vcf", "vcard", "ldif",
319
+
320
+ // ── Archives & Compression ──────────────────────────────────────────────
321
+ "zip", "rar", "7z", "tar", "gz", "gzip", "bz2", "bzip2",
322
+ "xz", "lzma", "lz", "lz4", "zst", "zstd", "br", "brotli",
323
+ "cab", "arj", "ace", "lzh", "lha", "cpio", "rpm", "deb",
324
+ "pkg", "dmg", "iso", "img", "vhd", "vhdx", "vmdk", "ova", "ovf",
325
+ "apk", "ipa", "xpi", "crx", "nupkg", "snap", "flatpak", "appimage",
326
+ "jar", "war", "ear",
327
+
328
+ // ── Executables & Libraries ─────────────────────────────────────────────
329
+ "exe", "msi", "dll", "sys", "so", "dylib", "lib", "a", "o", "obj",
330
+ "bin", "dat", "com", "bat", "cmd", "sh", "bash", "ps1", "psm1",
331
+
332
+ // ── 3D & CAD & Design ───────────────────────────────────────────────────
333
+ "glb", "gltf", "obj", "stl", "fbx", "dae", "3ds", "blend", "max",
334
+ "c4d", "ma", "mb", "lwo", "lws", "ply", "wrl", "vrml", "x3d",
335
+ "dwg", "dxf", "dgn", "step", "stp", "iges", "igs", "sat",
336
+ "skp", "3dm", "usd", "usda", "usdc", "usdz",
337
+
338
+ // ── GIS & Mapping ───────────────────────────────────────────────────────
339
+ "shp", "shx", "dbf", "prj", "cpg", "geojson", "topojson",
340
+ "kml", "kmz", "gpx", "osm", "pbf", "mbtiles",
341
+
342
+ // ── Scientific & Research ───────────────────────────────────────────────
343
+ "hdf5", "h5", "hdf", "nc", "netcdf", "fits", "mat", "sav",
344
+
345
+ // ── WebAssembly & Runtime ───────────────────────────────────────────────
346
+ "wasm", "wat", "swc",
347
+
348
+ // ── Certificates & Keys ─────────────────────────────────────────────────
349
+ "pem", "crt", "cer", "der", "p12", "pfx", "p7b", "p7c",
350
+ "csr", "key", "pub", "asc", "gpg", "sig",
351
+
352
+ // ── Web Manifests & PWA ─────────────────────────────────────────────────
353
+ "webmanifest", "manifest", "browserconfig",
354
+
355
+ // ── Misc ────────────────────────────────────────────────────────────────
356
+ "ani", "icns", "lnk", "url", "webloc", "desktop",
357
+ "torrent", "metalink", "magnet",
358
+ "sub", "srt", "ass", "ssa", "vtt", "sbv", "dfxp", "ttml",
359
+ "bak", "tmp", "temp", "swp", "swo", "lock",
360
+ "pid", "socket", "fifo"
361
+ ]);
362
+
220
363
  export default SessionManager;
@@ -0,0 +1,117 @@
1
+ import { createClient } from "redis";
2
+ import Log from "../Logging/Log.js";
3
+
4
+ /**
5
+ * Redis-based session storage.
6
+ * Stores sessions as JSON strings with automatic TTL expiry.
7
+ */
8
+ class RedisStore {
9
+ #client;
10
+ #prefix;
11
+ #ttl;
12
+ ready;
13
+
14
+ constructor(lifetime) {
15
+ this.#prefix = (process.env.APP_NAME || "app") + ":session:";
16
+ this.#ttl = Math.ceil(lifetime / 1000);
17
+ this.ready = this.#connect();
18
+ }
19
+
20
+ /**
21
+ * Gets session data by ID.
22
+ * @param {string} id
23
+ * @returns {Promise<Object|null>}
24
+ */
25
+ async get(id) {
26
+ const data = await this.#client.get(this.#prefix + id);
27
+
28
+ if (!data) {
29
+ return null;
30
+ }
31
+
32
+ try {
33
+ return JSON.parse(data);
34
+ }
35
+ catch {
36
+ await this.delete(id);
37
+
38
+ return null;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Stores session data with automatic TTL.
44
+ * @param {string} id
45
+ * @param {Object} value
46
+ */
47
+ async set(id, value) {
48
+ await this.#client.set(this.#prefix + id, JSON.stringify(value), {
49
+ EX: this.#ttl
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Deletes a session.
55
+ * @param {string} id
56
+ */
57
+ async delete(id) {
58
+ await this.#client.del(this.#prefix + id);
59
+ }
60
+
61
+ /**
62
+ * Garbage collection — no-op for Redis.
63
+ * Redis handles expiry automatically via TTL.
64
+ * @returns {Promise<number>}
65
+ */
66
+ async gc() {
67
+ return 0;
68
+ }
69
+
70
+ /**
71
+ * Closes the Redis connection gracefully.
72
+ */
73
+ async close() {
74
+ if (this.#client) {
75
+ await this.#client.quit();
76
+ this.#client = null;
77
+ }
78
+ }
79
+
80
+ /** @private */
81
+ async #connect() {
82
+ const host = process.env.REDIS_HOST || "127.0.0.1";
83
+ const port = Number(process.env.REDIS_PORT) || 6379;
84
+ const password = process.env.REDIS_PASSWORD || undefined;
85
+
86
+ this.#client = createClient({
87
+ socket: {
88
+ host,
89
+ port,
90
+ reconnectStrategy: (retries) => {
91
+ if (retries > 10) {
92
+ console.error("\x1b[31m✕ [Session Redis] Connection lost after 10 retries. Shutting down.\x1b[0m");
93
+ Log.fatal("Redis connection lost permanently", { retries });
94
+ process.emit("SIGTERM");
95
+
96
+ return false;
97
+ }
98
+
99
+ return Math.min(retries * 100, 5000);
100
+ }
101
+ },
102
+ password
103
+ });
104
+
105
+ this.#client.on("error", (err) => {
106
+ console.error(`\x1b[31m✕ [Session Redis] ${err.message}\x1b[0m`);
107
+ });
108
+
109
+ this.#client.on("ready", () => {
110
+ console.log("\x1b[32m✓ [Session Redis] Connected\x1b[0m");
111
+ });
112
+
113
+ await this.#client.connect();
114
+ }
115
+ }
116
+
117
+ export default RedisStore;
@@ -31,10 +31,6 @@ class Session {
31
31
  return this.#createdAt;
32
32
  }
33
33
 
34
- get lastActivity() {
35
- return this.#lastActivity;
36
- }
37
-
38
34
  /**
39
35
  * Gets a session value.
40
36
  * @param {string} key
@@ -1,24 +1,26 @@
1
+ import { randomBytes } from "crypto";
2
+
1
3
  /**
2
4
  * String utility class with helper methods.
3
5
  * Provides Laravel-style string manipulation functions.
4
- *
6
+ *
5
7
  * @example
6
8
  * Str.slug("Hello World"); // "hello-world"
7
9
  * Str.camel("user_name"); // "userName"
8
10
  */
9
11
  class Str {
10
12
  /**
11
- * Generates a random alphanumeric string.
13
+ * Generates a cryptographically secure random alphanumeric string.
12
14
  * @param {number} length - Desired string length
13
15
  * @returns {string} Random string
14
16
  */
15
17
  static random(length) {
16
18
  const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
19
+ const bytes = randomBytes(length);
17
20
  let result = "";
18
- const charactersLength = characters.length;
19
21
 
20
22
  for (let i = 0; i < length; i++) {
21
- result += characters.charAt(Math.floor(Math.random() * charactersLength));
23
+ result += characters.charAt(bytes[i] % characters.length);
22
24
  }
23
25
 
24
26
  return result;