@nitronjs/framework 0.3.1 → 0.3.3

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.
@@ -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 to avoid per-character replacement
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, '\x01MONTH\x01');
28
- result = result.replace(/MMM/g, '\x01MONTHSHORT\x01');
29
- result = result.replace(/DDDD/g, '\x01DAY\x01');
30
- result = result.replace(/DDD/g, '\x01DAYSHORT\x01');
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(/\x01MONTH\x01/g, lang.months[date.getMonth()]);
52
- result = result.replace(/\x01MONTHSHORT\x01/g, lang.monthsShort[date.getMonth()]);
53
- result = result.replace(/\x01DAY\x01/g, lang.days[date.getDay()]);
54
- result = result.replace(/\x01DAYSHORT\x01/g, lang.daysShort[date.getDay()]);
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
  }
@@ -109,7 +111,18 @@ class DateTime {
109
111
  date.setHours(date.getHours() + hours);
110
112
  return date.toISOString().slice(0, 19).replace('T', ' ');
111
113
  }
112
-
114
+
115
+ /**
116
+ * Add minutes to current date
117
+ * @param {number} minutes - Number of minutes to add
118
+ * @returns {string} SQL formatted datetime
119
+ */
120
+ static addMinutes(minutes) {
121
+ const date = this.#getDate();
122
+ date.setMinutes(date.getMinutes() + minutes);
123
+ return date.toISOString().slice(0, 19).replace('T', ' ');
124
+ }
125
+
113
126
  /**
114
127
  * Subtract days from current date
115
128
  * @param {number} days - Number of days to subtract
@@ -118,7 +131,7 @@ class DateTime {
118
131
  static subDays(days) {
119
132
  return this.addDays(-days);
120
133
  }
121
-
134
+
122
135
  /**
123
136
  * Subtract hours from current date
124
137
  * @param {number} hours - Number of hours to subtract
@@ -127,6 +140,15 @@ class DateTime {
127
140
  static subHours(hours) {
128
141
  return this.addHours(-hours);
129
142
  }
143
+
144
+ /**
145
+ * Subtract minutes from current date
146
+ * @param {number} minutes - Number of minutes to subtract
147
+ * @returns {string} SQL formatted datetime
148
+ */
149
+ static subMinutes(minutes) {
150
+ return this.addMinutes(-minutes);
151
+ }
130
152
  }
131
153
 
132
154
  export default DateTime;
@@ -29,7 +29,8 @@ class DevIndicator {
29
29
  const route = devData?.route;
30
30
  const timing = devData ? buildTimingSummary(devData) : null;
31
31
  const mwCount = devData?.middlewareTiming?.length || 0;
32
- const propsInfo = devData?.rawProps ? buildPropsSummary(devData) : null;
32
+ const hasProps = devData?.rawProps && Object.keys(devData.rawProps).length > 0;
33
+ const propsInfo = hasProps ? buildPropsSummary(devData) : null;
33
34
 
34
35
  const totalMs = timing ? parseFloat(timing.total) : 0;
35
36
  const timingWarnClass = totalMs >= 300 ? " ndi-critical" : totalMs >= 100 ? " ndi-warn" : "";
@@ -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.exit(1);
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.exit(1);
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 template file with placeholder replacement.
129
- * Template files are loaded from resources/views/ directory.
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. "emails/welcome")
133
- * @param {Object} data - Data to replace placeholders with
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 filePath = path.join(Paths.views, `${templateName}.html`);
138
- let content = fs.readFileSync(filePath, "utf-8");
140
+ const buildPath = path.join(Paths.buildViews, `${templateName}.js`);
139
141
 
140
- for (const [key, value] of Object.entries(data)) {
141
- content = content.replaceAll(`{{ ${key} }}`, escapeHtml(String(value ?? "")));
142
+ if (!fs.existsSync(buildPath)) {
143
+ throw new Error(`Mail template not found: ${templateName}`);
142
144
  }
143
145
 
144
- this.#htmlContent = content;
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, "&amp;")
212
- .replace(/</g, "&lt;")
213
- .replace(/>/g, "&gt;")
214
- .replace(/"/g, "&quot;")
215
- .replace(/'/g, "&#39;");
216
- }
217
-
218
221
  export default Mail;
@@ -36,6 +36,6 @@ const isMain = process.argv[1]?.endsWith("Entry.js");
36
36
  if (isMain) {
37
37
  start().catch(e => {
38
38
  console.error(e);
39
- process.exit(1);
39
+ process.exitCode = 1;
40
40
  });
41
41
  }
@@ -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
- * 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 }
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
- var RSC_ENDPOINT = "/__nitron/rsc";
14
+ const RSC_ENDPOINT = "/__nitron/rsc";
15
+ const NAVIGATION_TIMEOUT = 10000;
16
+ const SKIP_PATTERN = /^(\/storage|\/api|\/__|#|javascript:|mailto:|tel:)/;
18
17
 
19
- // URLs that should never be intercepted by SPA navigation
20
- var SKIP = /^(\/storage|\/api|\/__|#|javascript:|mailto:|tel:)/;
18
+ let navigating = false;
21
19
 
22
- var navigating = false;
20
+ // ============================================================
21
+ // Client-side route() helper
22
+ // ============================================================
23
23
 
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.
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
- var runtime = window.__NITRON_RUNTIME__;
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
- var pattern = runtime.routes[name];
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
- // --- Link Interception ---
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", onClick, true);
56
- window.addEventListener("popstate", onPopState);
56
+ document.addEventListener("click", handleLinkClick, true);
57
+ window.addEventListener("popstate", handleBackForward);
57
58
  }
58
59
 
59
- // Intercepts <a> clicks if the link is local, navigates via RSC instead of full reload
60
- function onClick(e) {
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
- var link = e.target.closest("a");
64
+
65
+ const link = e.target.closest("a");
63
66
 
64
67
  if (!link) return;
65
68
 
66
- var href = link.getAttribute("href");
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 (!isLocal(href)) return;
74
+ if (!isLocalUrl(href)) return;
71
75
 
72
76
  e.preventDefault();
73
77
  navigate(href, true);
74
78
  }
75
79
 
76
- // Browser back/forward re-fetch RSC payload for the new URL
77
- function onPopState() {
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
- // Returns true if the href points to the same origin and is not in the SKIP list
82
- function isLocal(href) {
83
- if (!href || SKIP.test(href)) return false;
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 — check if same origin
91
+ // Absolute URLs — compare origins
86
92
  if (/^https?:\/\/|^\/\//.test(href)) {
87
- try { return new URL(href, location.origin).origin === location.origin; }
88
- catch (e) { return false; }
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
- // --- RSC Wire Format Parser ---
105
+ // ============================================================
106
+ // RSC Response Parser
107
+ // ============================================================
95
108
 
96
- // Parses the length-prefixed response from /__nitron/rsc.
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 on malformed input.
99
- function parseLengthPrefixed(text) {
100
- var nl = text.indexOf("\n");
101
- if (nl === -1) return null;
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
- var len = parseInt(text.substring(0, nl), 10);
104
- if (isNaN(len) || len < 0) return null;
117
+ const payloadLength = parseInt(text.substring(0, newlineIndex), 10);
105
118
 
106
- var flight = text.substring(nl + 1, nl + 1 + len);
107
- var jsonStr = text.substring(nl + 1 + len);
108
- var data;
119
+ if (isNaN(payloadLength) || payloadLength < 0) return null;
109
120
 
110
- try { data = JSON.parse(jsonStr); }
111
- catch (e) { return null; }
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: flight,
115
- meta: data.meta || null,
116
- css: data.css || null,
117
- translations: data.translations || null
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
- // --- SPA Navigation ---
143
+ // ============================================================
144
+ // SPA Navigation
145
+ // ============================================================
122
146
 
123
- // Core navigation: fetches RSC payload and lets React reconcile.
124
- // Falls back to full page load on error or timeout (10s).
125
- function navigate(url, push) {
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
- var rsc = window.__NITRON_RSC__;
154
+ const rsc = window.__NITRON_RSC__;
129
155
 
130
- // RSC root not available (e.g. initial page was server-rendered without hydration)
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
- // Abort + fallback after 10 seconds to prevent hanging navigations
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
- if (!r.ok) throw new Error("HTTP " + r.status);
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
- return r.text().then(function(text) {
153
- return parseLengthPrefixed(text);
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
- // Merge translations BEFORE rsc.navigate — persistent components (layouts) keep their keys
160
- if (d.translations) {
161
- var prev = window.__NITRON_TRANSLATIONS__;
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
- window.__NITRON_TRANSLATIONS__ = prev
164
- ? Object.assign({}, prev, d.translations)
165
- : d.translations;
206
+ if (!data || !data.payload) {
207
+ throw new Error("Invalid RSC response");
166
208
  }
167
209
 
168
- // Use RSC consumer to render new payload via React reconciliation
169
- rsc.navigate(d.payload);
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
- if (d.meta && d.meta.title) document.title = d.meta.title;
174
- setMeta("description", d.meta ? d.meta.description : null);
233
+ updateMetaTag("description", data.meta ? data.meta.description : null);
175
234
 
176
- // Load new CSS stylesheets if needed
177
- if (d.css) loadNewCss(d.css);
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
- .catch(function() {
184
- // RSC fetch failed or aborted full page load as last resort
185
- clearTimeout(timer);
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
- // --- CSS & Meta Helpers ---
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
- // Loads CSS files that the new page needs but the current page doesn't have
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
- var existing = new Set();
275
+ const existing = new Set();
200
276
 
201
- document.querySelectorAll('link[rel="stylesheet"]').forEach(function(link) {
202
- existing.add(link.getAttribute("href"));
277
+ document.querySelectorAll('link[rel="stylesheet"]').forEach(function(el) {
278
+ existing.add(el.getAttribute("href"));
203
279
  });
204
280
 
205
- for (var i = 0; i < cssFiles.length; i++) {
281
+ for (let i = 0; i < cssFiles.length; i++) {
206
282
  if (!existing.has(cssFiles[i])) {
207
- var link = document.createElement("link");
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 (e.g. description)
216
- function setMeta(name, val) {
217
- if (!val) return;
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
- var m = document.querySelector('meta[name="' + name + '"]');
296
+ let tag = document.querySelector('meta[name="' + name + '"]');
220
297
 
221
- if (m) m.content = val;
298
+ if (tag) {
299
+ tag.content = value;
300
+ }
222
301
  else {
223
- m = document.createElement("meta");
224
- m.name = name;
225
- m.content = val;
226
- document.head.appendChild(m);
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
- // --- Initialize ---
309
+ // ============================================================
310
+ // Start
311
+ // ============================================================
231
312
 
232
- document.readyState === "loading"
233
- ? document.addEventListener("DOMContentLoaded", init)
234
- : init();
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", () => finish(Buffer.concat(chunks).toString("utf-8")));
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
  }