From e23a37c667c87581fdf419f6d9e94bb3d66a5335 Mon Sep 17 00:00:00 2001
From: Reda Bacha <47112551+redabacha@users.noreply.github.com>
Date: Sun, 31 Jul 2022 04:27:32 +0100
Subject: [PATCH 1/2] perf(remix-server-runtime): use faster alternative to
jsesc
---
.changeset/four-numbers-end.md | 5 ++
contributors.yml | 1 +
packages/remix-dev/package.json | 1 +
.../__tests__/markup-test.ts | 56 +++++++++++++++++++
packages/remix-server-runtime/markup.ts | 16 ++++++
packages/remix-server-runtime/package.json | 2 -
.../remix-server-runtime/serverHandoff.ts | 8 +--
yarn.lock | 8 +--
8 files changed, 87 insertions(+), 10 deletions(-)
create mode 100644 .changeset/four-numbers-end.md
create mode 100644 packages/remix-server-runtime/__tests__/markup-test.ts
create mode 100644 packages/remix-server-runtime/markup.ts
diff --git a/.changeset/four-numbers-end.md b/.changeset/four-numbers-end.md
new file mode 100644
index 00000000000..58d56d27bd7
--- /dev/null
+++ b/.changeset/four-numbers-end.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/server-runtime": patch
+---
+
+Improve performance when serializing data in the server runtime.
diff --git a/contributors.yml b/contributors.yml
index 5ca6dce6c87..e8505a4caea 100644
--- a/contributors.yml
+++ b/contributors.yml
@@ -322,6 +322,7 @@
- raulrpearson
- real34
- realjokele
+- redabacha
- reggie3
- rlfarman
- roachjc
diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json
index 34e194a1b1b..e32de32761b 100644
--- a/packages/remix-dev/package.json
+++ b/packages/remix-dev/package.json
@@ -64,6 +64,7 @@
"@types/gunzip-maybe": "^1.4.0",
"@types/inquirer": "^8.2.0",
"@types/jscodeshift": "^0.11.3",
+ "@types/jsesc": "^3.0.1",
"@types/lodash.debounce": "^4.0.6",
"@types/npmcli__package-json": "^2.0.0",
"@types/shelljs": "^0.8.11",
diff --git a/packages/remix-server-runtime/__tests__/markup-test.ts b/packages/remix-server-runtime/__tests__/markup-test.ts
new file mode 100644
index 00000000000..0457d902f2a
--- /dev/null
+++ b/packages/remix-server-runtime/__tests__/markup-test.ts
@@ -0,0 +1,56 @@
+import vm from "vm";
+
+import { escapeHtml } from "../markup";
+
+describe("escapeHtml", () => {
+ // These tests are based on https://github.com/zertosh/htmlescape/blob/3e6cf0614dd0f778fd0131e69070b77282150c15/test/htmlescape-test.js
+ // License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
+
+ test("with angle brackets should escape", () => {
+ let evilObj = { evil: "" };
+ expect(escapeHtml(JSON.stringify(evilObj))).toBe(
+ '{"evil":"\\u003cscript\\u003e\\u003c/script\\u003e"}'
+ );
+ });
+
+ test("with angle brackets should parse back", () => {
+ let evilObj = { evil: "" };
+ expect(JSON.parse(escapeHtml(JSON.stringify(evilObj)))).toMatchObject(
+ evilObj
+ );
+ });
+
+ test("with ampersands should escape", () => {
+ let evilObj = { evil: "&" };
+ expect(escapeHtml(JSON.stringify(evilObj))).toBe('{"evil":"\\u0026"}');
+ });
+
+ test("with ampersands should parse back", () => {
+ let evilObj = { evil: "&" };
+ expect(JSON.parse(escapeHtml(JSON.stringify(evilObj)))).toMatchObject(
+ evilObj
+ );
+ });
+
+ test('with "LINE SEPARATOR" and "PARAGRAPH SEPARATOR" should escape', () => {
+ let evilObj = { evil: "\u2028\u2029" };
+ expect(escapeHtml(JSON.stringify(evilObj))).toBe(
+ '{"evil":"\\u2028\\u2029"}'
+ );
+ });
+
+ test('with "LINE SEPARATOR" and "PARAGRAPH SEPARATOR" should parse back', () => {
+ let evilObj = { evil: "\u2028\u2029" };
+ expect(JSON.parse(escapeHtml(JSON.stringify(evilObj)))).toMatchObject(
+ evilObj
+ );
+ });
+
+ test("escaped line terminators should work", () => {
+ expect(() => {
+ vm.runInNewContext(
+ "(" + escapeHtml(JSON.stringify({ evil: "\u2028\u2029" })) + ")"
+ );
+ }).not.toThrow();
+ });
+});
diff --git a/packages/remix-server-runtime/markup.ts b/packages/remix-server-runtime/markup.ts
new file mode 100644
index 00000000000..82095267672
--- /dev/null
+++ b/packages/remix-server-runtime/markup.ts
@@ -0,0 +1,16 @@
+// This escapeHtml utility is based on https://github.com/zertosh/htmlescape
+// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
+
+const ESCAPE_LOOKUP: { [match: string]: string } = {
+ "&": "\\u0026",
+ ">": "\\u003e",
+ "<": "\\u003c",
+ "\u2028": "\\u2028",
+ "\u2029": "\\u2029",
+};
+
+const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
+
+export function escapeHtml(html: string) {
+ return html.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
+}
diff --git a/packages/remix-server-runtime/package.json b/packages/remix-server-runtime/package.json
index 840b9e0c44f..bc48daaf1de 100644
--- a/packages/remix-server-runtime/package.json
+++ b/packages/remix-server-runtime/package.json
@@ -19,14 +19,12 @@
"@types/cookie": "^0.4.0",
"@web3-storage/multipart-parser": "^1.0.0",
"cookie": "^0.4.1",
- "jsesc": "3.0.2",
"react-router-dom": "^6.2.2",
"set-cookie-parser": "^2.4.8",
"source-map": "^0.7.3"
},
"devDependencies": {
"@remix-run/web-file": "^3.0.2",
- "@types/jsesc": "^2.5.1",
"@types/set-cookie-parser": "^2.4.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
diff --git a/packages/remix-server-runtime/serverHandoff.ts b/packages/remix-server-runtime/serverHandoff.ts
index 6f20a8d1458..aee4cfbf0db 100644
--- a/packages/remix-server-runtime/serverHandoff.ts
+++ b/packages/remix-server-runtime/serverHandoff.ts
@@ -1,7 +1,7 @@
-import jsesc from "jsesc";
+import { escapeHtml } from "./markup";
export function createServerHandoffString(serverHandoff: any): string {
- // Use jsesc to escape data returned from the loaders. This string is
- // inserted directly into the HTML in the `` element.
- return jsesc(serverHandoff, { isScriptContext: true });
+ // Uses faster alternative of jsesc to escape data returned from the loaders.
+ // This string is inserted directly into the HTML in the `` element.
+ return escapeHtml(JSON.stringify(serverHandoff));
}
diff --git a/yarn.lock b/yarn.lock
index af120d1b234..243ed822308 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2792,10 +2792,10 @@
ast-types "^0.14.1"
recast "^0.20.3"
-"@types/jsesc@^2.5.1":
- version "2.5.1"
- resolved "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz"
- integrity sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==
+"@types/jsesc@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@types/jsesc/-/jsesc-3.0.1.tgz#ed1720ae08eae2f64341452e1693a84324029d99"
+ integrity sha512-F2g93pJlhV0RlW9uSUAM/hIxywlwlZcuRB/nZ82GaMPaO8mdexYbJ8Qt3UGbUS1M19YFQykEetrWW004M+vPCg==
"@types/json-schema@*", "@types/json-schema@^7.0.9":
version "7.0.9"
From 3cc17531ecef87333e3b0611951796b28df538b3 Mon Sep 17 00:00:00 2001
From: Reda Bacha <47112551+redabacha@users.noreply.github.com>
Date: Mon, 8 Aug 2022 23:51:37 +0100
Subject: [PATCH 2/2] chore: add comment to explain why htmlescape package is
inlined
---
packages/remix-server-runtime/markup.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/packages/remix-server-runtime/markup.ts b/packages/remix-server-runtime/markup.ts
index 82095267672..4ab1fdcc784 100644
--- a/packages/remix-server-runtime/markup.ts
+++ b/packages/remix-server-runtime/markup.ts
@@ -1,6 +1,9 @@
// This escapeHtml utility is based on https://github.com/zertosh/htmlescape
// License: https://github.com/zertosh/htmlescape/blob/0527ca7156a524d256101bb310a9f970f63078ad/LICENSE
+// We've chosen to inline the utility here to reduce the number of npm dependencies we have,
+// slightly decrease the code size compared the original package and make it esm compatible.
+
const ESCAPE_LOOKUP: { [match: string]: string } = {
"&": "\\u0026",
">": "\\u003e",