@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
@@ -1,20 +1,40 @@
1
+ /**
2
+ * SPA Client — Client-side navigation using RSC (React Server Components).
3
+ *
4
+ * Intercepts local <a> clicks and browser back/forward events,
5
+ * fetches a fresh RSC Flight payload from /__nitron/rsc, and lets
6
+ * React reconcile the DOM without a full page reload.
7
+ *
8
+ * Wire format from /__nitron/rsc:
9
+ * "<flightLength>\n<flightPayload><jsonMetadata>"
10
+ * - First line: byte length of the Flight payload
11
+ * - Then the Flight payload itself (React serialization format)
12
+ * - Then a JSON object with { meta, css, translations }
13
+ */
1
14
  (function() {
2
15
  "use strict";
3
16
 
4
- var ENDPOINT = "/__nitron/navigate";
17
+ var RSC_ENDPOINT = "/__nitron/rsc";
18
+
19
+ // URLs that should never be intercepted by SPA navigation
5
20
  var SKIP = /^(\/storage|\/api|\/__|#|javascript:|mailto:|tel:)/;
6
- var layouts = [];
21
+
7
22
  var navigating = false;
8
23
 
9
- // Route helper function for client-side
24
+ // --- Client-side route() helper ---
25
+
26
+ // Mirrors the server-side route() global — resolves named routes to URL paths.
27
+ // Route definitions are injected by the server into window.__NITRON_RUNTIME__.routes.
10
28
  globalThis.route = function(name, params) {
11
29
  var runtime = window.__NITRON_RUNTIME__;
30
+
12
31
  if (!runtime || !runtime.routes) {
13
32
  console.error("Route runtime not initialized");
14
33
  return "";
15
34
  }
16
35
 
17
36
  var pattern = runtime.routes[name];
37
+
18
38
  if (!pattern) {
19
39
  console.error('Route "' + name + '" not found');
20
40
  return "";
@@ -28,89 +48,140 @@
28
48
  });
29
49
  };
30
50
 
51
+ // --- Link Interception ---
52
+
31
53
  function init() {
32
- var rt = window.__NITRON_RUNTIME__;
33
- if (rt && rt.layouts) layouts = rt.layouts;
54
+ // Capture phase (true) so we intercept before any stopPropagation
34
55
  document.addEventListener("click", onClick, true);
35
56
  window.addEventListener("popstate", onPopState);
36
57
  }
37
58
 
59
+ // Intercepts <a> clicks — if the link is local, navigates via RSC instead of full reload
38
60
  function onClick(e) {
39
61
  if (navigating) return;
40
62
  var link = e.target.closest("a");
63
+
41
64
  if (!link) return;
65
+
42
66
  var href = link.getAttribute("href");
67
+
43
68
  if (!href || link.target === "_blank" || link.download) return;
44
69
  if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
45
70
  if (!isLocal(href)) return;
71
+
46
72
  e.preventDefault();
47
73
  navigate(href, true);
48
74
  }
49
75
 
76
+ // Browser back/forward — re-fetch RSC payload for the new URL
50
77
  function onPopState() {
51
78
  navigate(location.pathname + location.search, false);
52
79
  }
53
80
 
81
+ // Returns true if the href points to the same origin and is not in the SKIP list
54
82
  function isLocal(href) {
55
83
  if (!href || SKIP.test(href)) return false;
84
+
85
+ // Absolute URLs — check if same origin
56
86
  if (/^https?:\/\/|^\/\//.test(href)) {
57
87
  try { return new URL(href, location.origin).origin === location.origin; }
58
88
  catch (e) { return false; }
59
89
  }
90
+
60
91
  return true;
61
92
  }
62
93
 
94
+ // --- RSC Wire Format Parser ---
95
+
96
+ // Parses the length-prefixed response from /__nitron/rsc.
97
+ // Format: "<length>\n<flight payload><json metadata>"
98
+ // Returns { payload, meta, css, translations } or null on malformed input.
99
+ function parseLengthPrefixed(text) {
100
+ var nl = text.indexOf("\n");
101
+ if (nl === -1) return null;
102
+
103
+ var len = parseInt(text.substring(0, nl), 10);
104
+ if (isNaN(len) || len < 0) return null;
105
+
106
+ var flight = text.substring(nl + 1, nl + 1 + len);
107
+ var jsonStr = text.substring(nl + 1 + len);
108
+ var data;
109
+
110
+ try { data = JSON.parse(jsonStr); }
111
+ catch (e) { return null; }
112
+
113
+ return {
114
+ payload: flight,
115
+ meta: data.meta || null,
116
+ css: data.css || null,
117
+ translations: data.translations || null
118
+ };
119
+ }
120
+
121
+ // --- SPA Navigation ---
122
+
123
+ // Core navigation: fetches RSC payload and lets React reconcile.
124
+ // Falls back to full page load on error or timeout (10s).
63
125
  function navigate(url, push) {
64
126
  if (navigating) return;
127
+
128
+ var rsc = window.__NITRON_RSC__;
129
+
130
+ // RSC root not available (e.g. initial page was server-rendered without hydration)
131
+ if (!rsc || !rsc.root) {
132
+ location.href = url;
133
+ return;
134
+ }
135
+
65
136
  navigating = true;
66
137
 
138
+ // Abort + fallback after 10 seconds to prevent hanging navigations
67
139
  var ctrl = typeof AbortController !== "undefined" ? new AbortController() : null;
68
140
  var timer = setTimeout(function() { if (ctrl) ctrl.abort(); fallback(); }, 10000);
69
141
 
70
- fetch(ENDPOINT + "?url=" + encodeURIComponent(url), {
142
+ fetch(RSC_ENDPOINT + "?url=" + encodeURIComponent(url), {
71
143
  headers: { "X-Nitron-SPA": "1" },
72
144
  credentials: "same-origin",
73
145
  signal: ctrl ? ctrl.signal : undefined
74
146
  })
75
147
  .then(function(r) {
76
148
  clearTimeout(timer);
149
+
77
150
  if (!r.ok) throw new Error("HTTP " + r.status);
78
- return r.json();
151
+
152
+ return r.text().then(function(text) {
153
+ return parseLengthPrefixed(text);
154
+ });
79
155
  })
80
156
  .then(function(d) {
81
- if (d.error) { fallback(); return; }
82
- if (d.redirect) {
83
- navigating = false;
84
- isLocal(d.redirect) ? navigate(d.redirect, push) : (location.href = d.redirect);
85
- return;
86
- }
87
-
88
- if (!layouts.length || !arrEq(layouts, d.layouts || [])) {
89
- fallback();
90
- return;
91
- }
157
+ if (!d || !d.payload) { fallback(); return; }
92
158
 
93
- updateSlot(d.html);
94
- layouts = d.layouts || [];
95
- if (window.__NITRON_RUNTIME__) window.__NITRON_RUNTIME__.layouts = layouts;
159
+ // Merge translations BEFORE rsc.navigate — persistent components (layouts) keep their keys
160
+ if (d.translations) {
161
+ var prev = window.__NITRON_TRANSLATIONS__;
96
162
 
97
- if (d.hydrationScript) {
98
- if (d.runtime) Object.assign(window.__NITRON_RUNTIME__ || {}, d.runtime);
99
- if (d.props) window.__NITRON_PROPS__ = d.props;
100
- loadScript("/storage" + d.hydrationScript + "?t=" + Date.now(), true);
163
+ window.__NITRON_TRANSLATIONS__ = prev
164
+ ? Object.assign({}, prev, d.translations)
165
+ : d.translations;
101
166
  }
102
167
 
168
+ // Use RSC consumer to render new payload via React reconciliation
169
+ rsc.navigate(d.payload);
170
+
103
171
  if (push) history.pushState({ nitron: 1 }, "", url);
104
- if (d.meta) {
105
- if (d.meta.title) document.title = d.meta.title;
106
- setMeta("description", d.meta.description);
107
- }
172
+
173
+ if (d.meta && d.meta.title) document.title = d.meta.title;
174
+ setMeta("description", d.meta ? d.meta.description : null);
175
+
176
+ // Load new CSS stylesheets if needed
177
+ if (d.css) loadNewCss(d.css);
108
178
 
109
179
  scrollTo(0, 0);
110
180
  dispatchEvent(new CustomEvent("nitron:navigate", { detail: { url: url } }));
111
181
  navigating = false;
112
182
  })
113
183
  .catch(function() {
184
+ // RSC fetch failed or aborted — full page load as last resort
114
185
  clearTimeout(timer);
115
186
  fallback();
116
187
  });
@@ -121,34 +192,32 @@
121
192
  }
122
193
  }
123
194
 
124
- function arrEq(a, b) {
125
- if (!a || !b || a.length !== b.length) return false;
126
- for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
127
- return true;
128
- }
195
+ // --- CSS & Meta Helpers ---
129
196
 
130
- function updateSlot(html) {
131
- var slot = document.querySelector("[data-nitron-slot='page']");
132
- if (!slot) {
133
- location.reload();
134
- return;
135
- }
136
- var tmp = document.createElement("div");
137
- tmp.innerHTML = html;
138
- var inner = tmp.querySelector("[data-nitron-slot='page']");
139
- slot.innerHTML = inner ? inner.innerHTML : html;
140
- }
197
+ // Loads CSS files that the new page needs but the current page doesn't have
198
+ function loadNewCss(cssFiles) {
199
+ var existing = new Set();
200
+
201
+ document.querySelectorAll('link[rel="stylesheet"]').forEach(function(link) {
202
+ existing.add(link.getAttribute("href"));
203
+ });
141
204
 
142
- function loadScript(src, isModule) {
143
- var s = document.createElement("script");
144
- if (isModule) s.type = "module";
145
- s.src = src;
146
- document.body.appendChild(s);
205
+ for (var i = 0; i < cssFiles.length; i++) {
206
+ if (!existing.has(cssFiles[i])) {
207
+ var link = document.createElement("link");
208
+ link.rel = "stylesheet";
209
+ link.href = cssFiles[i];
210
+ document.head.appendChild(link);
211
+ }
212
+ }
147
213
  }
148
214
 
215
+ // Creates or updates a <meta> tag (e.g. description)
149
216
  function setMeta(name, val) {
150
217
  if (!val) return;
218
+
151
219
  var m = document.querySelector('meta[name="' + name + '"]');
220
+
152
221
  if (m) m.content = val;
153
222
  else {
154
223
  m = document.createElement("meta");
@@ -158,6 +227,8 @@
158
227
  }
159
228
  }
160
229
 
230
+ // --- Initialize ---
231
+
161
232
  document.readyState === "loading"
162
233
  ? document.addEventListener("DOMContentLoaded", init)
163
234
  : init();
@@ -0,0 +1,60 @@
1
+ import { existsSync, readFileSync, statSync } from "fs";
2
+ import path from "path";
3
+ import Paths from "../Core/Paths.js";
4
+ import Environment from "../Core/Environment.js";
5
+
6
+ const MANIFEST_PATH = path.join(Paths.build, "client-manifest.json");
7
+
8
+ /**
9
+ * Maps "use client" components to their browser-side chunk locations.
10
+ * Build writes client-manifest.json, this class loads it at render time.
11
+ *
12
+ * The Flight renderer looks up client components in this manifest to know
13
+ * which JS file the browser should load for each client component reference.
14
+ *
15
+ * Format (what react-server-dom-webpack expects):
16
+ * {
17
+ * "file:///abs/path/to/SearchBar.tsx": {
18
+ * id: "SearchBar",
19
+ * chunks: ["SearchBar", "js/Components/SearchBar.js"],
20
+ * name: "*"
21
+ * }
22
+ * }
23
+ */
24
+ class ClientManifest {
25
+ static #entries = null;
26
+ static #mtime = null;
27
+
28
+ /**
29
+ * Get the manifest object for FlightRenderer.
30
+ * Loads from client-manifest.json with mtime-based cache busting in dev.
31
+ *
32
+ * @returns {object}
33
+ */
34
+ static get() {
35
+ if (Environment.isDev && this.#entries) {
36
+ try {
37
+ const mtime = statSync(MANIFEST_PATH).mtimeMs;
38
+
39
+ if (mtime !== this.#mtime) {
40
+ this.#entries = null;
41
+ }
42
+ }
43
+ catch {}
44
+ }
45
+
46
+ if (!this.#entries) {
47
+ if (!existsSync(MANIFEST_PATH)) {
48
+ return {};
49
+ }
50
+
51
+ this.#entries = JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
52
+ this.#mtime = statSync(MANIFEST_PATH).mtimeMs;
53
+ }
54
+
55
+ return this.#entries;
56
+ }
57
+
58
+ }
59
+
60
+ export default ClientManifest;
@@ -0,0 +1,100 @@
1
+ import { createRequire } from "module";
2
+ import path from "path";
3
+ import { PassThrough } from "stream";
4
+ import React from "react";
5
+ import Environment from "../Core/Environment.js";
6
+
7
+ const require = createRequire(import.meta.url);
8
+
9
+ // The Flight renderer expects React's server internals (H = hooks dispatcher,
10
+ // A = async dispatcher). Normal React only exports __CLIENT_INTERNALS_..., so we
11
+ // provide __SERVER_INTERNALS_... as a separate object. Both dispatchers are set by
12
+ // their respective renderers before rendering, so parallel execution is safe:
13
+ // - react-dom/server uses __CLIENT_INTERNALS_... → no conflict
14
+ // - Flight renderer uses __SERVER_INTERNALS_... → no conflict
15
+ if (!React.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE) {
16
+ React.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE = { H: null, A: null };
17
+ }
18
+
19
+ // Load production Flight renderer via absolute path (bypasses package.json exports).
20
+ const serverNodePath = require.resolve("react-server-dom-webpack/server.node");
21
+ const prodPath = path.join(path.dirname(serverNodePath), "cjs/react-server-dom-webpack-server.node.production.js");
22
+
23
+ const { renderToPipeableStream } = require(prodPath);
24
+
25
+ /**
26
+ * Produces RSC Flight payloads from React element trees.
27
+ * Flight payload = serialized React element tree that the browser React can reconcile.
28
+ *
29
+ * Server components become inline data, client components become import references.
30
+ * The browser React reads the payload and builds its virtual DOM from it,
31
+ * enabling surgical DOM updates via reconciliation.
32
+ */
33
+ class FlightRenderer {
34
+ /**
35
+ * Render a React element tree to an RSC Flight payload string.
36
+ *
37
+ * @param {React.ReactElement} element - The full component tree (page + layouts)
38
+ * @param {object} clientManifest - Maps client component IDs to chunk info
39
+ * @param {object} [options] - Optional: onError callback
40
+ * @returns {Promise<string>} - The complete Flight payload
41
+ */
42
+ static render(element, clientManifest, options = {}) {
43
+ return new Promise((resolve, reject) => {
44
+ const chunks = [];
45
+ const stream = new PassThrough();
46
+ let done = false;
47
+ let renderError = null;
48
+
49
+ const finish = (result) => {
50
+ if (done) return;
51
+ done = true;
52
+ clearTimeout(timer);
53
+
54
+ if (renderError) {
55
+ reject(renderError);
56
+ }
57
+ else {
58
+ resolve(result);
59
+ }
60
+ };
61
+
62
+ const timeout = Environment.isDev ? 10000 : 3000;
63
+ const timer = setTimeout(() => {
64
+ const error = new Error("RSC render timeout - component took too long");
65
+ error.statusCode = 500;
66
+ renderError = error;
67
+ stream.destroy();
68
+ finish(null);
69
+ }, timeout);
70
+
71
+ stream.on("data", chunk => chunks.push(chunk));
72
+ stream.on("end", () => finish(Buffer.concat(chunks).toString("utf-8")));
73
+ stream.on("error", (error) => {
74
+ renderError = error;
75
+ finish(null);
76
+ });
77
+
78
+ try {
79
+ const { pipe } = renderToPipeableStream(element, clientManifest, {
80
+ onError: (error) => {
81
+ if (options.onError) {
82
+ options.onError(error);
83
+ }
84
+ renderError = error;
85
+ finish(null);
86
+ }
87
+ });
88
+
89
+ pipe(stream);
90
+ }
91
+ catch (error) {
92
+ renderError = error;
93
+ finish(null);
94
+ }
95
+ });
96
+ }
97
+
98
+ }
99
+
100
+ export default FlightRenderer;
@@ -86,9 +86,6 @@ class Layout {
86
86
  return LAYOUT_NAMES.has(basename);
87
87
  }
88
88
 
89
- static clearCache() {
90
- this.#cache.clear();
91
- }
92
89
  }
93
90
 
94
91
  export default Layout;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Runtime prop filtering for Flight payload security.
3
+ * Strips unused data from props before Flight serialization
4
+ * based on build-time AST analysis of component prop usage.
5
+ */
6
+
7
+ /** Props that always pass through without filtering. */
8
+ const BYPASS_PROPS = new Set(["children", "key", "ref"]);
9
+
10
+
11
+ class PropFilter {
12
+ /**
13
+ * Filter props before Flight serialization.
14
+ *
15
+ * @param {object} params - Raw props from the controller.
16
+ * @param {object|null} propUsage - Usage tree from build manifest (null = passthrough).
17
+ * @returns {object} Filtered props safe for client delivery.
18
+ */
19
+ static apply(params, propUsage) {
20
+ if (!params || typeof params !== "object") return params;
21
+ if (!propUsage || typeof propUsage !== "object") return params;
22
+
23
+ return pruneByUsage(params, propUsage);
24
+ }
25
+ }
26
+
27
+
28
+ /**
29
+ * Prunes an object to only include properties matching the usage tree.
30
+ *
31
+ * Usage tree format:
32
+ * - true → pass the value as-is
33
+ * - { sub: ... } → pass only matching sub-properties
34
+ * - absent key → strip entirely
35
+ *
36
+ * @param {*} data - Data to prune.
37
+ * @param {object|true} usage - Usage tree node.
38
+ * @returns {*} Pruned data.
39
+ */
40
+ function pruneByUsage(data, usage) {
41
+ if (usage === true) return data;
42
+
43
+ if (data === null || data === undefined) return data;
44
+ if (typeof data !== "object") return data;
45
+
46
+ if (Array.isArray(data)) {
47
+ if (usage["[]"]) {
48
+ return data.map(item => pruneByUsage(item, usage["[]"]));
49
+ }
50
+ // No element iteration in usage — fall through to object pruning
51
+ // to preserve scalar properties (e.g. length) without leaking elements.
52
+ }
53
+
54
+ const result = Object.create(null);
55
+
56
+ for (const key of Object.keys(usage)) {
57
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
58
+ if (!(key in data)) continue;
59
+
60
+ const subUsage = usage[key];
61
+ const value = data[key];
62
+
63
+ if (subUsage === true) {
64
+ result[key] = value;
65
+ }
66
+ else if (subUsage && typeof subUsage === "object") {
67
+ result[key] = pruneByUsage(value, subUsage);
68
+ }
69
+ }
70
+
71
+ // Bypass props always pass through even if not in usage tree
72
+ for (const bp of BYPASS_PROPS) {
73
+ if (bp in data && !(bp in result)) {
74
+ result[bp] = data[bp];
75
+ }
76
+ }
77
+
78
+ return result;
79
+ }
80
+
81
+ export default PropFilter;