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",