diff --git a/src/System/Router/Utils/__tests__/renderToStream.jest.tsx b/src/System/Router/Utils/__tests__/renderToStream.jest.tsx
new file mode 100644
index 00000000000..c9e69618f5d
--- /dev/null
+++ b/src/System/Router/Utils/__tests__/renderToStream.jest.tsx
@@ -0,0 +1,127 @@
+import { flushPromiseQueue } from "DevTools/flushPromiseQueue"
+import { renderToPipeableStream } from "react-dom/server"
+import { ArtsyResponse } from "Server/middleware/artsyExpress"
+import { ServerStyleSheet } from "styled-components"
+import { renderToStream } from "System/Router/Utils/renderToStream"
+
+jest.mock("react-dom/server", () => ({
+ renderToPipeableStream: jest.fn(),
+}))
+
+describe("renderToStream", () => {
+ const mockRenderToPipeableStream = renderToPipeableStream as jest.Mock
+
+ const res = ({
+ statusCode: 0,
+ setHeader: jest.fn(),
+ } as unknown) as ArtsyResponse
+
+ const sheet = ({
+ _emitSheetCSS: jest.fn(() => "mock-css"),
+ instance: {
+ clearTag: jest.fn(),
+ },
+ } as unknown) as ServerStyleSheet
+
+ afterEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it("should set the response status and content type on shell ready", async () => {
+ const mockPipe = jest.fn()
+ const mockAbort = jest.fn()
+
+ mockRenderToPipeableStream.mockImplementation((_, options) => {
+ setTimeout(() => {
+ options.onShellReady()
+ })
+
+ return { pipe: mockPipe, abort: mockAbort }
+ })
+
+ const jsx =
Hello World
+ renderToStream({ jsx, sheet, res })
+
+ await flushPromiseQueue()
+
+ expect(res.statusCode).toBe(200)
+ expect(res.setHeader).toHaveBeenCalledWith(
+ "Content-Type",
+ "text/html; charset=utf-8"
+ )
+ expect(mockPipe).toHaveBeenCalled()
+ })
+
+ it("should handle onError and set didError to true", async () => {
+ const mockPipe = jest.fn()
+ const mockAbort = jest.fn()
+
+ mockRenderToPipeableStream.mockImplementation((_, options) => {
+ options.onError(new Error("Test error"))
+
+ setTimeout(() => {
+ options.onShellReady()
+ })
+
+ return { pipe: mockPipe, abort: mockAbort }
+ })
+
+ const jsx = Hello Error
+ renderToStream({ jsx, sheet, res })
+
+ await flushPromiseQueue()
+
+ expect(res.statusCode).toBe(500)
+ expect(mockPipe).toHaveBeenCalled()
+ })
+
+ it("should call abort if STREAM_TIMEOUT is reached", () => {
+ jest.useFakeTimers()
+ const mockAbort = jest.fn()
+
+ mockRenderToPipeableStream.mockImplementation((_, options) => {
+ return { pipe: jest.fn(), abort: mockAbort }
+ })
+
+ const jsx = Timeout Test
+ renderToStream({ jsx, sheet, res })
+
+ jest.advanceTimersByTime(5000)
+
+ expect(mockAbort).toHaveBeenCalled()
+ jest.useRealTimers()
+ })
+
+ // eslint-disable-next-line jest/no-done-callback
+ it("should transform the stream and inject CSS into the HTML", done => {
+ const mockPipe = jest.fn()
+ const mockAbort = jest.fn()
+
+ mockRenderToPipeableStream.mockImplementation((_, options) => {
+ setTimeout(() => {
+ options.onShellReady()
+ })
+ return { pipe: mockPipe, abort: mockAbort }
+ })
+
+ const jsx = Stream Test
+ const stream = renderToStream({ jsx, sheet, res })
+
+ stream.write("")
+
+ const chunks: string[] = []
+ stream.on("data", chunk => {
+ chunks.push(chunk)
+ })
+
+ stream.on("end", () => {
+ const result = chunks.join("")
+ expect(result).toContain(
+ "mock-css"
+ )
+ done()
+ })
+
+ stream.end()
+ })
+})
diff --git a/src/System/Router/Utils/collectAssets.tsx b/src/System/Router/Utils/collectAssets.tsx
index 54ff430f234..4c489e85290 100644
--- a/src/System/Router/Utils/collectAssets.tsx
+++ b/src/System/Router/Utils/collectAssets.tsx
@@ -9,6 +9,7 @@ import { ENABLE_SSR_STREAMING } from "Server/config"
import { renderToStream } from "System/Router/Utils/renderToStream"
import { ArtsyResponse } from "Server/middleware/artsyExpress"
import { serializeRelayHydrationData } from "System/Router/Utils/serializeRelayHydrationData"
+import { Transform } from "stream"
const STATS = "loadable-stats.json"
@@ -59,7 +60,7 @@ export const collectAssets = async ({
let styleTags
if (ENABLE_SSR_STREAMING) {
- stream = renderToStream(jsx, sheet, res)
+ stream = renderToStream({ jsx, sheet, res })
} else {
html = renderToString(jsx)
styleTags = sheet.getStyleTags()
@@ -79,10 +80,6 @@ export const collectAssets = async ({
bundleScriptTags
.split("\n")
.map(script => {
- /**
- * In production, prefix injected script src with CDN endpoint.
- * @see https://github.com/artsy/force/blob/main/src/lib/middleware/asset.ts#L23
- */
if (getENV("CDN_URL")) {
const scriptTagWithCDN = script.replace(
/src="\/assets/g,
diff --git a/src/System/Router/Utils/renderToStream.tsx b/src/System/Router/Utils/renderToStream.tsx
index 680ca79f26c..7337adb5daf 100644
--- a/src/System/Router/Utils/renderToStream.tsx
+++ b/src/System/Router/Utils/renderToStream.tsx
@@ -1,10 +1,22 @@
+import { ReactNode } from "react"
import { renderToPipeableStream } from "react-dom/server"
import { ArtsyResponse } from "Server/middleware/artsyExpress"
import { Transform } from "stream"
+import { ServerStyleSheet } from "styled-components"
const STREAM_TIMEOUT = 5000
-export function renderToStream(jsx, sheet, res: ArtsyResponse) {
+interface RenderToStreamProps {
+ jsx: ReactNode
+ sheet: ServerStyleSheet
+ res: ArtsyResponse
+}
+
+export const renderToStream = ({
+ jsx,
+ sheet,
+ res,
+}: RenderToStreamProps): Transform => {
let didError = false
const decoder = new TextDecoder("utf-8")
@@ -49,11 +61,11 @@ export function renderToStream(jsx, sheet, res: ArtsyResponse) {
const { pipe, abort } = renderToPipeableStream(jsx, {
onError: error => {
didError = true
- console.error("error", error)
+ console.error("[renderToStream] onError:", error)
},
onShellError: error => {
didError = true
- console.log("shell error", error)
+ console.error("[renderToStream] onShellError:", error)
},
onShellReady: () => {
res.statusCode = didError ? 500 : 200
diff --git a/src/server.ts b/src/server.ts
index 4710d4007ec..2c7bbde70d4 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -24,9 +24,7 @@ initializeMiddleware(app)
const { routes, routePaths } = getRoutes()
-/**
- * Mount routes that will connect to global SSR router
- */
+// React app routes
app.get(
routePaths,
async (req: ArtsyRequest, res: ArtsyResponse, next: NextFunction) => {
@@ -51,10 +49,7 @@ app.get(
}
)
-/**
- * Mount server-side Express routes
- */
-
+// Common express routes
app
.use(appPreferencesServerRoutes)
.use(cookieConsentManagerServerRoutes)