Skip to content

Commit 9501999

Browse files
Add initial internal RSC Framework Mode plugin (#14061)
1 parent b481c0e commit 9501999

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1881
-502
lines changed

.eslintrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@
1919
"jest/globals": false
2020
}
2121
},
22+
{
23+
"files": ["packages/react-router-dev/config/default-rsc-entries/*"],
24+
"rules": {
25+
"@typescript-eslint/consistent-type-imports": "off"
26+
}
27+
},
2228
{
2329
// Only apply JSDoc lint rules to files we auto-generate docs for
2430
"files": [

integration/action-test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import {
77
} from "./helpers/create-fixture.js";
88
import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
99
import { PlaywrightFixture, selectHtml } from "./helpers/playwright-fixture.js";
10-
import type { TemplateName } from "./helpers/vite.js";
10+
import { type TemplateName, reactRouterConfig } from "./helpers/vite.js";
1111

1212
const templateNames = [
1313
"vite-5-template",
1414
"rsc-parcel-framework",
15+
"rsc-vite-framework",
1516
] as const satisfies TemplateName[];
1617

1718
test.describe("actions", () => {
@@ -31,6 +32,9 @@ test.describe("actions", () => {
3132
fixture = await createFixture({
3233
templateName,
3334
files: {
35+
"react-router.config.ts": reactRouterConfig({
36+
viteEnvironmentApi: templateName.includes("rsc"),
37+
}),
3438
"app/routes/urlencoded.tsx": js`
3539
import { Form, useActionData } from "react-router";
3640

integration/catch-boundary-data-test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import {
77
} from "./helpers/create-fixture.js";
88
import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
99
import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
10-
import type { TemplateName } from "./helpers/vite.js";
10+
import { reactRouterConfig, type TemplateName } from "./helpers/vite.js";
1111

1212
const templateNames = [
1313
"vite-5-template",
1414
"rsc-parcel-framework",
15+
"rsc-vite-framework",
1516
] as const satisfies TemplateName[];
1617

1718
let ROOT_BOUNDARY_TEXT = "ROOT_TEXT" as const;
@@ -48,6 +49,9 @@ test.describe("ErrorBoundary (thrown responses)", () => {
4849
fixture = await createFixture({
4950
templateName,
5051
files: {
52+
"react-router.config.ts": reactRouterConfig({
53+
viteEnvironmentApi: templateName.includes("rsc"),
54+
}),
5155
"app/root.tsx": js`
5256
import {
5357
Links,

integration/client-data-test.ts

Lines changed: 134 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -12,141 +12,144 @@ import { type TemplateName, reactRouterConfig } from "./helpers/vite.js";
1212

1313
const templateNames = [
1414
"vite-5-template",
15-
"rsc-parcel-framework",
15+
"rsc-vite-framework",
1616
] as const satisfies TemplateName[];
1717

18-
function getFiles({
19-
splitRouteModules,
20-
parentClientLoader,
21-
parentClientLoaderHydrate,
22-
parentAdditions,
23-
childClientLoader,
24-
childClientLoaderHydrate,
25-
childAdditions,
26-
}: {
27-
splitRouteModules: boolean;
28-
parentClientLoader: boolean;
29-
parentClientLoaderHydrate: boolean;
30-
parentAdditions?: string;
31-
childClientLoader: boolean;
32-
childClientLoaderHydrate: boolean;
33-
childAdditions?: string;
34-
}) {
35-
return {
36-
"react-router.config.ts": reactRouterConfig({ splitRouteModules }),
37-
"app/root.tsx": js`
38-
import { Outlet, Scripts } from "react-router"
39-
40-
export default function Root() {
41-
return (
42-
<html>
43-
<head></head>
44-
<body>
45-
<main>
46-
<Outlet />
47-
</main>
48-
<Scripts />
49-
</body>
50-
</html>
51-
);
52-
}
53-
`,
54-
"app/routes/_index.tsx": js`
55-
import { Link } from "react-router"
56-
export default function Component() {
57-
return <Link to="/parent/child">Go to /parent/child</Link>
58-
}
59-
`,
60-
"app/routes/parent.tsx": js`
61-
import { Outlet, useLoaderData } from "react-router"
62-
export function loader() {
63-
return { message: 'Parent Server Loader' };
64-
}
65-
${
66-
parentClientLoader
67-
? js`
68-
export async function clientLoader({ serverLoader }) {
69-
// Need a small delay to ensure we capture the server-rendered
70-
// fallbacks for assertions
71-
await new Promise(r => setTimeout(r, 100))
72-
let data = await serverLoader();
73-
return { message: data.message + " (mutated by client)" };
74-
}
75-
`
76-
: ""
77-
}
78-
${
79-
parentClientLoaderHydrate
80-
? js`
81-
clientLoader.hydrate = true;
82-
export function HydrateFallback() {
83-
return <p>Parent Fallback</p>
84-
}
85-
`
86-
: ""
87-
}
88-
${parentAdditions || ""}
89-
export default function Component() {
90-
let data = useLoaderData();
91-
return (
92-
<>
93-
<p id="parent-data">{data.message}</p>
94-
<Outlet/>
95-
</>
96-
);
97-
}
98-
`,
99-
"app/routes/parent.child.tsx": js`
100-
import { Form, Outlet, useActionData, useLoaderData } from "react-router"
101-
export function loader() {
102-
return { message: 'Child Server Loader' };
103-
}
104-
export function action() {
105-
return { message: 'Child Server Action' };
106-
}
107-
${
108-
childClientLoader
109-
? js`
110-
export async function clientLoader({ serverLoader }) {
111-
// Need a small delay to ensure we capture the server-rendered
112-
// fallbacks for assertions
113-
await new Promise(r => setTimeout(r, 100))
114-
let data = await serverLoader();
115-
return { message: data.message + " (mutated by client)" };
116-
}
117-
`
118-
: ""
119-
}
120-
${
121-
childClientLoaderHydrate
122-
? js`
123-
clientLoader.hydrate = true;
124-
export function HydrateFallback() {
125-
return <p>Child Fallback</p>
126-
}
127-
`
128-
: ""
129-
}
130-
${childAdditions || ""}
131-
export default function Component() {
132-
let data = useLoaderData();
133-
let actionData = useActionData();
134-
return (
135-
<>
136-
<p id="child-data">{data.message}</p>
137-
<Form method="post">
138-
<button type="submit">Submit</button>
139-
{actionData ? <p id="child-action-data">{actionData.message}</p> : null}
140-
</Form>
141-
</>
142-
);
143-
}
144-
`,
145-
};
146-
}
147-
14818
test.describe("Client Data", () => {
14919
for (const templateName of templateNames) {
20+
function getFiles({
21+
splitRouteModules,
22+
parentClientLoader,
23+
parentClientLoaderHydrate,
24+
parentAdditions,
25+
childClientLoader,
26+
childClientLoaderHydrate,
27+
childAdditions,
28+
}: {
29+
splitRouteModules: boolean;
30+
parentClientLoader: boolean;
31+
parentClientLoaderHydrate: boolean;
32+
parentAdditions?: string;
33+
childClientLoader: boolean;
34+
childClientLoaderHydrate: boolean;
35+
childAdditions?: string;
36+
}) {
37+
return {
38+
"react-router.config.ts": reactRouterConfig({
39+
splitRouteModules,
40+
viteEnvironmentApi: templateName.includes("rsc"),
41+
}),
42+
"app/root.tsx": js`
43+
import { Outlet, Scripts } from "react-router"
44+
45+
export default function Root() {
46+
return (
47+
<html>
48+
<head></head>
49+
<body>
50+
<main>
51+
<Outlet />
52+
</main>
53+
<Scripts />
54+
</body>
55+
</html>
56+
);
57+
}
58+
`,
59+
"app/routes/_index.tsx": js`
60+
import { Link } from "react-router"
61+
export default function Component() {
62+
return <Link to="/parent/child">Go to /parent/child</Link>
63+
}
64+
`,
65+
"app/routes/parent.tsx": js`
66+
import { Outlet, useLoaderData } from "react-router"
67+
export function loader() {
68+
return { message: 'Parent Server Loader' };
69+
}
70+
${
71+
parentClientLoader
72+
? js`
73+
export async function clientLoader({ serverLoader }) {
74+
// Need a small delay to ensure we capture the server-rendered
75+
// fallbacks for assertions
76+
await new Promise(r => setTimeout(r, 100))
77+
let data = await serverLoader();
78+
return { message: data.message + " (mutated by client)" };
79+
}
80+
`
81+
: ""
82+
}
83+
${
84+
parentClientLoaderHydrate
85+
? js`
86+
clientLoader.hydrate = true;
87+
export function HydrateFallback() {
88+
return <p>Parent Fallback</p>
89+
}
90+
`
91+
: ""
92+
}
93+
${parentAdditions || ""}
94+
export default function Component() {
95+
let data = useLoaderData();
96+
return (
97+
<>
98+
<p id="parent-data">{data.message}</p>
99+
<Outlet/>
100+
</>
101+
);
102+
}
103+
`,
104+
"app/routes/parent.child.tsx": js`
105+
import { Form, Outlet, useActionData, useLoaderData } from "react-router"
106+
export function loader() {
107+
return { message: 'Child Server Loader' };
108+
}
109+
export function action() {
110+
return { message: 'Child Server Action' };
111+
}
112+
${
113+
childClientLoader
114+
? js`
115+
export async function clientLoader({ serverLoader }) {
116+
// Need a small delay to ensure we capture the server-rendered
117+
// fallbacks for assertions
118+
await new Promise(r => setTimeout(r, 100))
119+
let data = await serverLoader();
120+
return { message: data.message + " (mutated by client)" };
121+
}
122+
`
123+
: ""
124+
}
125+
${
126+
childClientLoaderHydrate
127+
? js`
128+
clientLoader.hydrate = true;
129+
export function HydrateFallback() {
130+
return <p>Child Fallback</p>
131+
}
132+
`
133+
: ""
134+
}
135+
${childAdditions || ""}
136+
export default function Component() {
137+
let data = useLoaderData();
138+
let actionData = useActionData();
139+
return (
140+
<>
141+
<p id="child-data">{data.message}</p>
142+
<Form method="post">
143+
<button type="submit">Submit</button>
144+
{actionData ? <p id="child-action-data">{actionData.message}</p> : null}
145+
</Form>
146+
</>
147+
);
148+
}
149+
`,
150+
};
151+
}
152+
150153
let appFixture: AppFixture;
151154

152155
test.afterEach(async () => {

integration/helpers/create-fixture.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,20 @@ export async function createFixture(init: FixtureInit, mode?: ServerMode) {
199199
let build: ServerBuild | null = null;
200200
type RequestHandler = (request: Request) => Promise<Response>;
201201
let handler: RequestHandler;
202-
if (templateName.includes("parcel")) {
202+
if (templateName === "rsc-vite-framework") {
203+
handler = (await import(buildPath)).default;
204+
if (typeof handler !== "function") {
205+
throw new Error(
206+
"Expected a default request handler function export in Vite RSC Framework Mode server build",
207+
);
208+
}
209+
} else if (templateName === "rsc-parcel-framework") {
203210
let serverBuild = await import(buildPath);
204211
handler = (serverBuild?.requestHandler ??
205212
serverBuild?.default?.requestHandler) as RequestHandler;
206-
if (!handler) {
213+
if (typeof handler !== "function") {
207214
throw new Error(
208-
"Expected a 'requestHandler' export in Parcel server build",
215+
"Expected a 'requestHandler' function export in Parcel RSC Framework server build",
209216
);
210217
}
211218
} else {
@@ -356,7 +363,7 @@ export async function createAppFixture(fixture: Fixture, mode?: ServerMode) {
356363
});
357364
}
358365

359-
if (fixture.templateName.includes("parcel")) {
366+
if (fixture.templateName.includes("rsc")) {
360367
let port = await getPort();
361368
let { stop } = await spawnTestServer({
362369
cwd: fixture.projectDir,
@@ -370,7 +377,7 @@ export async function createAppFixture(fixture: Fixture, mode?: ServerMode) {
370377
let parsedPort = parseInt(matches[1], 10);
371378
if (port !== parsedPort) {
372379
throw new Error(
373-
`Expected Parcel build server to start on port ${port}, but it started on port ${parsedPort}`,
380+
`Expected RSC Framework Mode build server to start on port ${port}, but it started on port ${parsedPort}`,
374381
);
375382
}
376383
},
@@ -458,6 +465,7 @@ export async function createFixtureProject(
458465
: {
459466
"vite.config.js": await viteConfig.basic({
460467
port,
468+
templateName,
461469
}),
462470
}),
463471
...(hasReactRouterConfig
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
build
2+
node_modules

0 commit comments

Comments
 (0)