@reckona/mreact-router 0.0.202 → 0.0.204
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 +58 -11
- package/dist/actions.d.ts +8 -3
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +125 -45
- package/dist/actions.js.map +1 -1
- package/dist/adapters/aws-lambda.d.ts +15 -4
- package/dist/adapters/aws-lambda.d.ts.map +1 -1
- package/dist/adapters/aws-lambda.js +301 -63
- package/dist/adapters/aws-lambda.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +5 -2
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +35 -11
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/edge.d.ts +1 -1
- package/dist/adapters/edge.d.ts.map +1 -1
- package/dist/adapters/edge.js.map +1 -1
- package/dist/adapters/node.d.ts +5 -3
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +2 -1
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/static.d.ts.map +1 -1
- package/dist/adapters/static.js +24 -2
- package/dist/adapters/static.js.map +1 -1
- package/dist/build.d.ts +4 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +185 -13
- package/dist/build.js.map +1 -1
- package/dist/built-runtime.d.ts +2 -0
- package/dist/built-runtime.d.ts.map +1 -1
- package/dist/built-runtime.js +16 -0
- package/dist/built-runtime.js.map +1 -1
- package/dist/cache.d.ts +4 -2
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +216 -17
- package/dist/cache.js.map +1 -1
- package/dist/cli-options.d.ts +3 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +10 -2
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js +14 -3
- 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 +43 -3
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -2
- package/dist/config.js.map +1 -1
- package/dist/csp.d.ts +1 -0
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +9 -0
- package/dist/csp.js.map +1 -1
- package/dist/dehydrate-policy.d.ts +3 -0
- package/dist/dehydrate-policy.d.ts.map +1 -0
- package/dist/dehydrate-policy.js +16 -0
- package/dist/dehydrate-policy.js.map +1 -0
- package/dist/dev-server.d.ts +7 -2
- package/dist/dev-server.d.ts.map +1 -1
- package/dist/dev-server.js +120 -11
- package/dist/dev-server.js.map +1 -1
- package/dist/http.d.ts +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +147 -7
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +22 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +22 -8
- package/dist/logger.js.map +1 -1
- package/dist/metadata.d.ts +8 -0
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +60 -22
- package/dist/metadata.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 +357 -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/navigation.d.ts +1 -0
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +1 -1
- package/dist/navigation.js.map +1 -1
- package/dist/node-server.d.ts +5 -2
- package/dist/node-server.d.ts.map +1 -1
- package/dist/node-server.js +75 -6
- package/dist/node-server.js.map +1 -1
- package/dist/prerender-entry.d.ts +17 -1
- package/dist/prerender-entry.d.ts.map +1 -1
- package/dist/prerender-entry.js +98 -3
- package/dist/prerender-entry.js.map +1 -1
- package/dist/render.d.ts +16 -2
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +142 -135
- package/dist/render.js.map +1 -1
- package/dist/serve.d.ts +11 -2
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +67 -41
- package/dist/serve.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +52 -19
- package/dist/session.js.map +1 -1
- package/dist/upgrade.d.ts +59 -2
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +282 -1
- package/dist/upgrade.js.map +1 -1
- package/dist/vite-config.d.ts +2 -0
- package/dist/vite-config.d.ts.map +1 -1
- package/dist/vite-config.js +2 -0
- package/dist/vite-config.js.map +1 -1
- package/dist/vite.d.ts +4 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +117 -6
- package/dist/vite.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +146 -60
- package/src/adapters/aws-lambda.ts +426 -115
- package/src/adapters/cloudflare.ts +58 -12
- package/src/adapters/edge.ts +2 -0
- package/src/adapters/node.ts +8 -10
- package/src/adapters/static.ts +31 -2
- package/src/build.ts +217 -13
- package/src/built-runtime.ts +28 -0
- package/src/cache.ts +272 -19
- package/src/cli-options.ts +15 -2
- package/src/cli.ts +16 -2
- package/src/client.ts +47 -3
- package/src/config.ts +43 -6
- package/src/csp.ts +11 -0
- package/src/dehydrate-policy.ts +23 -0
- package/src/dev-server.ts +188 -15
- package/src/http.ts +172 -6
- package/src/index.ts +14 -1
- package/src/logger.ts +47 -10
- package/src/metadata.ts +125 -60
- package/src/navigation-marker.ts +423 -0
- package/src/navigation-runtime.ts +1 -0
- package/src/navigation.ts +1 -1
- package/src/node-server.ts +128 -22
- package/src/prerender-entry.ts +127 -5
- package/src/render.ts +859 -749
- package/src/serve.ts +112 -82
- package/src/session.ts +56 -25
- package/src/upgrade.ts +402 -3
- package/src/vite-config.ts +6 -0
- package/src/vite.ts +157 -5
|
@@ -0,0 +1,423 @@
|
|
|
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
|
+
selfClosing: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hasNavigationRouteMarker(html: string): boolean {
|
|
23
|
+
let cursor = 0;
|
|
24
|
+
let foreignContentDepth = 0;
|
|
25
|
+
let templateDepth = 0;
|
|
26
|
+
|
|
27
|
+
while (cursor < html.length) {
|
|
28
|
+
const openingBracket = html.indexOf("<", cursor);
|
|
29
|
+
if (openingBracket < 0) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (html.startsWith("<!--", openingBracket)) {
|
|
34
|
+
cursor = skipComment(html, openingBracket + 4);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (html.startsWith("<![CDATA[", openingBracket)) {
|
|
39
|
+
cursor =
|
|
40
|
+
foreignContentDepth > 0
|
|
41
|
+
? skipCdataSection(html, openingBracket + 9)
|
|
42
|
+
: skipBogusComment(html, openingBracket + 2);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const discriminator = html[openingBracket + 1];
|
|
47
|
+
if (discriminator === "/") {
|
|
48
|
+
const endTag = parseEndTag(html, openingBracket);
|
|
49
|
+
if (endTag.name === "template" && templateDepth > 0) {
|
|
50
|
+
templateDepth -= 1;
|
|
51
|
+
} else if (
|
|
52
|
+
(endTag.name === "svg" || endTag.name === "math") &&
|
|
53
|
+
foreignContentDepth > 0
|
|
54
|
+
) {
|
|
55
|
+
foreignContentDepth -= 1;
|
|
56
|
+
}
|
|
57
|
+
cursor = endTag.end;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (discriminator === "!" || discriminator === "?") {
|
|
61
|
+
cursor = skipTagTail(html, openingBracket + 2);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (!isAsciiLetter(discriminator)) {
|
|
65
|
+
cursor = openingBracket + 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const tag = parseStartTag(html, openingBracket);
|
|
70
|
+
if (tag === undefined) {
|
|
71
|
+
cursor = openingBracket + 1;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
tag.hasMarker &&
|
|
76
|
+
templateDepth === 0 &&
|
|
77
|
+
tag.name !== "html" &&
|
|
78
|
+
tag.name !== "head" &&
|
|
79
|
+
tag.name !== "body" &&
|
|
80
|
+
tag.name !== "frameset"
|
|
81
|
+
) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (tag.name === "plaintext") {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!tag.selfClosing && tag.name === "template") {
|
|
90
|
+
templateDepth += 1;
|
|
91
|
+
} else if (!tag.selfClosing && (tag.name === "svg" || tag.name === "math")) {
|
|
92
|
+
foreignContentDepth += 1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
cursor =
|
|
96
|
+
tag.name === "script"
|
|
97
|
+
? skipScriptElement(html, tag.end)
|
|
98
|
+
: foreignContentDepth === 0 && rawTextElementNames.has(tag.name)
|
|
99
|
+
? skipRawTextElement(html, tag.end, tag.name)
|
|
100
|
+
: tag.end;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseStartTag(html: string, openingBracket: number): ParsedStartTag | undefined {
|
|
107
|
+
let cursor = openingBracket + 1;
|
|
108
|
+
const nameStart = cursor;
|
|
109
|
+
|
|
110
|
+
while (cursor < html.length && !isTagNameTerminator(html[cursor])) {
|
|
111
|
+
if (isForbiddenTagNameCharacter(html[cursor])) {
|
|
112
|
+
return invalidStartTag(html, cursor, "");
|
|
113
|
+
}
|
|
114
|
+
cursor += 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (cursor === nameStart) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const name = html.slice(nameStart, cursor).toLowerCase();
|
|
122
|
+
let markerValueIsNonEmpty: boolean | undefined;
|
|
123
|
+
|
|
124
|
+
for (;;) {
|
|
125
|
+
cursor = skipAsciiWhitespace(html, cursor);
|
|
126
|
+
if (cursor >= html.length) {
|
|
127
|
+
return { end: html.length, hasMarker: false, name, selfClosing: false };
|
|
128
|
+
}
|
|
129
|
+
if (html[cursor] === ">") {
|
|
130
|
+
return {
|
|
131
|
+
end: cursor + 1,
|
|
132
|
+
hasMarker: markerValueIsNonEmpty === true,
|
|
133
|
+
name,
|
|
134
|
+
selfClosing: false,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (html[cursor] === "/" && html[cursor + 1] === ">") {
|
|
138
|
+
return {
|
|
139
|
+
end: cursor + 2,
|
|
140
|
+
hasMarker: markerValueIsNonEmpty === true,
|
|
141
|
+
name,
|
|
142
|
+
selfClosing: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (html[cursor] === "/") {
|
|
146
|
+
return invalidStartTag(html, cursor, name);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const attributeNameStart = cursor;
|
|
150
|
+
while (cursor < html.length && !isAttributeNameTerminator(html[cursor])) {
|
|
151
|
+
if (isForbiddenAttributeNameCharacter(html[cursor])) {
|
|
152
|
+
return invalidStartTag(html, cursor, name);
|
|
153
|
+
}
|
|
154
|
+
cursor += 1;
|
|
155
|
+
}
|
|
156
|
+
if (cursor === attributeNameStart) {
|
|
157
|
+
return invalidStartTag(html, cursor, name);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const attributeName = html.slice(attributeNameStart, cursor).toLowerCase();
|
|
161
|
+
cursor = skipAsciiWhitespace(html, cursor);
|
|
162
|
+
let valueIsNonEmpty = false;
|
|
163
|
+
|
|
164
|
+
if (html[cursor] === "=") {
|
|
165
|
+
cursor = skipAsciiWhitespace(html, cursor + 1);
|
|
166
|
+
const quote = html[cursor];
|
|
167
|
+
|
|
168
|
+
if (quote === '"' || quote === "'") {
|
|
169
|
+
const valueStart = cursor + 1;
|
|
170
|
+
const valueEnd = html.indexOf(quote, valueStart);
|
|
171
|
+
if (valueEnd < 0) {
|
|
172
|
+
return { end: html.length, hasMarker: false, name, selfClosing: false };
|
|
173
|
+
}
|
|
174
|
+
valueIsNonEmpty = valueEnd > valueStart;
|
|
175
|
+
cursor = valueEnd + 1;
|
|
176
|
+
} else {
|
|
177
|
+
const valueStart = cursor;
|
|
178
|
+
while (cursor < html.length && !isUnquotedValueTerminator(html[cursor])) {
|
|
179
|
+
if (isForbiddenUnquotedValueCharacter(html[cursor])) {
|
|
180
|
+
return invalidStartTag(html, cursor, name);
|
|
181
|
+
}
|
|
182
|
+
cursor += 1;
|
|
183
|
+
}
|
|
184
|
+
if (cursor === valueStart) {
|
|
185
|
+
return invalidStartTag(html, cursor, name);
|
|
186
|
+
}
|
|
187
|
+
valueIsNonEmpty = true;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (attributeName === markerAttribute && markerValueIsNonEmpty === undefined) {
|
|
192
|
+
markerValueIsNonEmpty = valueIsNonEmpty;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function invalidStartTag(html: string, cursor: number, name: string): ParsedStartTag {
|
|
198
|
+
return { end: skipMalformedTagTail(html, cursor), hasMarker: false, name, selfClosing: false };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function skipComment(html: string, cursor: number): number {
|
|
202
|
+
if (html[cursor] === ">") {
|
|
203
|
+
return cursor + 1;
|
|
204
|
+
}
|
|
205
|
+
if (html.startsWith("->", cursor)) {
|
|
206
|
+
return cursor + 2;
|
|
207
|
+
}
|
|
208
|
+
const standardEnd = html.indexOf("-->", cursor);
|
|
209
|
+
const abruptEnd = html.indexOf("--!>", cursor);
|
|
210
|
+
const end =
|
|
211
|
+
standardEnd < 0 ? abruptEnd : abruptEnd < 0 ? standardEnd : Math.min(standardEnd, abruptEnd);
|
|
212
|
+
return end < 0 ? html.length : end + (end === abruptEnd ? 4 : 3);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function skipBogusComment(html: string, cursor: number): number {
|
|
216
|
+
const end = html.indexOf(">", cursor);
|
|
217
|
+
return end < 0 ? html.length : end + 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function parseEndTag(html: string, openingBracket: number): { end: number; name: string } {
|
|
221
|
+
let cursor = openingBracket + 2;
|
|
222
|
+
const nameStart = cursor;
|
|
223
|
+
while (isAsciiLetter(html[cursor])) {
|
|
224
|
+
cursor += 1;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
end: skipTagTail(html, cursor),
|
|
228
|
+
name: html.slice(nameStart, cursor).toLowerCase(),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function skipMalformedTagTail(html: string, cursor: number): number {
|
|
233
|
+
const end = html.indexOf(">", cursor);
|
|
234
|
+
return end < 0 ? html.length : end + 1;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function skipCdataSection(html: string, cursor: number): number {
|
|
238
|
+
const end = html.indexOf("]]>", cursor);
|
|
239
|
+
return end < 0 ? html.length : end + 3;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function skipTagTail(html: string, cursor: number): number {
|
|
243
|
+
let quote: '"' | "'" | undefined;
|
|
244
|
+
while (cursor < html.length) {
|
|
245
|
+
const character = html[cursor];
|
|
246
|
+
if (quote !== undefined) {
|
|
247
|
+
if (character === quote) {
|
|
248
|
+
quote = undefined;
|
|
249
|
+
}
|
|
250
|
+
} else if (character === '"' || character === "'") {
|
|
251
|
+
quote = character;
|
|
252
|
+
} else if (character === ">") {
|
|
253
|
+
return cursor + 1;
|
|
254
|
+
}
|
|
255
|
+
cursor += 1;
|
|
256
|
+
}
|
|
257
|
+
return html.length;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function skipRawTextElement(html: string, cursor: number, name: string): number {
|
|
261
|
+
const closingPrefix = `</${name}`;
|
|
262
|
+
for (;;) {
|
|
263
|
+
const closingStart = indexOfAsciiCaseInsensitive(html, closingPrefix, cursor);
|
|
264
|
+
if (closingStart < 0) {
|
|
265
|
+
return html.length;
|
|
266
|
+
}
|
|
267
|
+
const delimiter = html[closingStart + closingPrefix.length];
|
|
268
|
+
if (delimiter === ">" || delimiter === "/" || isAsciiWhitespace(delimiter)) {
|
|
269
|
+
return skipTagTail(html, closingStart + closingPrefix.length);
|
|
270
|
+
}
|
|
271
|
+
cursor = closingStart + 2;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function skipScriptElement(html: string, cursor: number): number {
|
|
276
|
+
let state: "data" | "double-escaped" | "escaped" = "data";
|
|
277
|
+
|
|
278
|
+
while (cursor < html.length) {
|
|
279
|
+
if (state === "data" && html.startsWith("<!--", cursor)) {
|
|
280
|
+
state = "escaped";
|
|
281
|
+
cursor += 4;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (state !== "data" && html.startsWith("-->", cursor)) {
|
|
286
|
+
state = "data";
|
|
287
|
+
cursor += 3;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (html[cursor] === "<") {
|
|
292
|
+
if (isScriptTagAt(html, cursor, true)) {
|
|
293
|
+
if (state === "double-escaped") {
|
|
294
|
+
state = "escaped";
|
|
295
|
+
cursor += 8;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
return skipTagTail(html, cursor + 8);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (state === "escaped" && isScriptTagAt(html, cursor, false)) {
|
|
302
|
+
state = "double-escaped";
|
|
303
|
+
cursor += 7;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
cursor += 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return html.length;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function isScriptTagAt(html: string, cursor: number, closing: boolean): boolean {
|
|
315
|
+
const prefix = closing ? "</script" : "<script";
|
|
316
|
+
if (!matchesAsciiCaseInsensitive(html, prefix, cursor)) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
const delimiter = html[cursor + prefix.length];
|
|
320
|
+
return delimiter === ">" || delimiter === "/" || isAsciiWhitespace(delimiter);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function matchesAsciiCaseInsensitive(html: string, needle: string, start: number): boolean {
|
|
324
|
+
if (start + needle.length > html.length) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
for (let offset = 0; offset < needle.length; offset += 1) {
|
|
328
|
+
if (html.charAt(start + offset).toLowerCase() !== needle.charAt(offset)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function indexOfAsciiCaseInsensitive(html: string, needle: string, start: number): number {
|
|
336
|
+
const lastStart = html.length - needle.length;
|
|
337
|
+
for (let index = start; index <= lastStart; index += 1) {
|
|
338
|
+
let offset = 0;
|
|
339
|
+
while (
|
|
340
|
+
offset < needle.length &&
|
|
341
|
+
html.charAt(index + offset).toLowerCase() === needle.charAt(offset)
|
|
342
|
+
) {
|
|
343
|
+
offset += 1;
|
|
344
|
+
}
|
|
345
|
+
if (offset === needle.length) {
|
|
346
|
+
return index;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return -1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function skipAsciiWhitespace(html: string, cursor: number): number {
|
|
353
|
+
while (isAsciiWhitespace(html[cursor])) {
|
|
354
|
+
cursor += 1;
|
|
355
|
+
}
|
|
356
|
+
return cursor;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function isAsciiWhitespace(character: string | undefined): boolean {
|
|
360
|
+
return (
|
|
361
|
+
character === " " ||
|
|
362
|
+
character === "\t" ||
|
|
363
|
+
character === "\n" ||
|
|
364
|
+
character === "\f" ||
|
|
365
|
+
character === "\r"
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function isAsciiLetter(character: string | undefined): boolean {
|
|
370
|
+
if (character === undefined) {
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
const code = character.charCodeAt(0);
|
|
374
|
+
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function isTagNameTerminator(character: string | undefined): boolean {
|
|
378
|
+
return (
|
|
379
|
+
character === undefined ||
|
|
380
|
+
character === ">" ||
|
|
381
|
+
character === "/" ||
|
|
382
|
+
isAsciiWhitespace(character)
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function isForbiddenTagNameCharacter(character: string | undefined): boolean {
|
|
387
|
+
return (
|
|
388
|
+
character === "\0" ||
|
|
389
|
+
character === '"' ||
|
|
390
|
+
character === "'" ||
|
|
391
|
+
character === "<" ||
|
|
392
|
+
character === "="
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function isAttributeNameTerminator(character: string | undefined): boolean {
|
|
397
|
+
return (
|
|
398
|
+
character === undefined ||
|
|
399
|
+
character === ">" ||
|
|
400
|
+
character === "/" ||
|
|
401
|
+
character === "=" ||
|
|
402
|
+
isAsciiWhitespace(character)
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function isForbiddenAttributeNameCharacter(character: string | undefined): boolean {
|
|
407
|
+
return character === "\0" || character === '"' || character === "'" || character === "<";
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function isUnquotedValueTerminator(character: string | undefined): boolean {
|
|
411
|
+
return character === undefined || character === ">" || isAsciiWhitespace(character);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function isForbiddenUnquotedValueCharacter(character: string | undefined): boolean {
|
|
415
|
+
return (
|
|
416
|
+
character === "\0" ||
|
|
417
|
+
character === '"' ||
|
|
418
|
+
character === "'" ||
|
|
419
|
+
character === "`" ||
|
|
420
|
+
character === "<" ||
|
|
421
|
+
character === "="
|
|
422
|
+
);
|
|
423
|
+
}
|
package/src/navigation.ts
CHANGED
|
@@ -46,7 +46,7 @@ function containsControlCharacter(value: string): boolean {
|
|
|
46
46
|
// Allowed: path-absolute (`/foo`), query-only (`?x=1`), hash-only (`#x`),
|
|
47
47
|
// relative (`foo`). Rejected: protocol-relative (`//evil`), backslash variants
|
|
48
48
|
// (`/\evil`, `\\evil`), and anything with a scheme like `javascript:`.
|
|
49
|
-
function isSafeInternalRedirect(location: string): boolean {
|
|
49
|
+
export function isSafeInternalRedirect(location: string): boolean {
|
|
50
50
|
if (containsControlCharacter(location)) return false;
|
|
51
51
|
const trimmed = stripLeadingControlOrWhitespace(location);
|
|
52
52
|
if (trimmed === "") return false;
|
package/src/node-server.ts
CHANGED
|
@@ -9,28 +9,43 @@ import {
|
|
|
9
9
|
requestLogFields,
|
|
10
10
|
type AppRouterLogger,
|
|
11
11
|
} from "./logger.js";
|
|
12
|
-
import
|
|
12
|
+
import {
|
|
13
|
+
assertValidHttpUpgradeOriginPolicy,
|
|
14
|
+
closeServerWithUpgrades,
|
|
15
|
+
createManagedHttpUpgradeLifecycle,
|
|
16
|
+
validateHttpUpgradeOrigin,
|
|
17
|
+
type HttpUpgradeOriginPolicy,
|
|
18
|
+
type HttpUpgradeOriginValidation,
|
|
19
|
+
type ManagedHttpUpgradeHandler,
|
|
20
|
+
} from "./upgrade.js";
|
|
13
21
|
|
|
14
22
|
export interface StartNodeRequestServerOptions {
|
|
15
23
|
allowedHosts?: readonly string[] | undefined;
|
|
16
|
-
errorHandler?:
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
errorHandler?:
|
|
25
|
+
| ((error: unknown) => {
|
|
26
|
+
body: string;
|
|
27
|
+
status: number;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
})
|
|
30
|
+
| undefined;
|
|
21
31
|
hostname?: string | undefined;
|
|
22
32
|
hostPolicy?: "strict" | "trusted-proxy" | undefined;
|
|
23
33
|
logger?: AppRouterLogger | undefined;
|
|
24
|
-
onUpgrade?:
|
|
34
|
+
onUpgrade?: ManagedHttpUpgradeHandler | undefined;
|
|
25
35
|
port: number;
|
|
26
36
|
render(request: Request): Promise<Response>;
|
|
27
|
-
resolveHost?:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
resolveHost?:
|
|
38
|
+
| ((options: {
|
|
39
|
+
allowedHosts?: readonly string[] | undefined;
|
|
40
|
+
fallbackHost: string;
|
|
41
|
+
hostPolicy?: "strict" | "trusted-proxy" | undefined;
|
|
42
|
+
rawHost: string | undefined;
|
|
43
|
+
}) => string)
|
|
44
|
+
| undefined;
|
|
33
45
|
trustForwardedProto?: boolean | undefined;
|
|
46
|
+
upgradeCloseTimeoutMs?: number | undefined;
|
|
47
|
+
upgradeDecisionTimeoutMs?: number | undefined;
|
|
48
|
+
upgradeOriginPolicy?: HttpUpgradeOriginPolicy | undefined;
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
export function resolveNodeRequestProtocol(options: {
|
|
@@ -56,6 +71,18 @@ export function resolveNodeRequestProtocol(options: {
|
|
|
56
71
|
export async function startNodeRequestServer(
|
|
57
72
|
options: StartNodeRequestServerOptions,
|
|
58
73
|
): Promise<{ close(): Promise<void>; server: Server; url: string }> {
|
|
74
|
+
const upgradeCloseTimeoutMs = finiteNonNegativeTimeout(
|
|
75
|
+
options.upgradeCloseTimeoutMs,
|
|
76
|
+
1_000,
|
|
77
|
+
"upgradeCloseTimeoutMs",
|
|
78
|
+
);
|
|
79
|
+
const upgradeDecisionTimeoutMs = finiteNonNegativeTimeout(
|
|
80
|
+
options.upgradeDecisionTimeoutMs,
|
|
81
|
+
1_000,
|
|
82
|
+
"upgradeDecisionTimeoutMs",
|
|
83
|
+
);
|
|
84
|
+
assertValidHttpUpgradeOriginPolicy(options.upgradeOriginPolicy);
|
|
85
|
+
let listeningPort = options.port;
|
|
59
86
|
const server = createServer(async (incoming, outgoing) => {
|
|
60
87
|
const startedAt = logNow();
|
|
61
88
|
const fallbackRequestFields = {
|
|
@@ -65,7 +92,7 @@ export async function startNodeRequestServer(
|
|
|
65
92
|
};
|
|
66
93
|
|
|
67
94
|
try {
|
|
68
|
-
const fallbackHost =
|
|
95
|
+
const fallbackHost = formatNodeAuthority(options.hostname ?? "127.0.0.1", options.port);
|
|
69
96
|
const host = (options.resolveHost ?? defaultResolveRequestHost)({
|
|
70
97
|
allowedHosts: options.allowedHosts,
|
|
71
98
|
fallbackHost,
|
|
@@ -77,7 +104,7 @@ export async function startNodeRequestServer(
|
|
|
77
104
|
forwardedProto: incoming.headers["x-forwarded-proto"],
|
|
78
105
|
trustForwardedProto: options.trustForwardedProto,
|
|
79
106
|
});
|
|
80
|
-
const request = nodeRequestToWebRequest(incoming, `${protocol}://${host}
|
|
107
|
+
const request = nodeRequestToWebRequest(incoming, `${protocol}://${host}`, outgoing);
|
|
81
108
|
const logFields = requestLogFields(request, "node");
|
|
82
109
|
emitRouterLog(options.logger, "info", {
|
|
83
110
|
...logFields,
|
|
@@ -120,8 +147,21 @@ export async function startNodeRequestServer(
|
|
|
120
147
|
}
|
|
121
148
|
});
|
|
122
149
|
|
|
123
|
-
|
|
124
|
-
|
|
150
|
+
const upgradeLifecycle =
|
|
151
|
+
options.onUpgrade === undefined
|
|
152
|
+
? undefined
|
|
153
|
+
: createManagedHttpUpgradeLifecycle({
|
|
154
|
+
decisionTimeoutMs: upgradeDecisionTimeoutMs,
|
|
155
|
+
handler: options.onUpgrade,
|
|
156
|
+
isOriginAllowed: (request) =>
|
|
157
|
+
validateNodeUpgradeOrigin(request, {
|
|
158
|
+
...options,
|
|
159
|
+
listeningPort,
|
|
160
|
+
}),
|
|
161
|
+
logger: options.logger,
|
|
162
|
+
});
|
|
163
|
+
if (upgradeLifecycle !== undefined) {
|
|
164
|
+
server.on("upgrade", upgradeLifecycle.handle);
|
|
125
165
|
}
|
|
126
166
|
|
|
127
167
|
await new Promise<void>((resolve) =>
|
|
@@ -129,17 +169,83 @@ export async function startNodeRequestServer(
|
|
|
129
169
|
);
|
|
130
170
|
const address = server.address();
|
|
131
171
|
const port = typeof address === "object" && address !== null ? address.port : options.port;
|
|
172
|
+
listeningPort = port;
|
|
173
|
+
let closePromise: Promise<void> | undefined;
|
|
132
174
|
|
|
133
175
|
return {
|
|
134
176
|
server,
|
|
135
|
-
url: `http://${options.hostname ?? "127.0.0.1"
|
|
136
|
-
close: () =>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
177
|
+
url: `http://${formatNodeAuthority(options.hostname ?? "127.0.0.1", port)}`,
|
|
178
|
+
close: () => {
|
|
179
|
+
closePromise ??=
|
|
180
|
+
upgradeLifecycle === undefined
|
|
181
|
+
? new Promise<void>((resolve, reject) =>
|
|
182
|
+
server.close((error) => (error ? reject(error) : resolve())),
|
|
183
|
+
)
|
|
184
|
+
: closeServerWithUpgrades({
|
|
185
|
+
lifecycle: upgradeLifecycle,
|
|
186
|
+
server,
|
|
187
|
+
timeoutMs: upgradeCloseTimeoutMs,
|
|
188
|
+
});
|
|
189
|
+
return closePromise;
|
|
190
|
+
},
|
|
140
191
|
};
|
|
141
192
|
}
|
|
142
193
|
|
|
194
|
+
function validateNodeUpgradeOrigin(
|
|
195
|
+
request: import("node:http").IncomingMessage,
|
|
196
|
+
options: StartNodeRequestServerOptions & { listeningPort: number },
|
|
197
|
+
): HttpUpgradeOriginValidation {
|
|
198
|
+
const policy = options.upgradeOriginPolicy ?? "same-origin";
|
|
199
|
+
if (policy === "unchecked") {
|
|
200
|
+
return { ok: true, origin: request.headers.origin };
|
|
201
|
+
}
|
|
202
|
+
if (typeof policy === "object") {
|
|
203
|
+
return validateHttpUpgradeOrigin(request, policy);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const fallbackHost = formatNodeAuthority(options.hostname ?? "127.0.0.1", options.listeningPort);
|
|
207
|
+
const host = (options.resolveHost ?? defaultResolveRequestHost)({
|
|
208
|
+
allowedHosts: options.allowedHosts,
|
|
209
|
+
fallbackHost,
|
|
210
|
+
hostPolicy: options.hostPolicy,
|
|
211
|
+
rawHost: request.headers.host,
|
|
212
|
+
});
|
|
213
|
+
const protocol = resolveNodeRequestProtocol({
|
|
214
|
+
encrypted: (request.socket as { encrypted?: boolean }).encrypted === true,
|
|
215
|
+
forwardedProto: request.headers["x-forwarded-proto"],
|
|
216
|
+
trustForwardedProto: options.trustForwardedProto,
|
|
217
|
+
});
|
|
218
|
+
try {
|
|
219
|
+
return validateHttpUpgradeOrigin(request, { allowedOrigins: [`${protocol}://${host}`] });
|
|
220
|
+
} catch {
|
|
221
|
+
return { ok: false, reason: "malformed-origin" };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function formatNodeAuthority(hostname: string, port: number): string {
|
|
226
|
+
const host = hostname.includes(":") && !hostname.startsWith("[") ? `[${hostname}]` : hostname;
|
|
227
|
+
return `${host}:${port}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function finiteNonNegativeTimeout(
|
|
231
|
+
value: number | undefined,
|
|
232
|
+
fallback: number,
|
|
233
|
+
name: string,
|
|
234
|
+
): number {
|
|
235
|
+
const timeout = value ?? fallback;
|
|
236
|
+
if (
|
|
237
|
+
!Number.isFinite(timeout) ||
|
|
238
|
+
timeout < 0 ||
|
|
239
|
+
!Number.isSafeInteger(timeout) ||
|
|
240
|
+
timeout > 2_147_483_647
|
|
241
|
+
) {
|
|
242
|
+
throw new TypeError(
|
|
243
|
+
`${name} must be a finite non-negative safe integer no greater than 2147483647.`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return timeout;
|
|
247
|
+
}
|
|
248
|
+
|
|
143
249
|
function defaultResolveRequestHost(options: {
|
|
144
250
|
allowedHosts?: readonly string[] | undefined;
|
|
145
251
|
fallbackHost: string;
|