Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(remix-dev/vite): use global Request class in Vite dev server handler #8062

Merged
5 changes: 5 additions & 0 deletions .changeset/witty-gifts-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/dev": patch
---

Fix `request instanceof Request` checks when using Vite dev server
177 changes: 177 additions & 0 deletions integration/vite-dev-custom-entry-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { test, expect } from "@playwright/test";
import type { Readable } from "node:stream";
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import resolveBin from "resolve-bin";
import getPort from "get-port";
import waitOn from "wait-on";

import { createFixtureProject, js } from "./helpers/create-fixture.js";
import { killtree } from "./helpers/killtree.js";

test.describe("Vite custom entry dev", () => {
let projectDir: string;
let devProc: ChildProcessWithoutNullStreams;
let devPort: number;

test.beforeAll(async () => {
devPort = await getPort();
projectDir = await createFixtureProject({
compiler: "vite",
files: {
"remix.config.js": js`
throw new Error("Remix should not access remix.config.js when using Vite");
export default {};
`,
"vite.config.ts": js`
import { defineConfig } from "vite";
import { unstable_vitePlugin as remix } from "@remix-run/dev";

export default defineConfig({
server: {
port: ${devPort},
strictPort: true,
},
plugins: [
remix(),
],
});
`,
"app/entry.server.tsx": js`
import { PassThrough } from "node:stream";

import type { EntryContext } from "@remix-run/node";
import { createReadableStreamFromReadable } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { renderToPipeableStream } from "react-dom/server";

const ABORT_DELAY = 5_000;

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
let shellRendered = false;
const { pipe, abort } = renderToPipeableStream(
<RemixServer
context={remixContext}
url={request.url}
abortDelay={ABORT_DELAY}
/>,
{
onShellReady() {
shellRendered = true;
const body = new PassThrough();
const stream = createReadableStreamFromReadable(body);

responseHeaders.set("Content-Type", "text/html");

// Used to test that the request object is an instance of the global Request constructor
responseHeaders.set("x-test-request-instanceof-request", String(request instanceof Request));

resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
);

pipe(body);
},
onShellError(error: unknown) {
reject(error);
},
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
}
);

setTimeout(abort, ABORT_DELAY);
});
}
`,
"app/root.tsx": js`
import { Links, Meta, Outlet, Scripts, LiveReload } from "@remix-run/react";

export default function Root() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<div id="content">
<h1>Root</h1>
<Outlet />
</div>
<Scripts />
<LiveReload />
</body>
</html>
);
}
`,
"app/routes/_index.tsx": js`
export default function IndexRoute() {
return <div>IndexRoute</div>
}
`,
},
});

let nodeBin = process.argv[0];
let viteBin = resolveBin.sync("vite");
devProc = spawn(nodeBin, [viteBin, "dev"], {
cwd: projectDir,
env: process.env,
stdio: "pipe",
});
let devStdout = bufferize(devProc.stdout);
let devStderr = bufferize(devProc.stderr);

await waitOn({
resources: [`http://localhost:${devPort}/`],
timeout: 10000,
}).catch((err) => {
let stdout = devStdout();
let stderr = devStderr();
throw new Error(
[
err.message,
"",
"exit code: " + devProc.exitCode,
"stdout: " + stdout ? `\n${stdout}\n` : "<empty>",
"stderr: " + stderr ? `\n${stderr}\n` : "<empty>",
].join("\n")
);
});
});

test.afterAll(async () => {
devProc.pid && (await killtree(devProc.pid));
});

// Ensure libraries/consumers can perform an instanceof check on the request
test("request instanceof Request", async ({ request }) => {
let res = await request.get(`http://localhost:${devPort}/`);
expect(res.headers()).toMatchObject({
"x-test-request-instanceof-request": "true",
});
});
});

let bufferize = (stream: Readable): (() => string) => {
let buffer = "";
stream.on("data", (data) => (buffer += data.toString()));
return () => buffer;
};
2 changes: 0 additions & 2 deletions packages/remix-dev/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
"minimatch": "^9.0.0",
"node-fetch": "^2.6.9",
"ora": "^5.4.1",
"parse-multipart-data": "^1.5.0",
"picocolors": "^1.0.0",
"picomatch": "^2.3.1",
"pidtree": "^0.6.0",
Expand All @@ -72,7 +71,6 @@
"set-cookie-parser": "^2.6.0",
"tar-fs": "^2.1.1",
"tsconfig-paths": "^4.0.0",
"undici": "^5.22.1",
"ws": "^7.4.5"
},
"devDependencies": {
Expand Down
Loading