@reckona/mreact-router 0.0.201 → 0.0.203
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 +6 -3
- package/dist/actions.d.ts +2 -3
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +61 -41
- package/dist/actions.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +3 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +74 -11
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/node.d.ts +7 -0
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +7 -1
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/static.d.ts.map +1 -1
- package/dist/adapters/static.js +14 -3
- package/dist/adapters/static.js.map +1 -1
- package/dist/build.d.ts +5 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +137 -14
- package/dist/build.js.map +1 -1
- package/dist/built-runtime.d.ts +4 -1
- package/dist/built-runtime.d.ts.map +1 -1
- package/dist/built-runtime.js.map +1 -1
- package/dist/cache.d.ts +27 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +83 -16
- package/dist/cache.js.map +1 -1
- package/dist/cli-options.d.ts +7 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +27 -2
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -2
- package/dist/client.js.map +1 -1
- package/dist/navigation-marker.d.ts +2 -0
- package/dist/navigation-marker.d.ts.map +1 -0
- package/dist/navigation-marker.js +294 -0
- package/dist/navigation-marker.js.map +1 -0
- package/dist/navigation-runtime.d.ts +1 -1
- package/dist/navigation-runtime.d.ts.map +1 -1
- package/dist/navigation-runtime.js +1 -1
- package/dist/navigation-runtime.js.map +1 -1
- package/dist/node-server.d.ts +6 -0
- package/dist/node-server.d.ts.map +1 -1
- package/dist/node-server.js +19 -1
- package/dist/node-server.js.map +1 -1
- package/dist/prerender-entry.d.ts +7 -0
- package/dist/prerender-entry.d.ts.map +1 -0
- package/dist/prerender-entry.js +99 -0
- package/dist/prerender-entry.js.map +1 -0
- package/dist/render.d.ts +20 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +190 -110
- package/dist/render.js.map +1 -1
- package/dist/request-header-tracking.d.ts +44 -0
- package/dist/request-header-tracking.d.ts.map +1 -0
- package/dist/request-header-tracking.js +116 -0
- package/dist/request-header-tracking.js.map +1 -0
- package/dist/route-source.d.ts +16 -0
- package/dist/route-source.d.ts.map +1 -1
- package/dist/route-source.js +55 -1
- package/dist/route-source.js.map +1 -1
- package/dist/security-headers.d.ts +16 -0
- package/dist/security-headers.d.ts.map +1 -1
- package/dist/security-headers.js +37 -0
- package/dist/security-headers.js.map +1 -1
- package/dist/serve.d.ts +10 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +132 -42
- package/dist/serve.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +72 -56
- package/src/adapters/cloudflare.ts +118 -16
- package/src/adapters/node.ts +14 -1
- package/src/adapters/static.ts +18 -5
- package/src/build.ts +176 -12
- package/src/built-runtime.ts +2 -2
- package/src/cache.ts +127 -15
- package/src/cli-options.ts +39 -2
- package/src/cli.ts +7 -1
- package/src/client.ts +15 -2
- package/src/navigation-marker.ts +353 -0
- package/src/navigation-runtime.ts +1 -0
- package/src/node-server.ts +27 -1
- package/src/prerender-entry.ts +129 -0
- package/src/render.ts +267 -121
- package/src/request-header-tracking.ts +161 -0
- package/src/route-source.ts +71 -0
- package/src/security-headers.ts +41 -0
- package/src/serve.ts +188 -48
package/src/cli.ts
CHANGED
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
resolveCliHost,
|
|
26
26
|
resolveCliHostPolicy,
|
|
27
27
|
resolveCliRequestLogMode,
|
|
28
|
+
resolveCliStartPort,
|
|
29
|
+
resolveCliTrustForwardedProto,
|
|
28
30
|
} from "./cli-options.js";
|
|
29
31
|
import { startDevServer } from "./dev-server.js";
|
|
30
32
|
import { startServer } from "./serve.js";
|
|
@@ -168,7 +170,11 @@ if (parsed !== undefined) {
|
|
|
168
170
|
hostname: resolveCliHost(parsed.host, process.env),
|
|
169
171
|
logger,
|
|
170
172
|
outDir: resolve(routeArg ?? ".mreact"),
|
|
171
|
-
port:
|
|
173
|
+
port: resolveCliStartPort(parsed.port, process.env),
|
|
174
|
+
trustForwardedProto: resolveCliTrustForwardedProto(
|
|
175
|
+
parsed.trustForwardedProto,
|
|
176
|
+
process.env,
|
|
177
|
+
),
|
|
172
178
|
});
|
|
173
179
|
console.log(`mreact app router serving built output at ${server.url}`);
|
|
174
180
|
} else {
|
package/src/client.ts
CHANGED
|
@@ -78,6 +78,7 @@ export interface BuildClientRouteOutputOptions {
|
|
|
78
78
|
filename: string;
|
|
79
79
|
minify?: boolean | undefined;
|
|
80
80
|
routePath: string;
|
|
81
|
+
restoreRequestUrl?: boolean | undefined;
|
|
81
82
|
sourceMap?: boolean | undefined;
|
|
82
83
|
vitePlugins?: readonly PluginOption[] | undefined;
|
|
83
84
|
clientNavigation?: boolean | undefined;
|
|
@@ -2632,9 +2633,18 @@ export function withHydrationMarkers(options: {
|
|
|
2632
2633
|
}
|
|
2633
2634
|
|
|
2634
2635
|
export function withRouteMarkers(options: { html: string; routePath: string }): string {
|
|
2635
|
-
const
|
|
2636
|
+
const marker = routeMarkerParts(options.routePath);
|
|
2637
|
+
|
|
2638
|
+
return `${marker.prefix}${options.html}${marker.suffix}`;
|
|
2639
|
+
}
|
|
2636
2640
|
|
|
2637
|
-
|
|
2641
|
+
export function routeMarkerParts(routePath: string): { prefix: string; suffix: string } {
|
|
2642
|
+
const routeId = routeIdForPath(routePath);
|
|
2643
|
+
|
|
2644
|
+
return {
|
|
2645
|
+
prefix: `<div ${routeHydrationContract.routeMarkerAttribute}="${escapeHtmlAttribute(routeId)}">`,
|
|
2646
|
+
suffix: "</div>",
|
|
2647
|
+
};
|
|
2638
2648
|
}
|
|
2639
2649
|
|
|
2640
2650
|
export function hydrationMarkerParts(options: {
|
|
@@ -2872,6 +2882,8 @@ export async function buildClientRouteEntrySource(
|
|
|
2872
2882
|
);
|
|
2873
2883
|
|
|
2874
2884
|
const routeId = routeIdForPath(options.routePath);
|
|
2885
|
+
const restoreRequestUrl =
|
|
2886
|
+
options.restoreRequestUrl ?? (options.debugLabels === true || /\brequest\b/.test(options.code));
|
|
2875
2887
|
const routeUsesCells = detectRouteCellStateHint(compiled.code);
|
|
2876
2888
|
const routeUsesReactiveEffect = detectRouteReactiveEffectHint(compiled.code);
|
|
2877
2889
|
const routeUsesDomRefs = compiled.metadata.imports.some(
|
|
@@ -3401,6 +3413,7 @@ ${routeOutOfOrderFragmentHydration} const __mreactMarker = document.querySelect
|
|
|
3401
3413
|
const __mreactProps = __mreactPropsText === undefined
|
|
3402
3414
|
? {}
|
|
3403
3415
|
: JSON.parse(__mreactPropsText);
|
|
3416
|
+
${restoreRequestUrl ? " if (__mreactProps.request) __mreactProps.request.url = document.URL;\n" : ""}
|
|
3404
3417
|
const __mreactClientReferences = __mreactClientReferencesElement?.textContent === undefined
|
|
3405
3418
|
? []
|
|
3406
3419
|
: JSON.parse(__mreactClientReferencesElement.textContent);
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { routeHydrationContract } from "./route-hydration-contract.js";
|
|
2
|
+
|
|
3
|
+
const markerAttribute = routeHydrationContract.routeMarkerAttribute;
|
|
4
|
+
const rawTextElementNames = new Set([
|
|
5
|
+
"iframe",
|
|
6
|
+
"noembed",
|
|
7
|
+
"noframes",
|
|
8
|
+
"noscript",
|
|
9
|
+
"style",
|
|
10
|
+
"textarea",
|
|
11
|
+
"title",
|
|
12
|
+
"xmp",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
interface ParsedStartTag {
|
|
16
|
+
end: number;
|
|
17
|
+
hasMarker: boolean;
|
|
18
|
+
name: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function hasNavigationRouteMarker(html: string): boolean {
|
|
22
|
+
let cursor = 0;
|
|
23
|
+
|
|
24
|
+
while (cursor < html.length) {
|
|
25
|
+
const openingBracket = html.indexOf("<", cursor);
|
|
26
|
+
if (openingBracket < 0) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (html.startsWith("<!--", openingBracket)) {
|
|
31
|
+
cursor = skipComment(html, openingBracket + 4);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (html.startsWith("<![CDATA[", openingBracket)) {
|
|
36
|
+
cursor = skipCdataSection(html, openingBracket + 9);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const discriminator = html[openingBracket + 1];
|
|
41
|
+
if (discriminator === "!" || discriminator === "?" || discriminator === "/") {
|
|
42
|
+
cursor = skipTagTail(html, openingBracket + 2);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!isAsciiLetter(discriminator)) {
|
|
46
|
+
cursor = openingBracket + 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const tag = parseStartTag(html, openingBracket);
|
|
51
|
+
if (tag === undefined) {
|
|
52
|
+
cursor = openingBracket + 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (tag.hasMarker) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (tag.name === "plaintext") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
cursor =
|
|
64
|
+
tag.name === "script"
|
|
65
|
+
? skipScriptElement(html, tag.end)
|
|
66
|
+
: rawTextElementNames.has(tag.name)
|
|
67
|
+
? skipRawTextElement(html, tag.end, tag.name)
|
|
68
|
+
: tag.end;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseStartTag(html: string, openingBracket: number): ParsedStartTag | undefined {
|
|
75
|
+
let cursor = openingBracket + 1;
|
|
76
|
+
const nameStart = cursor;
|
|
77
|
+
|
|
78
|
+
while (cursor < html.length && !isTagNameTerminator(html[cursor])) {
|
|
79
|
+
if (isForbiddenTagNameCharacter(html[cursor])) {
|
|
80
|
+
return invalidStartTag(html, cursor, "");
|
|
81
|
+
}
|
|
82
|
+
cursor += 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (cursor === nameStart) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const name = html.slice(nameStart, cursor).toLowerCase();
|
|
90
|
+
let markerValueIsNonEmpty: boolean | undefined;
|
|
91
|
+
|
|
92
|
+
for (;;) {
|
|
93
|
+
cursor = skipAsciiWhitespace(html, cursor);
|
|
94
|
+
if (cursor >= html.length) {
|
|
95
|
+
return { end: html.length, hasMarker: false, name };
|
|
96
|
+
}
|
|
97
|
+
if (html[cursor] === ">") {
|
|
98
|
+
return { end: cursor + 1, hasMarker: markerValueIsNonEmpty === true, name };
|
|
99
|
+
}
|
|
100
|
+
if (html[cursor] === "/" && html[cursor + 1] === ">") {
|
|
101
|
+
return { end: cursor + 2, hasMarker: markerValueIsNonEmpty === true, name };
|
|
102
|
+
}
|
|
103
|
+
if (html[cursor] === "/") {
|
|
104
|
+
return invalidStartTag(html, cursor, name);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const attributeNameStart = cursor;
|
|
108
|
+
while (cursor < html.length && !isAttributeNameTerminator(html[cursor])) {
|
|
109
|
+
if (isForbiddenAttributeNameCharacter(html[cursor])) {
|
|
110
|
+
return invalidStartTag(html, cursor, name);
|
|
111
|
+
}
|
|
112
|
+
cursor += 1;
|
|
113
|
+
}
|
|
114
|
+
if (cursor === attributeNameStart) {
|
|
115
|
+
return invalidStartTag(html, cursor, name);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const attributeName = html.slice(attributeNameStart, cursor).toLowerCase();
|
|
119
|
+
cursor = skipAsciiWhitespace(html, cursor);
|
|
120
|
+
let valueIsNonEmpty = false;
|
|
121
|
+
|
|
122
|
+
if (html[cursor] === "=") {
|
|
123
|
+
cursor = skipAsciiWhitespace(html, cursor + 1);
|
|
124
|
+
const quote = html[cursor];
|
|
125
|
+
|
|
126
|
+
if (quote === '"' || quote === "'") {
|
|
127
|
+
const valueStart = cursor + 1;
|
|
128
|
+
const valueEnd = html.indexOf(quote, valueStart);
|
|
129
|
+
if (valueEnd < 0) {
|
|
130
|
+
return { end: html.length, hasMarker: false, name };
|
|
131
|
+
}
|
|
132
|
+
valueIsNonEmpty = valueEnd > valueStart;
|
|
133
|
+
cursor = valueEnd + 1;
|
|
134
|
+
} else {
|
|
135
|
+
const valueStart = cursor;
|
|
136
|
+
while (cursor < html.length && !isUnquotedValueTerminator(html[cursor])) {
|
|
137
|
+
if (isForbiddenUnquotedValueCharacter(html[cursor])) {
|
|
138
|
+
return invalidStartTag(html, cursor, name);
|
|
139
|
+
}
|
|
140
|
+
cursor += 1;
|
|
141
|
+
}
|
|
142
|
+
if (cursor === valueStart) {
|
|
143
|
+
return invalidStartTag(html, cursor, name);
|
|
144
|
+
}
|
|
145
|
+
valueIsNonEmpty = true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (attributeName === markerAttribute && markerValueIsNonEmpty === undefined) {
|
|
150
|
+
markerValueIsNonEmpty = valueIsNonEmpty;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function invalidStartTag(html: string, cursor: number, name: string): ParsedStartTag {
|
|
156
|
+
return { end: skipTagTail(html, cursor), hasMarker: false, name };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function skipComment(html: string, cursor: number): number {
|
|
160
|
+
const standardEnd = html.indexOf("-->", cursor);
|
|
161
|
+
const abruptEnd = html.indexOf("--!>", cursor);
|
|
162
|
+
const end =
|
|
163
|
+
standardEnd < 0 ? abruptEnd : abruptEnd < 0 ? standardEnd : Math.min(standardEnd, abruptEnd);
|
|
164
|
+
return end < 0 ? html.length : end + (end === abruptEnd ? 4 : 3);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function skipCdataSection(html: string, cursor: number): number {
|
|
168
|
+
const end = html.indexOf("]]>", cursor);
|
|
169
|
+
return end < 0 ? html.length : end + 3;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function skipTagTail(html: string, cursor: number): number {
|
|
173
|
+
let quote: '"' | "'" | undefined;
|
|
174
|
+
while (cursor < html.length) {
|
|
175
|
+
const character = html[cursor];
|
|
176
|
+
if (quote !== undefined) {
|
|
177
|
+
if (character === quote) {
|
|
178
|
+
quote = undefined;
|
|
179
|
+
}
|
|
180
|
+
} else if (character === '"' || character === "'") {
|
|
181
|
+
quote = character;
|
|
182
|
+
} else if (character === ">") {
|
|
183
|
+
return cursor + 1;
|
|
184
|
+
}
|
|
185
|
+
cursor += 1;
|
|
186
|
+
}
|
|
187
|
+
return html.length;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function skipRawTextElement(html: string, cursor: number, name: string): number {
|
|
191
|
+
const closingPrefix = `</${name}`;
|
|
192
|
+
for (;;) {
|
|
193
|
+
const closingStart = indexOfAsciiCaseInsensitive(html, closingPrefix, cursor);
|
|
194
|
+
if (closingStart < 0) {
|
|
195
|
+
return html.length;
|
|
196
|
+
}
|
|
197
|
+
const delimiter = html[closingStart + closingPrefix.length];
|
|
198
|
+
if (delimiter === ">" || delimiter === "/" || isAsciiWhitespace(delimiter)) {
|
|
199
|
+
return skipTagTail(html, closingStart + closingPrefix.length);
|
|
200
|
+
}
|
|
201
|
+
cursor = closingStart + 2;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function skipScriptElement(html: string, cursor: number): number {
|
|
206
|
+
let state: "data" | "double-escaped" | "escaped" = "data";
|
|
207
|
+
|
|
208
|
+
while (cursor < html.length) {
|
|
209
|
+
if (state === "data" && html.startsWith("<!--", cursor)) {
|
|
210
|
+
state = "escaped";
|
|
211
|
+
cursor += 4;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (state !== "data" && html.startsWith("-->", cursor)) {
|
|
216
|
+
state = "data";
|
|
217
|
+
cursor += 3;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (html[cursor] === "<") {
|
|
222
|
+
if (isScriptTagAt(html, cursor, true)) {
|
|
223
|
+
if (state === "double-escaped") {
|
|
224
|
+
state = "escaped";
|
|
225
|
+
cursor += 8;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
return skipTagTail(html, cursor + 8);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (state === "escaped" && isScriptTagAt(html, cursor, false)) {
|
|
232
|
+
state = "double-escaped";
|
|
233
|
+
cursor += 7;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
cursor += 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return html.length;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function isScriptTagAt(html: string, cursor: number, closing: boolean): boolean {
|
|
245
|
+
const prefix = closing ? "</script" : "<script";
|
|
246
|
+
if (!matchesAsciiCaseInsensitive(html, prefix, cursor)) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
const delimiter = html[cursor + prefix.length];
|
|
250
|
+
return delimiter === ">" || delimiter === "/" || isAsciiWhitespace(delimiter);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function matchesAsciiCaseInsensitive(html: string, needle: string, start: number): boolean {
|
|
254
|
+
if (start + needle.length > html.length) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
for (let offset = 0; offset < needle.length; offset += 1) {
|
|
258
|
+
if (html.charAt(start + offset).toLowerCase() !== needle.charAt(offset)) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function indexOfAsciiCaseInsensitive(html: string, needle: string, start: number): number {
|
|
266
|
+
const lastStart = html.length - needle.length;
|
|
267
|
+
for (let index = start; index <= lastStart; index += 1) {
|
|
268
|
+
let offset = 0;
|
|
269
|
+
while (
|
|
270
|
+
offset < needle.length &&
|
|
271
|
+
html.charAt(index + offset).toLowerCase() === needle.charAt(offset)
|
|
272
|
+
) {
|
|
273
|
+
offset += 1;
|
|
274
|
+
}
|
|
275
|
+
if (offset === needle.length) {
|
|
276
|
+
return index;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return -1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function skipAsciiWhitespace(html: string, cursor: number): number {
|
|
283
|
+
while (isAsciiWhitespace(html[cursor])) {
|
|
284
|
+
cursor += 1;
|
|
285
|
+
}
|
|
286
|
+
return cursor;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function isAsciiWhitespace(character: string | undefined): boolean {
|
|
290
|
+
return (
|
|
291
|
+
character === " " ||
|
|
292
|
+
character === "\t" ||
|
|
293
|
+
character === "\n" ||
|
|
294
|
+
character === "\f" ||
|
|
295
|
+
character === "\r"
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isAsciiLetter(character: string | undefined): boolean {
|
|
300
|
+
if (character === undefined) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
const code = character.charCodeAt(0);
|
|
304
|
+
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function isTagNameTerminator(character: string | undefined): boolean {
|
|
308
|
+
return (
|
|
309
|
+
character === undefined ||
|
|
310
|
+
character === ">" ||
|
|
311
|
+
character === "/" ||
|
|
312
|
+
isAsciiWhitespace(character)
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isForbiddenTagNameCharacter(character: string | undefined): boolean {
|
|
317
|
+
return (
|
|
318
|
+
character === "\0" ||
|
|
319
|
+
character === '"' ||
|
|
320
|
+
character === "'" ||
|
|
321
|
+
character === "<" ||
|
|
322
|
+
character === "="
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isAttributeNameTerminator(character: string | undefined): boolean {
|
|
327
|
+
return (
|
|
328
|
+
character === undefined ||
|
|
329
|
+
character === ">" ||
|
|
330
|
+
character === "/" ||
|
|
331
|
+
character === "=" ||
|
|
332
|
+
isAsciiWhitespace(character)
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isForbiddenAttributeNameCharacter(character: string | undefined): boolean {
|
|
337
|
+
return character === "\0" || character === '"' || character === "'" || character === "<";
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function isUnquotedValueTerminator(character: string | undefined): boolean {
|
|
341
|
+
return character === undefined || character === ">" || isAsciiWhitespace(character);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function isForbiddenUnquotedValueCharacter(character: string | undefined): boolean {
|
|
345
|
+
return (
|
|
346
|
+
character === "\0" ||
|
|
347
|
+
character === '"' ||
|
|
348
|
+
character === "'" ||
|
|
349
|
+
character === "`" ||
|
|
350
|
+
character === "<" ||
|
|
351
|
+
character === "="
|
|
352
|
+
);
|
|
353
|
+
}
|
package/src/node-server.ts
CHANGED
|
@@ -30,6 +30,27 @@ export interface StartNodeRequestServerOptions {
|
|
|
30
30
|
hostPolicy?: "strict" | "trusted-proxy" | undefined;
|
|
31
31
|
rawHost: string | undefined;
|
|
32
32
|
}) => string) | undefined;
|
|
33
|
+
trustForwardedProto?: boolean | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveNodeRequestProtocol(options: {
|
|
37
|
+
encrypted: boolean;
|
|
38
|
+
forwardedProto: string | readonly string[] | undefined;
|
|
39
|
+
trustForwardedProto?: boolean | undefined;
|
|
40
|
+
}): "http" | "https" {
|
|
41
|
+
if (options.encrypted) {
|
|
42
|
+
return "https";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (options.trustForwardedProto !== true) {
|
|
46
|
+
return "http";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const forwarded = Array.isArray(options.forwardedProto)
|
|
50
|
+
? options.forwardedProto[0]
|
|
51
|
+
: options.forwardedProto;
|
|
52
|
+
const first = forwarded?.split(",", 1)[0]?.trim().toLowerCase();
|
|
53
|
+
return first === "https" ? "https" : "http";
|
|
33
54
|
}
|
|
34
55
|
|
|
35
56
|
export async function startNodeRequestServer(
|
|
@@ -51,7 +72,12 @@ export async function startNodeRequestServer(
|
|
|
51
72
|
hostPolicy: options.hostPolicy,
|
|
52
73
|
rawHost: incoming.headers.host,
|
|
53
74
|
});
|
|
54
|
-
const
|
|
75
|
+
const protocol = resolveNodeRequestProtocol({
|
|
76
|
+
encrypted: (incoming.socket as { encrypted?: boolean }).encrypted === true,
|
|
77
|
+
forwardedProto: incoming.headers["x-forwarded-proto"],
|
|
78
|
+
trustForwardedProto: options.trustForwardedProto,
|
|
79
|
+
});
|
|
80
|
+
const request = nodeRequestToWebRequest(incoming, `${protocol}://${host}`);
|
|
55
81
|
const logFields = requestLogFields(request, "node");
|
|
56
82
|
emitRouterLog(options.logger, "info", {
|
|
57
83
|
...logFields,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { BuiltPrerenderedRoute } from "./build.js";
|
|
2
|
+
import { hasNavigationRouteMarker } from "./navigation-marker.js";
|
|
3
|
+
|
|
4
|
+
export const PRERENDERED_ROUTE_SCHEMA_VERSION = 4;
|
|
5
|
+
|
|
6
|
+
export function isCurrentPrerenderedRoute(value: unknown): value is BuiltPrerenderedRoute {
|
|
7
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const entry = value as Partial<BuiltPrerenderedRoute>;
|
|
12
|
+
return (
|
|
13
|
+
entry.schemaVersion === PRERENDERED_ROUTE_SCHEMA_VERSION &&
|
|
14
|
+
isStringRecord(entry.headers) &&
|
|
15
|
+
isShareableHeaderRecord(entry.headers) &&
|
|
16
|
+
hasRequiredNavigationVary(entry.headers) &&
|
|
17
|
+
!hasStoredHstsHeader(entry.headers) &&
|
|
18
|
+
isCanonicalHstsHeader(entry.strictTransportSecurity) &&
|
|
19
|
+
typeof entry.html === "string" &&
|
|
20
|
+
(entry.navigationHtml === undefined ||
|
|
21
|
+
(typeof entry.navigationHtml === "string" &&
|
|
22
|
+
hasNavigationRouteMarker(entry.navigationHtml))) &&
|
|
23
|
+
typeof entry.status === "number" &&
|
|
24
|
+
Number.isInteger(entry.status) &&
|
|
25
|
+
entry.status >= 100 &&
|
|
26
|
+
entry.status <= 599
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasStoredHstsHeader(headers: Record<string, string>): boolean {
|
|
31
|
+
try {
|
|
32
|
+
return new Headers(headers).has("strict-transport-security");
|
|
33
|
+
} catch {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isCanonicalHstsHeader(value: unknown): value is string | undefined {
|
|
39
|
+
return (
|
|
40
|
+
value === undefined ||
|
|
41
|
+
(typeof value === "string" &&
|
|
42
|
+
/^max-age=(?:0|[1-9][0-9]*)(?:; includeSubDomains)?(?:; preload)?$/.test(value))
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isVisitorDependentResponse(response: Response): boolean {
|
|
47
|
+
return isVisitorDependentHeaders(response.headers);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function storedPrerenderedRouteHeaders(headers: Headers): Record<string, string> {
|
|
51
|
+
const stored: Record<string, string> = {};
|
|
52
|
+
|
|
53
|
+
headers.forEach((value, key) => {
|
|
54
|
+
if (key !== "strict-transport-security") {
|
|
55
|
+
stored[key] = value;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return stored;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function replayedPrerenderedRouteHeaders(
|
|
63
|
+
entry: BuiltPrerenderedRoute,
|
|
64
|
+
request: Request,
|
|
65
|
+
): Headers {
|
|
66
|
+
const headers = new Headers(entry.headers);
|
|
67
|
+
|
|
68
|
+
if (!hasNavigationVary(headers)) {
|
|
69
|
+
headers.append("vary", "x-mreact-navigation");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (entry.strictTransportSecurity !== undefined && request.url.startsWith("https://")) {
|
|
73
|
+
headers.set("strict-transport-security", entry.strictTransportSecurity);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return headers;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasRequiredNavigationVary(headers: Record<string, string>): boolean {
|
|
80
|
+
try {
|
|
81
|
+
return hasNavigationVary(new Headers(headers));
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function hasNavigationVary(headers: Headers): boolean {
|
|
88
|
+
return (
|
|
89
|
+
headers
|
|
90
|
+
.get("vary")
|
|
91
|
+
?.split(",")
|
|
92
|
+
.some((value) => value.trim().toLowerCase() === "x-mreact-navigation") === true
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isShareableHeaderRecord(headers: Record<string, string>): boolean {
|
|
97
|
+
try {
|
|
98
|
+
return !isVisitorDependentHeaders(new Headers(headers));
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isVisitorDependentHeaders(headers: Headers): boolean {
|
|
105
|
+
const cacheControl = headers.get("cache-control") ?? "";
|
|
106
|
+
const vary = headers.get("vary");
|
|
107
|
+
const hasVisitorDependentVary =
|
|
108
|
+
vary !== null &&
|
|
109
|
+
vary.split(",").some((value) => value.trim().toLowerCase() !== "x-mreact-navigation");
|
|
110
|
+
const forbidsSharedStorage = cacheControl
|
|
111
|
+
.split(",")
|
|
112
|
+
.some((directive) => /^(?:private|no-cache|no-store)(?:=|$)/i.test(directive.trim()));
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
headers.get("x-mreact-cache")?.toUpperCase() === "DYNAMIC" ||
|
|
116
|
+
headers.has("set-cookie") ||
|
|
117
|
+
hasVisitorDependentVary ||
|
|
118
|
+
forbidsSharedStorage
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
123
|
+
return (
|
|
124
|
+
typeof value === "object" &&
|
|
125
|
+
value !== null &&
|
|
126
|
+
!Array.isArray(value) &&
|
|
127
|
+
Object.values(value).every((entry) => typeof entry === "string")
|
|
128
|
+
);
|
|
129
|
+
}
|