Skip to content

Commit 947991a

Browse files
committed
feat: add vite preview support
1 parent 89e2bfe commit 947991a

File tree

4 files changed

+397
-0
lines changed

4 files changed

+397
-0
lines changed

.changeset/stupid-forks-admire.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-router/dev": minor
3+
---
4+
5+
feat: add `vite preview` support

integration/helpers/vite.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,24 @@ export const createDev =
415415
export const dev = createDev([reactRouterBin, "dev"]);
416416
export const customDev = createDev(["./server.mjs"]);
417417

418+
export const vitePreview = async ({
419+
cwd,
420+
port,
421+
}: {
422+
cwd: string;
423+
port: number;
424+
}) => {
425+
let nodeBin = process.argv[0];
426+
let viteBin = path.join(cwd, "node_modules", "vite", "bin", "vite.js");
427+
let proc = spawn(nodeBin, [viteBin, "preview", "--port", String(port), "--strict-port"], {
428+
cwd,
429+
stdio: "pipe",
430+
env: { NODE_ENV: "production" },
431+
});
432+
await waitForServer(proc, { port });
433+
return () => proc.kill();
434+
};
435+
418436
// Used for testing errors thrown on build when we don't want to start and
419437
// wait for the server
420438
export const viteDevCmd = ({ cwd }: { cwd: string }) => {
@@ -452,6 +470,13 @@ type Fixtures = {
452470
port: number;
453471
cwd: string;
454472
}>;
473+
vitePreview: (
474+
files: Files,
475+
templateName?: TemplateName,
476+
) => Promise<{
477+
port: number;
478+
cwd: string;
479+
}>;
455480
wranglerPagesDev: (files: Files) => Promise<{
456481
port: number;
457482
cwd: string;
@@ -499,6 +524,18 @@ export const test = base.extend<Fixtures>({
499524
});
500525
stop?.();
501526
},
527+
vitePreview: async (_, use) => {
528+
let stop: (() => unknown) | undefined;
529+
await use(async (files, template) => {
530+
let port = await getPort();
531+
let cwd = await createProject(await files({ port }), template);
532+
let { status } = build({ cwd });
533+
expect(status).toBe(0);
534+
stop = await vitePreview({ cwd, port });
535+
return { port, cwd };
536+
});
537+
stop?.();
538+
},
502539
// eslint-disable-next-line no-empty-pattern
503540
wranglerPagesDev: async ({}, use) => {
504541
let stop: (() => unknown) | undefined;

integration/vite-preview-test.ts

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
import { expect } from "@playwright/test";
2+
import dedent from "dedent";
3+
4+
import {
5+
reactRouterConfig,
6+
viteConfig,
7+
test,
8+
type Files,
9+
} from "./helpers/vite.js";
10+
11+
const tsx = dedent;
12+
13+
test.describe("Vite preview", () => {
14+
test("serves built app with vite preview", async ({ vitePreview, page }) => {
15+
const files: Files = async ({ port }) => ({
16+
"react-router.config.ts": reactRouterConfig({
17+
viteEnvironmentApi: true,
18+
}),
19+
"vite.config.ts": await viteConfig.basic({
20+
port,
21+
templateName: "vite-6-template",
22+
}),
23+
"app/root.tsx": tsx`
24+
import { Links, Meta, Outlet, Scripts } from "react-router";
25+
26+
export default function Root() {
27+
return (
28+
<html lang="en">
29+
<head>
30+
<Meta />
31+
<Links />
32+
</head>
33+
<body>
34+
<div id="content">
35+
<h1>Root</h1>
36+
<Outlet />
37+
</div>
38+
<Scripts />
39+
</body>
40+
</html>
41+
);
42+
}
43+
`,
44+
"app/routes/_index.tsx": tsx`
45+
export default function IndexRoute() {
46+
return (
47+
<div id="index">
48+
<h2 data-title>Index</h2>
49+
<p data-env>Environment: production</p>
50+
</div>
51+
);
52+
}
53+
`,
54+
"app/routes/about.tsx": tsx`
55+
export default function AboutRoute() {
56+
return (
57+
<div id="about">
58+
<h2 data-title>About</h2>
59+
<p>This is the about page</p>
60+
</div>
61+
);
62+
}
63+
`,
64+
"app/routes/loader-data.tsx": tsx`
65+
import { useLoaderData } from "react-router";
66+
67+
export function loader() {
68+
return { message: "Hello from loader" };
69+
}
70+
71+
export default function LoaderDataRoute() {
72+
const { message } = useLoaderData<typeof loader>();
73+
return (
74+
<div id="loader-data">
75+
<h2 data-title>Loader Data</h2>
76+
<p data-message>{message}</p>
77+
</div>
78+
);
79+
}
80+
`,
81+
});
82+
83+
const { port } = await vitePreview(files, "vite-6-template");
84+
await page.goto(`http://localhost:${port}/`, {
85+
waitUntil: "networkidle",
86+
});
87+
88+
// Ensure no errors on page load
89+
expect(page.errors).toEqual([]);
90+
91+
await expect(page.locator("#index [data-title]")).toHaveText("Index");
92+
await expect(page.locator("#index [data-env]")).toHaveText(
93+
"Environment: production",
94+
);
95+
});
96+
97+
test("handles navigation between routes", async ({ vitePreview, page }) => {
98+
const files: Files = async ({ port }) => ({
99+
"react-router.config.ts": reactRouterConfig({
100+
viteEnvironmentApi: true,
101+
}),
102+
"vite.config.ts": await viteConfig.basic({
103+
port,
104+
templateName: "vite-6-template",
105+
}),
106+
"app/root.tsx": tsx`
107+
import { Links, Meta, Outlet, Scripts, Link } from "react-router";
108+
109+
export default function Root() {
110+
return (
111+
<html lang="en">
112+
<head>
113+
<Meta />
114+
<Links />
115+
</head>
116+
<body>
117+
<div id="content">
118+
<nav>
119+
<Link to="/" data-link-home>Home</Link>
120+
<Link to="/about" data-link-about>About</Link>
121+
</nav>
122+
<Outlet />
123+
</div>
124+
<Scripts />
125+
</body>
126+
</html>
127+
);
128+
}
129+
`,
130+
"app/routes/_index.tsx": tsx`
131+
export default function IndexRoute() {
132+
return (
133+
<div id="index">
134+
<h2 data-title>Index</h2>
135+
</div>
136+
);
137+
}
138+
`,
139+
"app/routes/about.tsx": tsx`
140+
export default function AboutRoute() {
141+
return (
142+
<div id="about">
143+
<h2 data-title>About</h2>
144+
</div>
145+
);
146+
}
147+
`,
148+
});
149+
150+
const { port } = await vitePreview(files, "vite-6-template");
151+
await page.goto(`http://localhost:${port}/`, {
152+
waitUntil: "networkidle",
153+
});
154+
155+
expect(page.errors).toEqual([]);
156+
await expect(page.locator("#index [data-title]")).toHaveText("Index");
157+
158+
// Navigate to about page
159+
await page.click("[data-link-about]");
160+
await page.waitForLoadState("networkidle");
161+
162+
expect(page.errors).toEqual([]);
163+
await expect(page.locator("#about [data-title]")).toHaveText("About");
164+
165+
// Navigate back to home
166+
await page.click("[data-link-home]");
167+
await page.waitForLoadState("networkidle");
168+
169+
expect(page.errors).toEqual([]);
170+
await expect(page.locator("#index [data-title]")).toHaveText("Index");
171+
});
172+
173+
test("handles loader data correctly", async ({ vitePreview, page }) => {
174+
const files: Files = async ({ port }) => ({
175+
"react-router.config.ts": reactRouterConfig({
176+
viteEnvironmentApi: true,
177+
}),
178+
"vite.config.ts": await viteConfig.basic({
179+
port,
180+
templateName: "vite-6-template",
181+
}),
182+
"app/root.tsx": tsx`
183+
import { Links, Meta, Outlet, Scripts } from "react-router";
184+
185+
export default function Root() {
186+
return (
187+
<html lang="en">
188+
<head>
189+
<Meta />
190+
<Links />
191+
</head>
192+
<body>
193+
<div id="content">
194+
<Outlet />
195+
</div>
196+
<Scripts />
197+
</body>
198+
</html>
199+
);
200+
}
201+
`,
202+
"app/routes/_index.tsx": tsx`
203+
import { useLoaderData } from "react-router";
204+
205+
export function loader() {
206+
return {
207+
message: "Hello from loader",
208+
timestamp: Date.now()
209+
};
210+
}
211+
212+
export default function IndexRoute() {
213+
const { message, timestamp } = useLoaderData<typeof loader>();
214+
return (
215+
<div id="index">
216+
<h2 data-title>Index</h2>
217+
<p data-message>{message}</p>
218+
<p data-timestamp>{timestamp}</p>
219+
</div>
220+
);
221+
}
222+
`,
223+
});
224+
225+
const { port } = await vitePreview(files, "vite-6-template");
226+
await page.goto(`http://localhost:${port}/`, {
227+
waitUntil: "networkidle",
228+
});
229+
230+
expect(page.errors).toEqual([]);
231+
await expect(page.locator("#index [data-title]")).toHaveText("Index");
232+
await expect(page.locator("#index [data-message]")).toHaveText(
233+
"Hello from loader",
234+
);
235+
236+
// Check that timestamp exists and is a number
237+
const timestampText = await page
238+
.locator("#index [data-timestamp]")
239+
.textContent();
240+
expect(timestampText).toBeTruthy();
241+
expect(Number(timestampText)).toBeGreaterThan(0);
242+
});
243+
244+
test("handles direct navigation to dynamic routes", async ({
245+
vitePreview,
246+
page,
247+
}) => {
248+
const files: Files = async ({ port }) => ({
249+
"react-router.config.ts": reactRouterConfig({
250+
viteEnvironmentApi: true,
251+
}),
252+
"vite.config.ts": await viteConfig.basic({
253+
port,
254+
templateName: "vite-6-template",
255+
}),
256+
"app/root.tsx": tsx`
257+
import { Links, Meta, Outlet, Scripts } from "react-router";
258+
259+
export default function Root() {
260+
return (
261+
<html lang="en">
262+
<head>
263+
<Meta />
264+
<Links />
265+
</head>
266+
<body>
267+
<div id="content">
268+
<Outlet />
269+
</div>
270+
<Scripts />
271+
</body>
272+
</html>
273+
);
274+
}
275+
`,
276+
"app/routes/_index.tsx": tsx`
277+
export default function IndexRoute() {
278+
return <div id="index"><h2>Index</h2></div>;
279+
}
280+
`,
281+
"app/routes/products.$id.tsx": tsx`
282+
import { useLoaderData, useParams } from "react-router";
283+
284+
export function loader({ params }: { params: { id: string } }) {
285+
return {
286+
productId: params.id,
287+
};
288+
}
289+
290+
export default function ProductRoute() {
291+
const { productId } = useLoaderData<typeof loader>();
292+
return (
293+
<div id="product">
294+
<h2 data-title>Product Details</h2>
295+
<p data-id>{productId}</p>
296+
<p data-name>Product {productId}</p>
297+
</div>
298+
);
299+
}
300+
`,
301+
});
302+
303+
const { port } = await vitePreview(files, "vite-6-template");
304+
await page.goto(`http://localhost:${port}/products/123`, {
305+
waitUntil: "networkidle",
306+
});
307+
308+
expect(page.errors).toEqual([]);
309+
await expect(page.locator("#product [data-title]")).toHaveText(
310+
"Product Details",
311+
);
312+
await expect(page.locator("#product [data-id]")).toHaveText("123");
313+
await expect(page.locator("#product [data-name]")).toHaveText(
314+
"Product 123",
315+
);
316+
});
317+
});

0 commit comments

Comments
 (0)