Skip to content

Commit 138ef89

Browse files
authored
fix: static generated RSC x-component and response type selection (#99)
This PR fixes issues around static generated RSC x-component files and how response type selection works to send an RSC payload. The response type selection needs to be on par with the static generation to maintain the same solution for both static and dynamic rendering. Changes request payload to select RSC rendering. Instead of using HTTP headers, the framework now uses an URL pathname suffix, including outlet name and remote or standard RSC rendering and static generation uses the same format in filenames. This fixes #95. Adds build options to enable compression (GZip and Brotli) and opt-out of static RSC rendering, see more about this in the updated docs. Adds static export option to render a single outlet. This is mainly to keep consistency with the URL format right now, but might be a useful feature for SSG. Updates the deployment Adapter API to not copy compressed files by default, but copy static RSC files and also the Vercel adapter to add the necessary `Content-Type` header for static `.x-component` files. This fixes #94. Removes the `standalone` route option and mode as it was not really useful and there are other ways to achieve the same functionality.
1 parent a70e816 commit 138ef89

File tree

27 files changed

+357
-291
lines changed

27 files changed

+357
-291
lines changed

docs/react-server.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default {
2121
return [
2222
...paths.map(({ path }) => ({
2323
path: path.replace(/^\/en/, ""),
24+
rsc: false,
2425
})),
2526
{
2627
path: "/sitemap.xml",

docs/src/pages/en/(pages)/deploy/api.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ The adapter handler function will receive the following properties:
7575
The `files` object contains the following functions:
7676

7777
- [ ] `static`: The function to get the static files.
78+
- [ ] `compressed`: The function to get the compressed static files.
7879
- [ ] `assets`: The function to get the assets files.
7980
- [ ] `client`: The function to get the client files.
8081
- [ ] `public`: The function to get the public files.
@@ -89,6 +90,7 @@ const staticFiles = await files.static();
8990
The `copy` object contains the following functions:
9091

9192
- [ ] `static`: The function to copy the static files.
93+
- [ ] `compressed`: The function to copy the compressed static files.
9294
- [ ] `assets`: The function to copy the assets files.
9395
- [ ] `client`: The function to copy the client files.
9496
- [ ] `public`: The function to copy the public files.

docs/src/pages/en/(pages)/framework/cli.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ You can disable client build if you only want to build the server part of your a
9898
**export:** Static export. Default is `false`.
9999
You can export your app as static HTML pages. You can define the routes to export in your `react-server.config.mjs` file. See more details at [Static generation](/router/static).
100100

101+
**compression:** Enable compression. Default is `false`.
102+
You can enable compression if you want to compress your static build. Compression is not enabled by default for static builds. Gzip and Brotli compression is used when compression is enabled. The production mode server serves these compressed files by default when the compressed static files are present.
103+
101104
**deploy:** Deploy using adapter. Default is `false`.
102105
If you use an adapter in your `react-server.config.mjs` file, the adapter will pre-build your app for deployment and when you use this argument, the adapter will also deploy your app at the end of the build process.
103106

docs/src/pages/en/(pages)/router/server-routing.mdx

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -178,34 +178,6 @@ export default function App() {
178178
}
179179
```
180180

181-
<Link name="route-rendering-in-standalone-mode">
182-
## Route rendering in standalone-mode
183-
</Link>
184-
185-
If you want a route to render only it's children when using client-side navigation, you can set the value of the `standalone` prop on the `Route` component to be `false`. This will prevent the route from using it's `render` function when the path matches the route. Upon client-side navigation or refreshing, the route will only render it's children.
186-
187-
```tsx
188-
import { Route } from '@lazarv/react-server/router';
189-
190-
function Layout({ children }) {
191-
return (
192-
<div>
193-
<h1>Layout</h1>
194-
{children}
195-
</div>
196-
);
197-
}
198-
199-
export default function App() {
200-
return (
201-
<Route path="/" render={Layout} standalone={false}>
202-
<Route path="/" exact element={<Home />} />
203-
<Route path="/about" element={<About />} />
204-
</Route>
205-
);
206-
}
207-
```
208-
209181
<Link name="redirects">
210182
## Redirects
211183
</Link>

docs/src/pages/en/(pages)/router/static.page.mdx

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ To mark a page as static, create a file with the matching path for the page with
1414

1515
For pages without any parameters, export default `true`.
1616

17-
```ts
17+
```ts filename="users.static.ts"
1818
export default true;
1919
```
2020

2121
The smallest possible way to mark a page as static is by creating a `.static.json` file defining `true`.
2222

23-
```json
23+
```json filename="users.static.json"
2424
true
2525
```
2626

@@ -32,13 +32,13 @@ true
3232

3333
For dynamic routes, if you have a page at `/users/:id` you can create a file at `/users/[id].static.ts` with the following content:
3434

35-
```ts
35+
```ts filename="users/[id].static.ts"
3636
export default [{ id: 1 }, { id: 2 }, { id: 3 }];
3737
```
3838

3939
You can either export an array of route parameters or an async function returning an array of route parameters.
4040

41-
```ts
41+
```ts filename="users/[id].static.ts"
4242
export default async () => [{ id: 1 }, { id: 2 }, { id: 3 }];
4343
```
4444

@@ -52,7 +52,7 @@ You can use static JSON data for your static pages by creating a file with the `
5252

5353
For example, if you have a page at `/users/:id` you can create a file at `/users/[id].static.json` with the following content:
5454

55-
```json
55+
```json filename="users/[id].static.json"
5656
[{ "id": 1 }, { "id": 2 }, { "id": 3 }]
5757
```
5858

@@ -64,7 +64,7 @@ For example, if you have a page at `/users/:id` you can create a file at `/users
6464

6565
You can override all static paths by defining an `export()` function in your `@lazarv/react-server` configuration file. This function will be called with an array of all static paths and you can return a new array of paths to override the default static paths. In this example, we remove the `/en` prefix from all static paths.
6666

67-
```js
67+
```js filename="react-server.config.mjs"
6868
export default {
6969
export(paths) {
7070
return paths.map(({ path }) => ({
@@ -76,7 +76,7 @@ export default {
7676

7777
You can also use this function to add new static paths or remove some paths.
7878

79-
```js
79+
```js filename="react-server.config.mjs"
8080
export default {
8181
export(paths) {
8282
return [
@@ -87,7 +87,7 @@ export default {
8787
};
8888
```
8989

90-
```js
90+
```js filename="react-server.config.mjs"
9191
export default {
9292
export(paths) {
9393
return paths.filter(({ path }) => path !== "/en");
@@ -101,7 +101,7 @@ export default {
101101

102102
You can also export API routes as static routes. To do this, you can define your static path as an object with the `path`, `filename`, `method` and `headers` properties, where `path` is the route path, `filename` is the filename for the static file, `method` is the HTTP method for the request and `headers` is an object with the headers for the request. `method` and `headers` are optional.
103103

104-
```js
104+
```js filename="react-server.config.mjs"
105105
export default {
106106
export() {
107107
return [
@@ -124,7 +124,7 @@ export default {
124124

125125
You can also export micro-frontend routes as static. To do this, you can define your static path as an object with the `path` and `remote` properties, where `path` is the route path and `remote` is a flag to indicate that the route is a micro-frontend route and the remote response payload needs to be generated at build time. By using static export for micro-frontends, you can improve the performance of your application by pre-rendering the micro-frontend content at build time.
126126

127-
```js
127+
```js filename="react-server.config.mjs"
128128
export default {
129129
export() {
130130
return [
@@ -135,4 +135,32 @@ export default {
135135
];
136136
},
137137
};
138-
```
138+
```
139+
140+
<Link name="static-export-outlets">
141+
## Static export outlets
142+
</Link>
143+
144+
You can also export outlets as static. To do this, you can define your static path as an object with the `path` and `outlet` properties, where `path` is the route path and `outlet` is the name of the outlet. By using static export for outlets, you can improve the performance of your application by pre-rendering the outlet content at build time. Exported outlets will be rendered as RSC components. Client-side navigation to an exported outlet will use the static outlet content instead of making a request to the server.
145+
146+
```js filename="react-server.config.mjs"
147+
export default {
148+
export() {
149+
return [{ path: "/photos/1", outlet: "modal" }];
150+
},
151+
};
152+
```
153+
154+
<Link name="disable-static-export-for-rsc-routes">
155+
## Disable static export for RSC routes
156+
</Link>
157+
158+
You can disable static export for RSC routes by setting the `rsc` property to `false`. This is useful if you have a page that is an RSC route but you don't want to pre-render it at build time or you don't plan to use the RSC payload for that route. This will prevent the RSC payload from being generated at build time and the route will be rendered only as a regular HTML page to reduce the deployment size.
159+
160+
```js filename="react-server.config.mjs"
161+
export default {
162+
export() {
163+
return [{ path: "/photos/1", rsc: false }];
164+
},
165+
};
166+
```

packages/react-server-adapter-core/index.mjs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -344,11 +344,8 @@ export function createAdapter({
344344
success(`${name} output successfully prepared.`);
345345

346346
const files = {
347-
static: () =>
348-
getFiles(
349-
["**/*", "!**/*.html.gz", "!**/*.html.br", "!**/x-component.*"],
350-
distDir
351-
),
347+
static: () => getFiles(["**/*", "!**/*.gz", "!**/*.br"], distDir),
348+
compressed: () => getFiles(["**/*.gz", "**/*.br"], distDir),
352349
assets: () => getFiles(["assets/**/*"], reactServerDir),
353350
client: () =>
354351
getFiles(["client/**/*", "!**/*-manifest.json"], reactServerDir),
@@ -378,6 +375,14 @@ export function createAdapter({
378375
out ?? outStaticDir,
379376
reactServerDir
380377
),
378+
compressed: async (out) =>
379+
copyFiles(
380+
"copying compressed files",
381+
await files.compressed(),
382+
distDir,
383+
out ?? outStaticDir,
384+
reactServerDir
385+
),
381386
assets: async (out) =>
382387
copyFiles(
383388
"copying assets",

packages/react-server-adapter-vercel/index.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ export const adapter = createAdapter({
6262
version: 3,
6363
...adapterOptions,
6464
routes: [
65+
{
66+
src: "/(.*)(@([^.]+)\\.)?(rsc|remote)\\.x-component$",
67+
headers: {
68+
"Content-Type": "text/x-component; charset=utf-8",
69+
},
70+
},
6571
{ handle: "filesystem" },
6672
...(adapterOptions?.routes ?? []),
6773
adapterOptions?.routes?.find((route) => route.status === 404) ?? {

packages/react-server/bin/commands/build.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default (cli) =>
1515
.option("--server", "[boolean] build server", { default: true })
1616
.option("--client", "[boolean] build client", { default: true })
1717
.option("--export", "[boolean] static export")
18+
.option("--compression", "[boolean] enable compression", { default: false })
1819
.option("--adapter <adapter>", "[boolean|string] adapter", {
1920
default: "",
2021
type: [String],

packages/react-server/client/ClientProvider.jsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,7 @@ const prefetchOutlet = (to, { outlet = PAGE_ROOT, ttl = Infinity }) => {
4040
if (flightCache.has(key)) {
4141
cache.set(outlet, flightCache.get(key));
4242
} else {
43-
getFlightResponse(to, {
44-
outlet,
45-
standalone: outlet !== PAGE_ROOT,
46-
});
43+
getFlightResponse(to, { outlet });
4744
flightCache.set(key, cache.get(outlet));
4845
if (typeof ttl === "number" && ttl < Infinity) {
4946
setTimeout(() => {
@@ -232,7 +229,6 @@ export const streamOptions = (outlet, remote) => ({
232229
body: formData,
233230
outlet: target,
234231
remote,
235-
standalone: target !== PAGE_ROOT,
236232
callServer: id || true,
237233
onFetch: (res) => {
238234
const callServer =
@@ -297,18 +293,25 @@ function getFlightResponse(url, options = {}) {
297293
self[`__flightHydration__${options.outlet || PAGE_ROOT}__`] = true;
298294
activeChunk.set(options.outlet || url, cache.get(options.outlet || url));
299295
} else if (!options.fromScript) {
296+
const src = new URL(url === PAGE_ROOT ? location.href : url, location);
297+
const outlet =
298+
options.outlet && options.outlet !== PAGE_ROOT
299+
? `@${options.outlet}.`
300+
: "";
301+
src.pathname = `${src.pathname}/${outlet}rsc.x-component`.replace(
302+
/\/+/g,
303+
"/"
304+
);
300305
cache.set(
301306
options.outlet || url,
302307
createFromFetch(
303-
fetch(url === PAGE_ROOT ? location.href : url, {
308+
fetch(src.toString(), {
304309
...options.request,
305310
method: options.method,
306311
body: options.body,
307312
headers: {
308313
...options.request?.headers,
309-
accept: `text/x-component${
310-
options.standalone && url !== PAGE_ROOT ? ";standalone" : ""
311-
}${options.remote && url !== PAGE_ROOT ? ";remote" : ""}`,
314+
accept: "text/x-component",
312315
"React-Server-Outlet": encodeURIComponent(
313316
options.outlet || PAGE_ROOT
314317
),

packages/react-server/client/Link.jsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,18 @@ export default function Link({
3535
} catch (e) {
3636
onError?.(e);
3737
}
38-
}, []);
38+
}, [
39+
to,
40+
target,
41+
local,
42+
outlet,
43+
root,
44+
replace,
45+
push,
46+
rollback,
47+
onNavigate,
48+
onError,
49+
]);
3950

4051
const handleNavigate = async (e) => {
4152
e.preventDefault();

0 commit comments

Comments
 (0)