@nitronjs/framework 0.2.27 → 0.3.1
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 +260 -170
- package/lib/Auth/Auth.js +2 -2
- package/lib/Build/CssBuilder.js +5 -7
- package/lib/Build/EffectivePropUsage.js +174 -0
- package/lib/Build/FactoryTransform.js +1 -21
- package/lib/Build/FileAnalyzer.js +2 -33
- package/lib/Build/Manager.js +354 -58
- package/lib/Build/PropUsageAnalyzer.js +1189 -0
- package/lib/Build/jsxRuntime.js +25 -155
- package/lib/Build/plugins.js +212 -146
- package/lib/Build/propUtils.js +70 -0
- package/lib/Console/Commands/DevCommand.js +30 -10
- package/lib/Console/Commands/MakeCommand.js +8 -1
- package/lib/Console/Output.js +0 -2
- package/lib/Console/Stubs/rsc-consumer.tsx +74 -0
- package/lib/Console/Stubs/vendor-dev.tsx +30 -41
- package/lib/Console/Stubs/vendor.tsx +25 -1
- package/lib/Core/Config.js +0 -6
- package/lib/Core/Paths.js +0 -19
- package/lib/Database/Migration/Checksum.js +0 -3
- package/lib/Database/Migration/MigrationRepository.js +0 -8
- package/lib/Database/Migration/MigrationRunner.js +1 -2
- package/lib/Database/Model.js +19 -11
- package/lib/Database/QueryBuilder.js +25 -4
- package/lib/Database/Schema/Blueprint.js +10 -0
- package/lib/Database/Schema/Manager.js +2 -0
- package/lib/Date/DateTime.js +1 -1
- package/lib/Dev/DevContext.js +44 -0
- package/lib/Dev/DevErrorPage.js +990 -0
- package/lib/Dev/DevIndicator.js +836 -0
- package/lib/HMR/Server.js +16 -37
- package/lib/Http/Server.js +171 -23
- package/lib/Logging/Log.js +34 -2
- package/lib/Mail/Mail.js +41 -10
- package/lib/Route/Router.js +43 -19
- package/lib/Runtime/Entry.js +10 -6
- package/lib/Session/Manager.js +103 -1
- package/lib/Session/Session.js +0 -4
- package/lib/Support/Str.js +6 -4
- package/lib/Translation/Lang.js +376 -32
- package/lib/Translation/pluralize.js +81 -0
- package/lib/Validation/MagicBytes.js +120 -0
- package/lib/Validation/Validator.js +46 -29
- package/lib/View/Client/hmr-client.js +100 -90
- package/lib/View/Client/spa.js +121 -50
- package/lib/View/ClientManifest.js +60 -0
- package/lib/View/FlightRenderer.js +100 -0
- package/lib/View/Layout.js +0 -3
- package/lib/View/PropFilter.js +81 -0
- package/lib/View/View.js +230 -495
- package/lib/index.d.ts +22 -1
- package/package.json +2 -2
- package/skeleton/config/app.js +1 -0
- package/skeleton/config/server.js +13 -0
- package/skeleton/config/session.js +3 -0
- package/lib/Build/HydrationBuilder.js +0 -190
- package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
- package/lib/Console/Stubs/page-hydration.tsx +0 -53
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Signatures ordered by total check bytes (offset + bytes.length) descending
|
|
2
|
+
const SIGNATURES = [
|
|
3
|
+
// 12-byte region: WebP (RIFF at 0, WEBP at 8)
|
|
4
|
+
{ regions: [{ bytes: [0x52, 0x49, 0x46, 0x46], offset: 0 }, { bytes: [0x57, 0x45, 0x42, 0x50], offset: 8 }], mime: "image/webp" },
|
|
5
|
+
// 8 bytes
|
|
6
|
+
{ bytes: [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A], offset: 0, mime: "image/png" },
|
|
7
|
+
// 8-byte region: MP4/ISOBMFF (ftyp at offset 4)
|
|
8
|
+
{ bytes: [0x66, 0x74, 0x79, 0x70], offset: 4, mime: "video/mp4" },
|
|
9
|
+
// 6 bytes
|
|
10
|
+
{ bytes: [0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C], offset: 0, mime: "application/x-7z-compressed" },
|
|
11
|
+
{ bytes: [0x52, 0x61, 0x72, 0x21, 0x1A, 0x07], offset: 0, mime: "application/x-rar-compressed" },
|
|
12
|
+
// 4 bytes
|
|
13
|
+
{ bytes: [0x25, 0x50, 0x44, 0x46], offset: 0, mime: "application/pdf" },
|
|
14
|
+
{ bytes: [0x50, 0x4B, 0x03, 0x04], offset: 0, mime: "application/zip" },
|
|
15
|
+
{ bytes: [0x47, 0x49, 0x46, 0x38], offset: 0, mime: "image/gif" },
|
|
16
|
+
{ bytes: [0x1A, 0x45, 0xDF, 0xA3], offset: 0, mime: "video/webm" },
|
|
17
|
+
{ bytes: [0x00, 0x00, 0x01, 0x00], offset: 0, mime: "image/x-icon" },
|
|
18
|
+
// 3 bytes
|
|
19
|
+
{ bytes: [0xFF, 0xD8, 0xFF], offset: 0, mime: "image/jpeg" },
|
|
20
|
+
{ bytes: [0x49, 0x44, 0x33], offset: 0, mime: "audio/mpeg" },
|
|
21
|
+
// 2 bytes
|
|
22
|
+
{ bytes: [0x1F, 0x8B], offset: 0, mime: "application/gzip" },
|
|
23
|
+
{ bytes: [0x42, 0x4D], offset: 0, mime: "image/bmp" }
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const MIME_FAMILIES = [
|
|
27
|
+
// ZIP family (jar/apk excluded)
|
|
28
|
+
["application/zip", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.oasis.opendocument.text", "application/vnd.oasis.opendocument.spreadsheet", "application/vnd.oasis.opendocument.presentation"],
|
|
29
|
+
// MPEG-4/ISOBMFF family
|
|
30
|
+
["video/mp4", "audio/mp4", "audio/m4a", "video/quicktime", "video/3gpp"],
|
|
31
|
+
// EBML family
|
|
32
|
+
["video/webm", "video/x-matroska", "audio/webm"]
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Detect MIME type from buffer magic bytes.
|
|
37
|
+
* Returns null if buffer is empty or no signature matches.
|
|
38
|
+
*/
|
|
39
|
+
export function detectMime(buffer) {
|
|
40
|
+
if (!buffer || buffer.length === 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const sig of SIGNATURES) {
|
|
45
|
+
if (sig.regions) {
|
|
46
|
+
let allMatch = true;
|
|
47
|
+
|
|
48
|
+
for (const region of sig.regions) {
|
|
49
|
+
if (buffer.length < region.offset + region.bytes.length) {
|
|
50
|
+
allMatch = false;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < region.bytes.length; i++) {
|
|
55
|
+
if (buffer[region.offset + i] !== region.bytes[i]) {
|
|
56
|
+
allMatch = false;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!allMatch) break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (allMatch) return sig.mime;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
if (buffer.length < sig.offset + sig.bytes.length) continue;
|
|
68
|
+
|
|
69
|
+
let match = true;
|
|
70
|
+
|
|
71
|
+
for (let i = 0; i < sig.bytes.length; i++) {
|
|
72
|
+
if (buffer[sig.offset + i] !== sig.bytes[i]) {
|
|
73
|
+
match = false;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (match) return sig.mime;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if two MIME types belong to the same family.
|
|
87
|
+
* ZIP family includes Office XML formats. MPEG-4 family shares ftyp. EBML family shares header.
|
|
88
|
+
*/
|
|
89
|
+
export function isSameMimeFamily(mime1, mime2) {
|
|
90
|
+
if (!mime1 || !mime2) return false;
|
|
91
|
+
|
|
92
|
+
const m1 = mime1.toLowerCase(), m2 = mime2.toLowerCase();
|
|
93
|
+
|
|
94
|
+
if (m1 === m2) return true;
|
|
95
|
+
|
|
96
|
+
for (const family of MIME_FAMILIES) {
|
|
97
|
+
const has1 = family.includes(m1) || (family === MIME_FAMILIES[0] && (m1.startsWith("application/vnd.openxmlformats-officedocument.") || m1.startsWith("application/vnd.oasis.opendocument.")));
|
|
98
|
+
const has2 = family.includes(m2) || (family === MIME_FAMILIES[0] && (m2.startsWith("application/vnd.openxmlformats-officedocument.") || m2.startsWith("application/vnd.oasis.opendocument.")));
|
|
99
|
+
|
|
100
|
+
if (has1 && has2) return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Regex: null bytes, URL-encoded null, bidi/invisible Unicode, control chars
|
|
107
|
+
const UNSAFE_CHARS = /\x00|%00|[\u202A-\u202E\u200E\u200F\u2066-\u2069\u200B-\u200D\uFEFF]|[\x01-\x08\x0B\x0C\x0E-\x1F]/gi;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Strip dangerous characters from filename.
|
|
111
|
+
* Removes null bytes, URL-encoded nulls, Unicode bidi/invisible chars, control chars.
|
|
112
|
+
* Returns "unnamed" if result is empty.
|
|
113
|
+
*/
|
|
114
|
+
export function sanitizeFilename(filename) {
|
|
115
|
+
if (!filename) return "unnamed";
|
|
116
|
+
|
|
117
|
+
const cleaned = filename.replace(UNSAFE_CHARS, "");
|
|
118
|
+
|
|
119
|
+
return cleaned || "unnamed";
|
|
120
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { MIME_TYPES } from "./MimeTypes.js";
|
|
2
|
+
import { isSameMimeFamily } from "./MagicBytes.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Laravel-style Validator for NitronJS
|
|
@@ -8,13 +9,12 @@ import { MIME_TYPES } from "./MimeTypes.js";
|
|
|
8
9
|
* - Use "required" to make a field mandatory
|
|
9
10
|
* - Use "nullable" to explicitly allow null (skips ALL validation)
|
|
10
11
|
*
|
|
11
|
-
* SECURITY
|
|
12
|
-
* File validation (mimes, mime_types)
|
|
13
|
-
*
|
|
14
|
-
* Always verify file signatures (magic bytes) server-side before storage.
|
|
12
|
+
* SECURITY:
|
|
13
|
+
* File validation (mimes, mime_types) uses magic byte verification via preHandler
|
|
14
|
+
* when available. Falls back to declared MIME type for unrecognized formats.
|
|
15
15
|
*
|
|
16
16
|
* UNITS:
|
|
17
|
-
* - min/max for files:
|
|
17
|
+
* - min/max for files: KB (e.g., max:2048 = 2MB)
|
|
18
18
|
* - min/max for strings: characters
|
|
19
19
|
* - min/max for arrays: item count
|
|
20
20
|
* - min/max for numbers: numeric value
|
|
@@ -76,6 +76,9 @@ class Validator {
|
|
|
76
76
|
|
|
77
77
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
78
78
|
const part = parts[i];
|
|
79
|
+
|
|
80
|
+
if (part === "__proto__" || part === "constructor" || part === "prototype") break;
|
|
81
|
+
|
|
79
82
|
const nextPart = parts[i + 1];
|
|
80
83
|
const isNextIndex = /^\d+$/.test(nextPart);
|
|
81
84
|
|
|
@@ -138,9 +141,21 @@ class Validator {
|
|
|
138
141
|
|
|
139
142
|
// Backend Multipart (Fastify @fastify/multipart)
|
|
140
143
|
if (typeof value.mimetype === "string" && typeof value.toBuffer === "function") {
|
|
144
|
+
let mime = value.mimetype.toLowerCase();
|
|
145
|
+
|
|
146
|
+
// Three-way _magicMime branch:
|
|
147
|
+
// undefined → magic byte check not performed, use declared MIME
|
|
148
|
+
// null → check performed but unrecognized, fallback to declared MIME
|
|
149
|
+
// string → check performed and recognized, use magic MIME (with family check)
|
|
150
|
+
if (typeof value._magicMime === "string") {
|
|
151
|
+
if (!isSameMimeFamily(mime, value._magicMime)) {
|
|
152
|
+
mime = value._magicMime;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
141
156
|
return {
|
|
142
|
-
mime
|
|
143
|
-
size: value._buf?.length ?? null
|
|
157
|
+
mime,
|
|
158
|
+
size: value._fileSize ?? value._buf?.length ?? null
|
|
144
159
|
};
|
|
145
160
|
}
|
|
146
161
|
|
|
@@ -494,7 +509,7 @@ class Validator {
|
|
|
494
509
|
* Minimum size/length/value
|
|
495
510
|
* - Arrays: minimum item count
|
|
496
511
|
* - Strings: minimum character count
|
|
497
|
-
* - Files: minimum size in
|
|
512
|
+
* - Files: minimum size in KB
|
|
498
513
|
* - Numbers: minimum value
|
|
499
514
|
*/
|
|
500
515
|
min: (value, params, field) => {
|
|
@@ -512,6 +527,17 @@ class Validator {
|
|
|
512
527
|
};
|
|
513
528
|
}
|
|
514
529
|
|
|
530
|
+
// File: size in KB (before generic object — file objects are also objects)
|
|
531
|
+
const file = this.#getFileInfo(value);
|
|
532
|
+
if (file && file.size !== null) {
|
|
533
|
+
const minBytes = min * 1024;
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
passes: file.size >= minBytes,
|
|
537
|
+
message: `The ${field} file must be at least ${this.#formatBytes(minBytes)}.`
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
515
541
|
// Object: property count
|
|
516
542
|
if (value !== null && typeof value === "object") {
|
|
517
543
|
const keyCount = Object.keys(value).length;
|
|
@@ -521,15 +547,6 @@ class Validator {
|
|
|
521
547
|
};
|
|
522
548
|
}
|
|
523
549
|
|
|
524
|
-
// File: size in bytes
|
|
525
|
-
const file = this.#getFileInfo(value);
|
|
526
|
-
if (file && file.size !== null) {
|
|
527
|
-
return {
|
|
528
|
-
passes: file.size >= min,
|
|
529
|
-
message: `The ${field} file must be at least ${this.#formatBytes(min)}.`
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
|
|
533
550
|
// String: character count
|
|
534
551
|
if (typeof value === "string") {
|
|
535
552
|
return {
|
|
@@ -556,7 +573,7 @@ class Validator {
|
|
|
556
573
|
* Maximum size/length/value
|
|
557
574
|
* - Arrays: maximum item count
|
|
558
575
|
* - Strings: maximum character count
|
|
559
|
-
* - Files: maximum size in
|
|
576
|
+
* - Files: maximum size in KB
|
|
560
577
|
* - Numbers: maximum value
|
|
561
578
|
*/
|
|
562
579
|
max: (value, params, field) => {
|
|
@@ -574,6 +591,17 @@ class Validator {
|
|
|
574
591
|
};
|
|
575
592
|
}
|
|
576
593
|
|
|
594
|
+
// File: size in KB (before generic object — file objects are also objects)
|
|
595
|
+
const file = this.#getFileInfo(value);
|
|
596
|
+
if (file && file.size !== null) {
|
|
597
|
+
const maxBytes = max * 1024;
|
|
598
|
+
|
|
599
|
+
return {
|
|
600
|
+
passes: file.size <= maxBytes,
|
|
601
|
+
message: `The ${field} file must not exceed ${this.#formatBytes(maxBytes)}.`
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
577
605
|
// Object: property count
|
|
578
606
|
if (value !== null && typeof value === "object") {
|
|
579
607
|
const keyCount = Object.keys(value).length;
|
|
@@ -583,15 +611,6 @@ class Validator {
|
|
|
583
611
|
};
|
|
584
612
|
}
|
|
585
613
|
|
|
586
|
-
// File: size in bytes
|
|
587
|
-
const file = this.#getFileInfo(value);
|
|
588
|
-
if (file && file.size !== null) {
|
|
589
|
-
return {
|
|
590
|
-
passes: file.size <= max,
|
|
591
|
-
message: `The ${field} file must not exceed ${this.#formatBytes(max)}.`
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
|
|
595
614
|
// String: character count
|
|
596
615
|
if (typeof value === "string") {
|
|
597
616
|
return {
|
|
@@ -615,8 +634,6 @@ class Validator {
|
|
|
615
634
|
},
|
|
616
635
|
|
|
617
636
|
// ─── File Rules ──────────────────────────────────────────────────────
|
|
618
|
-
// ⚠️ WARNING: These rules check MIME headers only, which can be spoofed.
|
|
619
|
-
// Always verify file signatures (magic bytes) server-side!
|
|
620
637
|
|
|
621
638
|
/** Must be a valid file object */
|
|
622
639
|
file: (value, params, field) => {
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HMR Client
|
|
3
|
-
*
|
|
2
|
+
* HMR Client — Browser-side hot module replacement handler.
|
|
3
|
+
*
|
|
4
|
+
* Connects to the dev server via Socket.IO WebSocket.
|
|
5
|
+
* On file change events, fetches a fresh RSC (React Server Components)
|
|
6
|
+
* Flight payload and lets React reconcile the DOM — no full page reload needed.
|
|
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 }
|
|
4
13
|
*/
|
|
5
14
|
(function() {
|
|
6
15
|
"use strict";
|
|
7
16
|
|
|
17
|
+
var RSC_ENDPOINT = "/__nitron/rsc";
|
|
8
18
|
var socket = null;
|
|
9
|
-
var errorOverlay = null;
|
|
10
19
|
|
|
11
|
-
// Connection
|
|
20
|
+
// --- Connection ---
|
|
21
|
+
|
|
12
22
|
function connect() {
|
|
13
23
|
if (socket) return;
|
|
14
24
|
|
|
@@ -23,12 +33,14 @@
|
|
|
23
33
|
path: "/__nitron_hmr",
|
|
24
34
|
transports: ["websocket"],
|
|
25
35
|
reconnection: true,
|
|
26
|
-
reconnectionAttempts:
|
|
36
|
+
reconnectionAttempts: Infinity,
|
|
27
37
|
reconnectionDelay: 1000,
|
|
38
|
+
reconnectionDelayMax: 5000,
|
|
28
39
|
timeout: 5000
|
|
29
40
|
});
|
|
30
41
|
}
|
|
31
42
|
catch (e) {
|
|
43
|
+
// Socket.IO not available — HMR disabled silently (dev-only feature)
|
|
32
44
|
return;
|
|
33
45
|
}
|
|
34
46
|
|
|
@@ -45,131 +57,129 @@
|
|
|
45
57
|
window.__nitron_hmr_connected__ = false;
|
|
46
58
|
});
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
// Unified RSC-based change handler
|
|
61
|
+
socket.on("hmr:change", function(data) {
|
|
62
|
+
hideErrorOverlay();
|
|
63
|
+
|
|
64
|
+
if (data.changeType === "css") {
|
|
65
|
+
refreshCss();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// page, layout — all use RSC refetch
|
|
70
|
+
refetchRSC(data.cssChanged);
|
|
50
71
|
});
|
|
51
72
|
|
|
52
73
|
socket.on("hmr:reload", function() {
|
|
53
74
|
location.reload();
|
|
54
75
|
});
|
|
55
76
|
|
|
56
|
-
socket.on("hmr:css", function(data) {
|
|
57
|
-
refreshCss(data.file);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
77
|
socket.on("hmr:error", function(data) {
|
|
61
78
|
showErrorOverlay(data.message || "Unknown error", data.file);
|
|
62
79
|
});
|
|
63
80
|
}
|
|
64
81
|
|
|
65
|
-
//
|
|
66
|
-
|
|
82
|
+
// --- RSC Wire Format Parser ---
|
|
83
|
+
|
|
84
|
+
// Parses the length-prefixed response from /__nitron/rsc.
|
|
85
|
+
// Format: "<length>\n<flight payload><json metadata>"
|
|
86
|
+
// Returns { payload, meta, css, translations } or null on malformed input.
|
|
87
|
+
function parseLengthPrefixed(text) {
|
|
88
|
+
var nl = text.indexOf("\n");
|
|
89
|
+
if (nl === -1) return null;
|
|
90
|
+
|
|
91
|
+
var len = parseInt(text.substring(0, nl), 10);
|
|
92
|
+
if (isNaN(len) || len < 0) return null;
|
|
93
|
+
|
|
94
|
+
var flight = text.substring(nl + 1, nl + 1 + len);
|
|
95
|
+
var jsonStr = text.substring(nl + 1 + len);
|
|
96
|
+
var data;
|
|
97
|
+
|
|
98
|
+
try { data = JSON.parse(jsonStr); }
|
|
99
|
+
catch (e) { return null; }
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
payload: flight,
|
|
103
|
+
meta: data.meta || null,
|
|
104
|
+
css: data.css || null,
|
|
105
|
+
translations: data.translations || null
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// --- RSC Page Update ---
|
|
110
|
+
|
|
111
|
+
// Fetches a fresh Flight payload for the current URL and hands it to React.
|
|
112
|
+
// React's reconciliation updates only the changed DOM nodes — no full reload.
|
|
113
|
+
function refetchRSC(cssChanged) {
|
|
114
|
+
var rsc = window.__NITRON_RSC__;
|
|
115
|
+
|
|
116
|
+
if (!rsc || !rsc.root) {
|
|
117
|
+
location.reload();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
67
121
|
var url = location.pathname + location.search;
|
|
68
122
|
|
|
69
|
-
fetch("
|
|
123
|
+
fetch(RSC_ENDPOINT + "?url=" + encodeURIComponent(url), {
|
|
70
124
|
headers: { "X-Nitron-SPA": "1" },
|
|
71
125
|
credentials: "same-origin"
|
|
72
126
|
})
|
|
73
|
-
.then(function(
|
|
74
|
-
if (!
|
|
127
|
+
.then(function(r) {
|
|
128
|
+
if (!r.ok) throw new Error("HTTP " + r.status);
|
|
75
129
|
|
|
76
|
-
return
|
|
130
|
+
return r.text().then(function(text) {
|
|
131
|
+
return parseLengthPrefixed(text);
|
|
132
|
+
});
|
|
77
133
|
})
|
|
78
|
-
.then(function(
|
|
79
|
-
if (
|
|
134
|
+
.then(function(d) {
|
|
135
|
+
if (!d || !d.payload) {
|
|
80
136
|
location.reload();
|
|
81
|
-
|
|
82
137
|
return;
|
|
83
138
|
}
|
|
84
139
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
location.reload();
|
|
140
|
+
// Merge translations BEFORE navigate — persistent components (layouts) keep their keys
|
|
141
|
+
if (d.translations) {
|
|
142
|
+
var prev = window.__NITRON_TRANSLATIONS__;
|
|
89
143
|
|
|
90
|
-
|
|
144
|
+
window.__NITRON_TRANSLATIONS__ = prev
|
|
145
|
+
? Object.assign({}, prev, d.translations)
|
|
146
|
+
: d.translations;
|
|
91
147
|
}
|
|
92
148
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
var newSlot = container.querySelector("[data-nitron-slot='page']");
|
|
97
|
-
slot.innerHTML = newSlot ? newSlot.innerHTML : data.html;
|
|
98
|
-
|
|
99
|
-
if (data.hydrationScript) {
|
|
100
|
-
if (data.runtime && window.__NITRON_RUNTIME__) {
|
|
101
|
-
Object.assign(window.__NITRON_RUNTIME__, data.runtime);
|
|
102
|
-
}
|
|
149
|
+
// React reconciliation — only changed DOM nodes update
|
|
150
|
+
rsc.navigate(d.payload);
|
|
103
151
|
|
|
104
|
-
|
|
105
|
-
window.__NITRON_PROPS__ = data.props;
|
|
106
|
-
}
|
|
152
|
+
if (d.meta && d.meta.title) document.title = d.meta.title;
|
|
107
153
|
|
|
108
|
-
|
|
109
|
-
}
|
|
154
|
+
if (cssChanged) refreshCss();
|
|
110
155
|
})
|
|
111
156
|
.catch(function() {
|
|
157
|
+
// RSC fetch failed — full reload as last resort
|
|
112
158
|
location.reload();
|
|
113
159
|
});
|
|
114
160
|
}
|
|
115
161
|
|
|
116
|
-
|
|
117
|
-
var script = document.createElement("script");
|
|
118
|
-
script.type = "module";
|
|
119
|
-
script.src = "/storage" + scriptPath + "?t=" + Date.now();
|
|
120
|
-
|
|
121
|
-
script.onload = function() {
|
|
122
|
-
if (window.__NITRON_REFRESH__) {
|
|
123
|
-
try {
|
|
124
|
-
window.__NITRON_REFRESH__.performReactRefresh();
|
|
125
|
-
}
|
|
126
|
-
catch (e) {}
|
|
127
|
-
}
|
|
162
|
+
// --- CSS Hot Reload ---
|
|
128
163
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
document.head.appendChild(script);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// CSS Updates
|
|
136
|
-
function refreshCss(filename) {
|
|
164
|
+
// Cache-busts all stylesheets by appending ?t=<timestamp>
|
|
165
|
+
function refreshCss() {
|
|
137
166
|
var links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
138
167
|
var timestamp = Date.now();
|
|
139
168
|
|
|
140
|
-
if (!filename) {
|
|
141
|
-
for (var i = 0; i < links.length; i++) {
|
|
142
|
-
var href = (links[i].href || "").split("?")[0];
|
|
143
|
-
links[i].href = href + "?t=" + timestamp;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
var found = false;
|
|
150
|
-
var target = filename.split(/[\\/]/).pop();
|
|
151
|
-
|
|
152
169
|
for (var i = 0; i < links.length; i++) {
|
|
153
170
|
var href = (links[i].href || "").split("?")[0];
|
|
154
|
-
|
|
155
|
-
if (href.endsWith("/" + target)) {
|
|
156
|
-
links[i].href = href + "?t=" + timestamp;
|
|
157
|
-
found = true;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (!found) {
|
|
162
|
-
location.reload();
|
|
171
|
+
links[i].href = href + "?t=" + timestamp;
|
|
163
172
|
}
|
|
164
173
|
}
|
|
165
174
|
|
|
166
|
-
// Error Overlay
|
|
175
|
+
// --- Build Error Overlay ---
|
|
176
|
+
|
|
167
177
|
function showErrorOverlay(message, file) {
|
|
168
178
|
hideErrorOverlay();
|
|
169
179
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
180
|
+
var overlay = document.createElement("div");
|
|
181
|
+
overlay.id = "__nitron_error__";
|
|
182
|
+
overlay.innerHTML =
|
|
173
183
|
'<div style="position:fixed;inset:0;background:rgba(0,0,0,.95);color:#ff4444;padding:32px;font-family:monospace;z-index:999999;overflow:auto">' +
|
|
174
184
|
'<div style="font-size:24px;font-weight:bold;margin-bottom:16px">Build Error</div>' +
|
|
175
185
|
'<div style="color:#888;margin-bottom:16px">' + escapeHtml(file || "") + '</div>' +
|
|
@@ -177,7 +187,7 @@
|
|
|
177
187
|
'<button onclick="this.parentNode.parentNode.remove()" style="position:absolute;top:16px;right:16px;background:#333;color:#fff;border:none;padding:8px 16px;cursor:pointer;border-radius:4px">Close</button>' +
|
|
178
188
|
'</div>';
|
|
179
189
|
|
|
180
|
-
document.body.appendChild(
|
|
190
|
+
document.body.appendChild(overlay);
|
|
181
191
|
}
|
|
182
192
|
|
|
183
193
|
function hideErrorOverlay() {
|
|
@@ -186,11 +196,10 @@
|
|
|
186
196
|
if (el) {
|
|
187
197
|
el.remove();
|
|
188
198
|
}
|
|
189
|
-
|
|
190
|
-
errorOverlay = null;
|
|
191
199
|
}
|
|
192
200
|
|
|
193
|
-
// Utilities
|
|
201
|
+
// --- Utilities ---
|
|
202
|
+
|
|
194
203
|
function escapeHtml(str) {
|
|
195
204
|
return String(str || "")
|
|
196
205
|
.replace(/&/g, "&")
|
|
@@ -198,7 +207,8 @@
|
|
|
198
207
|
.replace(/>/g, ">");
|
|
199
208
|
}
|
|
200
209
|
|
|
201
|
-
// Initialize
|
|
210
|
+
// --- Initialize ---
|
|
211
|
+
|
|
202
212
|
if (document.readyState === "loading") {
|
|
203
213
|
document.addEventListener("DOMContentLoaded", connect);
|
|
204
214
|
}
|