@nitronjs/framework 0.3.0 → 0.3.2
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/cli/create.js +6 -3
- package/cli/njs.js +26 -23
- package/lib/Auth/Auth.js +9 -4
- package/lib/Auth/Mfa.js +518 -0
- package/lib/Build/FileAnalyzer.js +33 -5
- package/lib/Build/Manager.js +22 -7
- package/lib/Build/PropUsageAnalyzer.js +123 -12
- package/lib/Console/Commands/BuildCommand.js +2 -2
- package/lib/Console/Commands/DevCommand.js +18 -9
- package/lib/Console/Commands/MakeCommand.js +2 -2
- package/lib/Console/Commands/MigrateCommand.js +1 -1
- package/lib/Console/Commands/MigrateFreshCommand.js +1 -1
- package/lib/Console/Commands/MigrateRollbackCommand.js +1 -1
- package/lib/Console/Commands/MigrateStatusCommand.js +1 -1
- package/lib/Console/Commands/SeedCommand.js +1 -1
- package/lib/Console/Commands/StartCommand.js +2 -1
- package/lib/Console/Commands/StorageLinkCommand.js +21 -32
- package/lib/Console/Stubs/rsc-consumer.tsx +17 -1
- package/lib/Console/Stubs/vendor-dev.tsx +31 -0
- package/lib/Console/Stubs/vendor.tsx +31 -0
- package/lib/Date/DateTime.js +12 -10
- package/lib/Http/Server.js +3 -2
- package/lib/Mail/Mail.js +22 -19
- package/lib/Runtime/Entry.js +1 -1
- package/lib/View/Client/spa.js +196 -112
- package/lib/View/FlightRenderer.js +5 -1
- package/lib/View/View.js +49 -1
- package/lib/index.js +1 -0
- package/package.json +3 -1
package/lib/Date/DateTime.js
CHANGED
|
@@ -22,12 +22,14 @@ class DateTime {
|
|
|
22
22
|
const lang = locale[Config.get('app.locale', 'en')] || locale.en;
|
|
23
23
|
const pad = (n) => String(n).padStart(2, '0');
|
|
24
24
|
|
|
25
|
-
// Replace multi-character tokens first
|
|
25
|
+
// Replace multi-character tokens first using non-alpha placeholders so the
|
|
26
|
+
// single-character replacement pass below cannot accidentally corrupt them.
|
|
27
|
+
// \x10-\x13 are control characters not present in the format token regex.
|
|
26
28
|
let result = formatString;
|
|
27
|
-
result = result.replace(/MMMM/g, '\
|
|
28
|
-
result = result.replace(/MMM/g, '\
|
|
29
|
-
result = result.replace(/DDDD/g, '\
|
|
30
|
-
result = result.replace(/DDD/g, '\
|
|
29
|
+
result = result.replace(/MMMM/g, '\x10');
|
|
30
|
+
result = result.replace(/MMM/g, '\x11');
|
|
31
|
+
result = result.replace(/DDDD/g, '\x12');
|
|
32
|
+
result = result.replace(/DDD/g, '\x13');
|
|
31
33
|
|
|
32
34
|
const map = {
|
|
33
35
|
'Y': date.getFullYear(),
|
|
@@ -47,11 +49,11 @@ class DateTime {
|
|
|
47
49
|
|
|
48
50
|
result = result.replace(/[YynmdjHislDMF]/g, match => map[match]);
|
|
49
51
|
|
|
50
|
-
// Restore multi-character tokens
|
|
51
|
-
result = result.replace(/\
|
|
52
|
-
result = result.replace(/\
|
|
53
|
-
result = result.replace(/\
|
|
54
|
-
result = result.replace(/\
|
|
52
|
+
// Restore multi-character tokens from their safe placeholders
|
|
53
|
+
result = result.replace(/\x10/g, lang.months[date.getMonth()]);
|
|
54
|
+
result = result.replace(/\x11/g, lang.monthsShort[date.getMonth()]);
|
|
55
|
+
result = result.replace(/\x12/g, lang.days[date.getDay()]);
|
|
56
|
+
result = result.replace(/\x13/g, lang.daysShort[date.getDay()]);
|
|
55
57
|
|
|
56
58
|
return result;
|
|
57
59
|
}
|
package/lib/Http/Server.js
CHANGED
|
@@ -60,7 +60,8 @@ class Server {
|
|
|
60
60
|
|
|
61
61
|
if (!process.env.APP_KEY || process.env.APP_KEY.trim() === "") {
|
|
62
62
|
console.error("\x1b[31m✕ APP_KEY is not set. Please set APP_KEY in your .env file before starting the server.\x1b[0m");
|
|
63
|
-
process.
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
this.#config = Config.all("server");
|
|
@@ -484,7 +485,7 @@ class Server {
|
|
|
484
485
|
const banner = this.#renderBanner({ success: false, error: err });
|
|
485
486
|
if (banner) console.log(banner);
|
|
486
487
|
Log.fatal("Server startup failed", { error: err.message, code: err.code, stack: err.stack });
|
|
487
|
-
process.
|
|
488
|
+
process.exitCode = 1;
|
|
488
489
|
}
|
|
489
490
|
}
|
|
490
491
|
|
package/lib/Mail/Mail.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import nodemailer from "nodemailer";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { createElement } from "react";
|
|
6
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
4
7
|
import Paths from "../Core/Paths.js";
|
|
5
8
|
|
|
6
9
|
/**
|
|
@@ -22,6 +25,7 @@ class Mail {
|
|
|
22
25
|
#htmlContent = null;
|
|
23
26
|
#attachments = [];
|
|
24
27
|
#calendarContent = null;
|
|
28
|
+
#viewTemplate = null;
|
|
25
29
|
|
|
26
30
|
/**
|
|
27
31
|
* Creates a new Mail instance with sender address.
|
|
@@ -125,23 +129,21 @@ class Mail {
|
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
/**
|
|
128
|
-
* Sets HTML content from a
|
|
129
|
-
*
|
|
130
|
-
* Placeholders use {{ key }} syntax.
|
|
132
|
+
* Sets HTML content from a React component template (.tsx/.jsx).
|
|
133
|
+
* Uses the built version from .nitron/build/views/ and renders to static HTML.
|
|
131
134
|
*
|
|
132
|
-
* @param {string} templateName - Template path relative to views dir (e.g. "
|
|
133
|
-
* @param {Object} data - Data to
|
|
135
|
+
* @param {string} templateName - Template path relative to views dir (e.g. "Mail/Welcome")
|
|
136
|
+
* @param {Object} data - Data to pass as props to the React component
|
|
134
137
|
* @returns {Mail} This instance for chaining
|
|
135
138
|
*/
|
|
136
139
|
view(templateName, data = {}) {
|
|
137
|
-
const
|
|
138
|
-
let content = fs.readFileSync(filePath, "utf-8");
|
|
140
|
+
const buildPath = path.join(Paths.buildViews, `${templateName}.js`);
|
|
139
141
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
if (!fs.existsSync(buildPath)) {
|
|
143
|
+
throw new Error(`Mail template not found: ${templateName}`);
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
this.#
|
|
146
|
+
this.#viewTemplate = { buildPath, data };
|
|
145
147
|
|
|
146
148
|
return this;
|
|
147
149
|
}
|
|
@@ -152,6 +154,16 @@ class Mail {
|
|
|
152
154
|
* @returns {Promise<Object>} Nodemailer send result
|
|
153
155
|
*/
|
|
154
156
|
async send(transportCallback = null) {
|
|
157
|
+
if (this.#viewTemplate) {
|
|
158
|
+
const { buildPath, data } = this.#viewTemplate;
|
|
159
|
+
const mod = await import(pathToFileURL(buildPath).href + "?t=" + Date.now());
|
|
160
|
+
const factory = mod.default;
|
|
161
|
+
const exports = typeof factory === "function" && mod.__factory ? await factory() : mod;
|
|
162
|
+
const Component = exports.default || exports;
|
|
163
|
+
const element = createElement(Component, data);
|
|
164
|
+
this.#htmlContent = renderToStaticMarkup(element);
|
|
165
|
+
}
|
|
166
|
+
|
|
155
167
|
let transporter;
|
|
156
168
|
|
|
157
169
|
if (typeof transportCallback === "function") {
|
|
@@ -206,13 +218,4 @@ class Mail {
|
|
|
206
218
|
}
|
|
207
219
|
}
|
|
208
220
|
|
|
209
|
-
function escapeHtml(str) {
|
|
210
|
-
return str
|
|
211
|
-
.replace(/&/g, "&")
|
|
212
|
-
.replace(/</g, "<")
|
|
213
|
-
.replace(/>/g, ">")
|
|
214
|
-
.replace(/"/g, """)
|
|
215
|
-
.replace(/'/g, "'");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
221
|
export default Mail;
|
package/lib/Runtime/Entry.js
CHANGED
package/lib/View/Client/spa.js
CHANGED
|
@@ -5,35 +5,34 @@
|
|
|
5
5
|
* fetches a fresh RSC Flight payload from /__nitron/rsc, and lets
|
|
6
6
|
* React reconcile the DOM without a full page reload.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 }
|
|
8
|
+
* RSC wire format: "<payloadLength>\n<flightPayload><jsonMetadata>"
|
|
9
|
+
* Redirect format: { "redirect": "/target/path" }
|
|
13
10
|
*/
|
|
14
11
|
(function() {
|
|
15
12
|
"use strict";
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
const RSC_ENDPOINT = "/__nitron/rsc";
|
|
15
|
+
const NAVIGATION_TIMEOUT = 10000;
|
|
16
|
+
const SKIP_PATTERN = /^(\/storage|\/api|\/__|#|javascript:|mailto:|tel:)/;
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
var SKIP = /^(\/storage|\/api|\/__|#|javascript:|mailto:|tel:)/;
|
|
18
|
+
let navigating = false;
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
// ============================================================
|
|
21
|
+
// Client-side route() helper
|
|
22
|
+
// ============================================================
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
// Route definitions are injected by the server into window.__NITRON_RUNTIME__.routes.
|
|
24
|
+
// Mirrors the server-side route() global.
|
|
25
|
+
// Resolves named routes to URL paths using route definitions
|
|
26
|
+
// injected by the server into window.__NITRON_RUNTIME__.routes.
|
|
28
27
|
globalThis.route = function(name, params) {
|
|
29
|
-
|
|
28
|
+
const runtime = window.__NITRON_RUNTIME__;
|
|
30
29
|
|
|
31
30
|
if (!runtime || !runtime.routes) {
|
|
32
31
|
console.error("Route runtime not initialized");
|
|
33
32
|
return "";
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
const pattern = runtime.routes[name];
|
|
37
36
|
|
|
38
37
|
if (!pattern) {
|
|
39
38
|
console.error('Route "' + name + '" not found');
|
|
@@ -42,92 +41,119 @@
|
|
|
42
41
|
|
|
43
42
|
if (!params) return pattern;
|
|
44
43
|
|
|
45
|
-
// Replace :param tokens with actual values
|
|
44
|
+
// Replace :param tokens with actual values (e.g. :id -> 5)
|
|
46
45
|
return pattern.replace(/:(\w+)/g, function(match, key) {
|
|
47
46
|
return params[key] !== undefined ? params[key] : match;
|
|
48
47
|
});
|
|
49
48
|
};
|
|
50
49
|
|
|
51
|
-
//
|
|
50
|
+
// ============================================================
|
|
51
|
+
// Initialization & Event Listeners
|
|
52
|
+
// ============================================================
|
|
52
53
|
|
|
53
54
|
function init() {
|
|
54
55
|
// Capture phase (true) so we intercept before any stopPropagation
|
|
55
|
-
document.addEventListener("click",
|
|
56
|
-
window.addEventListener("popstate",
|
|
56
|
+
document.addEventListener("click", handleLinkClick, true);
|
|
57
|
+
window.addEventListener("popstate", handleBackForward);
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
//
|
|
60
|
-
|
|
60
|
+
// When user clicks an <a> link, intercept it and navigate via RSC
|
|
61
|
+
// instead of doing a full page reload
|
|
62
|
+
function handleLinkClick(e) {
|
|
61
63
|
if (navigating) return;
|
|
62
|
-
|
|
64
|
+
|
|
65
|
+
const link = e.target.closest("a");
|
|
63
66
|
|
|
64
67
|
if (!link) return;
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
const href = link.getAttribute("href");
|
|
67
70
|
|
|
71
|
+
// Skip: no href, external tab, download links, modifier keys
|
|
68
72
|
if (!href || link.target === "_blank" || link.download) return;
|
|
69
73
|
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
|
|
70
|
-
if (!
|
|
74
|
+
if (!isLocalUrl(href)) return;
|
|
71
75
|
|
|
72
76
|
e.preventDefault();
|
|
73
77
|
navigate(href, true);
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
//
|
|
77
|
-
|
|
80
|
+
// When browser back/forward buttons are pressed,
|
|
81
|
+
// re-fetch the RSC payload for the current URL
|
|
82
|
+
function handleBackForward() {
|
|
78
83
|
navigate(location.pathname + location.search, false);
|
|
79
84
|
}
|
|
80
85
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
// Check if the URL belongs to this site (same origin) and is not
|
|
87
|
+
// a path we should skip (storage files, API routes, internal routes)
|
|
88
|
+
function isLocalUrl(href) {
|
|
89
|
+
if (!href || SKIP_PATTERN.test(href)) return false;
|
|
84
90
|
|
|
85
|
-
// Absolute URLs —
|
|
91
|
+
// Absolute URLs — compare origins
|
|
86
92
|
if (/^https?:\/\/|^\/\//.test(href)) {
|
|
87
|
-
try {
|
|
88
|
-
|
|
93
|
+
try {
|
|
94
|
+
return new URL(href, location.origin).origin === location.origin;
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
89
99
|
}
|
|
90
100
|
|
|
101
|
+
// Relative URLs are always local
|
|
91
102
|
return true;
|
|
92
103
|
}
|
|
93
104
|
|
|
94
|
-
//
|
|
105
|
+
// ============================================================
|
|
106
|
+
// RSC Response Parser
|
|
107
|
+
// ============================================================
|
|
95
108
|
|
|
96
|
-
// Parses the length-prefixed response from
|
|
109
|
+
// Parses the length-prefixed RSC response from the server.
|
|
97
110
|
// Format: "<length>\n<flight payload><json metadata>"
|
|
98
|
-
// Returns { payload, meta, css, translations } or null
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
// Returns { payload, meta, css, layouts, translations } or null if malformed.
|
|
112
|
+
function parseRscResponse(text) {
|
|
113
|
+
const newlineIndex = text.indexOf("\n");
|
|
114
|
+
|
|
115
|
+
if (newlineIndex === -1) return null;
|
|
102
116
|
|
|
103
|
-
|
|
104
|
-
if (isNaN(len) || len < 0) return null;
|
|
117
|
+
const payloadLength = parseInt(text.substring(0, newlineIndex), 10);
|
|
105
118
|
|
|
106
|
-
|
|
107
|
-
var jsonStr = text.substring(nl + 1 + len);
|
|
108
|
-
var data;
|
|
119
|
+
if (isNaN(payloadLength) || payloadLength < 0) return null;
|
|
109
120
|
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
const payloadStart = newlineIndex + 1;
|
|
122
|
+
const payload = text.substring(payloadStart, payloadStart + payloadLength);
|
|
123
|
+
const metadataStr = text.substring(payloadStart + payloadLength);
|
|
124
|
+
|
|
125
|
+
let metadata;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
metadata = JSON.parse(metadataStr);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
112
133
|
|
|
113
134
|
return {
|
|
114
|
-
payload:
|
|
115
|
-
meta:
|
|
116
|
-
css:
|
|
117
|
-
|
|
135
|
+
payload: payload,
|
|
136
|
+
meta: metadata.meta || null,
|
|
137
|
+
css: metadata.css || null,
|
|
138
|
+
layouts: metadata.layouts || null,
|
|
139
|
+
translations: metadata.translations || null
|
|
118
140
|
};
|
|
119
141
|
}
|
|
120
142
|
|
|
121
|
-
//
|
|
143
|
+
// ============================================================
|
|
144
|
+
// SPA Navigation
|
|
145
|
+
// ============================================================
|
|
122
146
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
147
|
+
// Main navigation function.
|
|
148
|
+
// Fetches the RSC payload from the server, then lets React update the DOM.
|
|
149
|
+
// If server returns a redirect, follows it via SPA (no full page reload).
|
|
150
|
+
// Falls back to full page load if anything goes wrong or takes too long.
|
|
151
|
+
async function navigate(url, push) {
|
|
126
152
|
if (navigating) return;
|
|
127
153
|
|
|
128
|
-
|
|
154
|
+
const rsc = window.__NITRON_RSC__;
|
|
129
155
|
|
|
130
|
-
// RSC
|
|
156
|
+
// If RSC is not set up, do a normal page load
|
|
131
157
|
if (!rsc || !rsc.root) {
|
|
132
158
|
location.href = url;
|
|
133
159
|
return;
|
|
@@ -135,76 +161,126 @@
|
|
|
135
161
|
|
|
136
162
|
navigating = true;
|
|
137
163
|
|
|
138
|
-
|
|
139
|
-
var ctrl = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
140
|
-
var timer = setTimeout(function() { if (ctrl) ctrl.abort(); fallback(); }, 10000);
|
|
141
|
-
|
|
142
|
-
fetch(RSC_ENDPOINT + "?url=" + encodeURIComponent(url), {
|
|
143
|
-
headers: { "X-Nitron-SPA": "1" },
|
|
144
|
-
credentials: "same-origin",
|
|
145
|
-
signal: ctrl ? ctrl.signal : undefined
|
|
146
|
-
})
|
|
147
|
-
.then(function(r) {
|
|
148
|
-
clearTimeout(timer);
|
|
164
|
+
const abortController = new AbortController();
|
|
149
165
|
|
|
150
|
-
|
|
166
|
+
// Safety timeout: if navigation takes more than 10 seconds,
|
|
167
|
+
// abort the fetch and fall back to a normal page load
|
|
168
|
+
const timeout = setTimeout(function() {
|
|
169
|
+
abortController.abort();
|
|
170
|
+
navigating = false;
|
|
171
|
+
location.href = url;
|
|
172
|
+
}, NAVIGATION_TIMEOUT);
|
|
151
173
|
|
|
152
|
-
|
|
153
|
-
|
|
174
|
+
try {
|
|
175
|
+
const response = await fetch(RSC_ENDPOINT + "?url=" + encodeURIComponent(url), {
|
|
176
|
+
headers: { "X-Nitron-SPA": "1" },
|
|
177
|
+
credentials: "same-origin",
|
|
178
|
+
signal: abortController.signal
|
|
154
179
|
});
|
|
155
|
-
})
|
|
156
|
-
.then(function(d) {
|
|
157
|
-
if (!d || !d.payload) { fallback(); return; }
|
|
158
180
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
181
|
+
clearTimeout(timeout);
|
|
182
|
+
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
throw new Error("HTTP " + response.status);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const contentType = response.headers.get("content-type") || "";
|
|
188
|
+
|
|
189
|
+
// Server returned a redirect (JSON response like { redirect: "/path" })
|
|
190
|
+
// Navigate to the redirect target via SPA instead of full page reload
|
|
191
|
+
if (contentType.indexOf("application/json") !== -1) {
|
|
192
|
+
const json = await response.json();
|
|
193
|
+
|
|
194
|
+
if (json.redirect) {
|
|
195
|
+
navigating = false;
|
|
196
|
+
navigate(json.redirect, true);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Normal RSC payload — parse it and render
|
|
203
|
+
const text = await response.text();
|
|
204
|
+
const data = parseRscResponse(text);
|
|
162
205
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
: d.translations;
|
|
206
|
+
if (!data || !data.payload) {
|
|
207
|
+
throw new Error("Invalid RSC response");
|
|
166
208
|
}
|
|
167
209
|
|
|
168
|
-
|
|
169
|
-
|
|
210
|
+
if (!layoutsMatch(data.layouts)) {
|
|
211
|
+
navigating = false;
|
|
212
|
+
location.href = url;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Merge new translations with existing ones.
|
|
217
|
+
// This keeps translation keys alive for persistent layout components.
|
|
218
|
+
if (data.translations) {
|
|
219
|
+
const existing = window.__NITRON_TRANSLATIONS__;
|
|
220
|
+
|
|
221
|
+
window.__NITRON_TRANSLATIONS__ = existing
|
|
222
|
+
? Object.assign({}, existing, data.translations)
|
|
223
|
+
: data.translations;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Let React reconcile the new payload into the DOM
|
|
227
|
+
rsc.navigate(data.payload);
|
|
170
228
|
|
|
229
|
+
// Update browser URL and page title
|
|
171
230
|
if (push) history.pushState({ nitron: 1 }, "", url);
|
|
231
|
+
if (data.meta && data.meta.title) document.title = data.meta.title;
|
|
172
232
|
|
|
173
|
-
|
|
174
|
-
setMeta("description", d.meta ? d.meta.description : null);
|
|
233
|
+
updateMetaTag("description", data.meta ? data.meta.description : null);
|
|
175
234
|
|
|
176
|
-
// Load new CSS
|
|
177
|
-
if (
|
|
235
|
+
// Load any new CSS files this page needs
|
|
236
|
+
if (data.css) loadNewCss(data.css);
|
|
178
237
|
|
|
238
|
+
// Scroll to top and fire a custom event for any listeners
|
|
179
239
|
scrollTo(0, 0);
|
|
180
240
|
dispatchEvent(new CustomEvent("nitron:navigate", { detail: { url: url } }));
|
|
181
241
|
navigating = false;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
clearTimeout(
|
|
186
|
-
fallback();
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
function fallback() {
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
// Something went wrong — fall back to a normal full page load
|
|
245
|
+
clearTimeout(timeout);
|
|
190
246
|
navigating = false;
|
|
191
247
|
location.href = url;
|
|
192
248
|
}
|
|
193
249
|
}
|
|
194
250
|
|
|
195
|
-
//
|
|
251
|
+
// ============================================================
|
|
252
|
+
// Helper Functions
|
|
253
|
+
// ============================================================
|
|
254
|
+
|
|
255
|
+
function layoutsMatch(targetLayouts) {
|
|
256
|
+
const currentLayouts = window.__NITRON_RUNTIME__?.layouts || [];
|
|
257
|
+
const newLayouts = targetLayouts || [];
|
|
258
|
+
|
|
259
|
+
if (currentLayouts.length !== newLayouts.length) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (let i = 0; i < currentLayouts.length; i++) {
|
|
264
|
+
if (currentLayouts[i] !== newLayouts[i]) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
196
271
|
|
|
197
|
-
//
|
|
272
|
+
// Checks if any new CSS files need to be loaded for the new page.
|
|
273
|
+
// Skips files that are already loaded.
|
|
198
274
|
function loadNewCss(cssFiles) {
|
|
199
|
-
|
|
275
|
+
const existing = new Set();
|
|
200
276
|
|
|
201
|
-
document.querySelectorAll('link[rel="stylesheet"]').forEach(function(
|
|
202
|
-
existing.add(
|
|
277
|
+
document.querySelectorAll('link[rel="stylesheet"]').forEach(function(el) {
|
|
278
|
+
existing.add(el.getAttribute("href"));
|
|
203
279
|
});
|
|
204
280
|
|
|
205
|
-
for (
|
|
281
|
+
for (let i = 0; i < cssFiles.length; i++) {
|
|
206
282
|
if (!existing.has(cssFiles[i])) {
|
|
207
|
-
|
|
283
|
+
const link = document.createElement("link");
|
|
208
284
|
link.rel = "stylesheet";
|
|
209
285
|
link.href = cssFiles[i];
|
|
210
286
|
document.head.appendChild(link);
|
|
@@ -212,24 +288,32 @@
|
|
|
212
288
|
}
|
|
213
289
|
}
|
|
214
290
|
|
|
215
|
-
// Creates or updates a <meta> tag
|
|
216
|
-
|
|
217
|
-
|
|
291
|
+
// Creates or updates a <meta> tag in the document head.
|
|
292
|
+
// Used to update SEO meta tags when navigating between pages.
|
|
293
|
+
function updateMetaTag(name, value) {
|
|
294
|
+
if (!value) return;
|
|
218
295
|
|
|
219
|
-
|
|
296
|
+
let tag = document.querySelector('meta[name="' + name + '"]');
|
|
220
297
|
|
|
221
|
-
if (
|
|
298
|
+
if (tag) {
|
|
299
|
+
tag.content = value;
|
|
300
|
+
}
|
|
222
301
|
else {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
document.head.appendChild(
|
|
302
|
+
tag = document.createElement("meta");
|
|
303
|
+
tag.name = name;
|
|
304
|
+
tag.content = value;
|
|
305
|
+
document.head.appendChild(tag);
|
|
227
306
|
}
|
|
228
307
|
}
|
|
229
308
|
|
|
230
|
-
//
|
|
309
|
+
// ============================================================
|
|
310
|
+
// Start
|
|
311
|
+
// ============================================================
|
|
231
312
|
|
|
232
|
-
document.readyState === "loading"
|
|
233
|
-
|
|
234
|
-
|
|
313
|
+
if (document.readyState === "loading") {
|
|
314
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
init();
|
|
318
|
+
}
|
|
235
319
|
})();
|
|
@@ -69,7 +69,10 @@ class FlightRenderer {
|
|
|
69
69
|
}, timeout);
|
|
70
70
|
|
|
71
71
|
stream.on("data", chunk => chunks.push(chunk));
|
|
72
|
-
stream.on("end", () =>
|
|
72
|
+
stream.on("end", () => {
|
|
73
|
+
const payload = Buffer.concat(chunks).toString("utf-8");
|
|
74
|
+
finish(payload);
|
|
75
|
+
});
|
|
73
76
|
stream.on("error", (error) => {
|
|
74
77
|
renderError = error;
|
|
75
78
|
finish(null);
|
|
@@ -89,6 +92,7 @@ class FlightRenderer {
|
|
|
89
92
|
pipe(stream);
|
|
90
93
|
}
|
|
91
94
|
catch (error) {
|
|
95
|
+
console.error("[FlightRenderer] renderToPipeableStream threw:", error?.message);
|
|
92
96
|
renderError = error;
|
|
93
97
|
finish(null);
|
|
94
98
|
}
|