Skip to content

Commit 1a73718

Browse files
committed
Merge branch 'dev' into brophdawg11/remove-invariant
2 parents d1bf58a + 5706fbd commit 1a73718

File tree

9 files changed

+77
-5
lines changed

9 files changed

+77
-5
lines changed

.changeset/long-jokes-wait.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": patch
3+
---
4+
5+
Fix useBlocker to return IDLE_BLOCKER during SSR

.changeset/nasty-carrots-breathe.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@remix-run/router": minor
3+
---
4+
5+
Remix document response now respects headers and status code set in defer()

.changeset/witty-melons-sip.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router-dom": patch
3+
---
4+
5+
Properly escape HTML characters in StaticRouterProvider serialized hydration data

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,4 @@
178178
- xavier-lc
179179
- xcsnowcity
180180
- yuleicul
181+
- zheng-chuang

packages/react-router-dom/__tests__/data-static-router-test.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,37 @@ describe("A <StaticRouterProvider>", () => {
269269
);
270270
});
271271

272+
it("escapes HTML tags in serialized hydration data", async () => {
273+
let routes = [
274+
{
275+
path: "/",
276+
loader: () => ({
277+
key: "uh </script> oh",
278+
}),
279+
element: <h1>👋</h1>,
280+
},
281+
];
282+
let { query } = createStaticHandler(routes);
283+
284+
let context = (await query(
285+
new Request("http://localhost/", {
286+
signal: new AbortController().signal,
287+
})
288+
)) as StaticHandlerContext;
289+
290+
let html = ReactDOMServer.renderToStaticMarkup(
291+
<React.StrictMode>
292+
<StaticRouterProvider
293+
router={createStaticRouter(routes, context)}
294+
context={context}
295+
/>
296+
</React.StrictMode>
297+
);
298+
expect(html).toMatchInlineSnapshot(
299+
`"<h1>👋</h1><script>window.__staticRouterHydrationData = JSON.parse("{\\"loaderData\\":{\\"0\\":{\\"key\\":\\"uh \\u003c/script\\u003e oh\\"}},\\"actionData\\":null,\\"errors\\":null}");</script>"`
300+
);
301+
});
302+
272303
it("serializes ErrorResponse instances", async () => {
273304
let routes = [
274305
{

packages/react-router-dom/server.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
StaticHandlerContext,
88
} from "@remix-run/router";
99
import {
10+
IDLE_BLOCKER,
1011
IDLE_FETCHER,
1112
IDLE_NAVIGATION,
1213
Action,
@@ -113,7 +114,7 @@ export function StaticRouterProvider({
113114
// up parsing on the client. Dual-stringify is needed to ensure all quotes
114115
// are properly escaped in the resulting string. See:
115116
// https://v8.dev/blog/cost-of-javascript-2019#json
116-
let json = JSON.stringify(JSON.stringify(data));
117+
let json = htmlEscape(JSON.stringify(JSON.stringify(data)));
117118
hydrateScript = `window.__staticRouterHydrationData = JSON.parse(${json});`;
118119
}
119120

@@ -299,7 +300,7 @@ export function createStaticRouter(
299300
throw msg("dispose");
300301
},
301302
getBlocker() {
302-
throw msg("getBlocker");
303+
return IDLE_BLOCKER;
303304
},
304305
deleteBlocker() {
305306
throw msg("deleteBlocker");
@@ -322,3 +323,19 @@ function encodeLocation(to: To): Path {
322323
hash: path.hash || "",
323324
};
324325
}
326+
327+
// This utility is based on https://github.com/zertosh/htmlescape
328+
// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
329+
const ESCAPE_LOOKUP: { [match: string]: string } = {
330+
"&": "\\u0026",
331+
">": "\\u003e",
332+
"<": "\\u003c",
333+
"\u2028": "\\u2028",
334+
"\u2029": "\\u2029",
335+
};
336+
337+
const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
338+
339+
function htmlEscape(str: string): string {
340+
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
341+
}

packages/router/__tests__/router-test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12350,6 +12350,10 @@ describe("a router", () => {
1235012350
"x-custom": "yes",
1235112351
}),
1235212352
},
12353+
statusCode: 201,
12354+
loaderHeaders: {
12355+
deferred: new Headers({ "x-custom": "yes" }),
12356+
},
1235312357
});
1235412358
});
1235512359

packages/router/router.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3183,7 +3183,12 @@ async function callLoaderOrAction(
31833183
}
31843184

31853185
if (result instanceof DeferredData) {
3186-
return { type: ResultType.deferred, deferredData: result };
3186+
return {
3187+
type: ResultType.deferred,
3188+
deferredData: result,
3189+
statusCode: result.init?.status,
3190+
headers: result.init?.headers && new Headers(result.init.headers),
3191+
};
31873192
}
31883193

31893194
return { type: ResultType.data, data: result };

rollup.utils.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ function getBuildDirectories(packageName, folderName) {
3030

3131
if (process.env.LOCAL_BUILD_DIRECTORY) {
3232
try {
33-
let nodeModulesDir = path.join(
34-
process.cwd(),
33+
let nodeModulesDir = path.resolve(
3534
process.env.LOCAL_BUILD_DIRECTORY,
3635
"node_modules"
3736
);

0 commit comments

Comments
 (0)