Skip to content

Commit

Permalink
fix: static generated RSC x-component and response type selection (#99)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lazarv authored Dec 17, 2024
1 parent a70e816 commit 138ef89
Show file tree
Hide file tree
Showing 27 changed files with 357 additions and 291 deletions.
1 change: 1 addition & 0 deletions docs/react-server.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export default {
return [
...paths.map(({ path }) => ({
path: path.replace(/^\/en/, ""),
rsc: false,
})),
{
path: "/sitemap.xml",
Expand Down
2 changes: 2 additions & 0 deletions docs/src/pages/en/(pages)/deploy/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ The adapter handler function will receive the following properties:
The `files` object contains the following functions:

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

- [ ] `static`: The function to copy the static files.
- [ ] `compressed`: The function to copy the compressed static files.
- [ ] `assets`: The function to copy the assets files.
- [ ] `client`: The function to copy the client files.
- [ ] `public`: The function to copy the public files.
Expand Down
3 changes: 3 additions & 0 deletions docs/src/pages/en/(pages)/framework/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ You can disable client build if you only want to build the server part of your a
**export:** Static export. Default is `false`.
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).

**compression:** Enable compression. Default is `false`.
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.

**deploy:** Deploy using adapter. Default is `false`.
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.

Expand Down
28 changes: 0 additions & 28 deletions docs/src/pages/en/(pages)/router/server-routing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -178,34 +178,6 @@ export default function App() {
}
```

<Link name="route-rendering-in-standalone-mode">
## Route rendering in standalone-mode
</Link>

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.

```tsx
import { Route } from '@lazarv/react-server/router';

function Layout({ children }) {
return (
<div>
<h1>Layout</h1>
{children}
</div>
);
}

export default function App() {
return (
<Route path="/" render={Layout} standalone={false}>
<Route path="/" exact element={<Home />} />
<Route path="/about" element={<About />} />
</Route>
);
}
```

<Link name="redirects">
## Redirects
</Link>
Expand Down
50 changes: 39 additions & 11 deletions docs/src/pages/en/(pages)/router/static.page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ To mark a page as static, create a file with the matching path for the page with

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

```ts
```ts filename="users.static.ts"
export default true;
```

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

```json
```json filename="users.static.json"
true
```

Expand All @@ -32,13 +32,13 @@ true

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:

```ts
```ts filename="users/[id].static.ts"
export default [{ id: 1 }, { id: 2 }, { id: 3 }];
```

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

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

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

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

```json
```json filename="users/[id].static.json"
[{ "id": 1 }, { "id": 2 }, { "id": 3 }]
```

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

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.

```js
```js filename="react-server.config.mjs"
export default {
export(paths) {
return paths.map(({ path }) => ({
Expand All @@ -76,7 +76,7 @@ export default {

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

```js
```js filename="react-server.config.mjs"
export default {
export(paths) {
return [
Expand All @@ -87,7 +87,7 @@ export default {
};
```

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

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.

```js
```js filename="react-server.config.mjs"
export default {
export() {
return [
Expand All @@ -124,7 +124,7 @@ export default {

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.

```js
```js filename="react-server.config.mjs"
export default {
export() {
return [
Expand All @@ -135,4 +135,32 @@ export default {
];
},
};
```
```

<Link name="static-export-outlets">
## Static export outlets
</Link>

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.

```js filename="react-server.config.mjs"
export default {
export() {
return [{ path: "/photos/1", outlet: "modal" }];
},
};
```

<Link name="disable-static-export-for-rsc-routes">
## Disable static export for RSC routes
</Link>

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.

```js filename="react-server.config.mjs"
export default {
export() {
return [{ path: "/photos/1", rsc: false }];
},
};
```
15 changes: 10 additions & 5 deletions packages/react-server-adapter-core/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,8 @@ export function createAdapter({
success(`${name} output successfully prepared.`);

const files = {
static: () =>
getFiles(
["**/*", "!**/*.html.gz", "!**/*.html.br", "!**/x-component.*"],
distDir
),
static: () => getFiles(["**/*", "!**/*.gz", "!**/*.br"], distDir),
compressed: () => getFiles(["**/*.gz", "**/*.br"], distDir),
assets: () => getFiles(["assets/**/*"], reactServerDir),
client: () =>
getFiles(["client/**/*", "!**/*-manifest.json"], reactServerDir),
Expand Down Expand Up @@ -378,6 +375,14 @@ export function createAdapter({
out ?? outStaticDir,
reactServerDir
),
compressed: async (out) =>
copyFiles(
"copying compressed files",
await files.compressed(),
distDir,
out ?? outStaticDir,
reactServerDir
),
assets: async (out) =>
copyFiles(
"copying assets",
Expand Down
6 changes: 6 additions & 0 deletions packages/react-server-adapter-vercel/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ export const adapter = createAdapter({
version: 3,
...adapterOptions,
routes: [
{
src: "/(.*)(@([^.]+)\\.)?(rsc|remote)\\.x-component$",
headers: {
"Content-Type": "text/x-component; charset=utf-8",
},
},
{ handle: "filesystem" },
...(adapterOptions?.routes ?? []),
adapterOptions?.routes?.find((route) => route.status === 404) ?? {
Expand Down
1 change: 1 addition & 0 deletions packages/react-server/bin/commands/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default (cli) =>
.option("--server", "[boolean] build server", { default: true })
.option("--client", "[boolean] build client", { default: true })
.option("--export", "[boolean] static export")
.option("--compression", "[boolean] enable compression", { default: false })
.option("--adapter <adapter>", "[boolean|string] adapter", {
default: "",
type: [String],
Expand Down
21 changes: 12 additions & 9 deletions packages/react-server/client/ClientProvider.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,7 @@ const prefetchOutlet = (to, { outlet = PAGE_ROOT, ttl = Infinity }) => {
if (flightCache.has(key)) {
cache.set(outlet, flightCache.get(key));
} else {
getFlightResponse(to, {
outlet,
standalone: outlet !== PAGE_ROOT,
});
getFlightResponse(to, { outlet });
flightCache.set(key, cache.get(outlet));
if (typeof ttl === "number" && ttl < Infinity) {
setTimeout(() => {
Expand Down Expand Up @@ -232,7 +229,6 @@ export const streamOptions = (outlet, remote) => ({
body: formData,
outlet: target,
remote,
standalone: target !== PAGE_ROOT,
callServer: id || true,
onFetch: (res) => {
const callServer =
Expand Down Expand Up @@ -297,18 +293,25 @@ function getFlightResponse(url, options = {}) {
self[`__flightHydration__${options.outlet || PAGE_ROOT}__`] = true;
activeChunk.set(options.outlet || url, cache.get(options.outlet || url));
} else if (!options.fromScript) {
const src = new URL(url === PAGE_ROOT ? location.href : url, location);
const outlet =
options.outlet && options.outlet !== PAGE_ROOT
? `@${options.outlet}.`
: "";
src.pathname = `${src.pathname}/${outlet}rsc.x-component`.replace(
/\/+/g,
"/"
);
cache.set(
options.outlet || url,
createFromFetch(
fetch(url === PAGE_ROOT ? location.href : url, {
fetch(src.toString(), {
...options.request,
method: options.method,
body: options.body,
headers: {
...options.request?.headers,
accept: `text/x-component${
options.standalone && url !== PAGE_ROOT ? ";standalone" : ""
}${options.remote && url !== PAGE_ROOT ? ";remote" : ""}`,
accept: "text/x-component",
"React-Server-Outlet": encodeURIComponent(
options.outlet || PAGE_ROOT
),
Expand Down
13 changes: 12 additions & 1 deletion packages/react-server/client/Link.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,18 @@ export default function Link({
} catch (e) {
onError?.(e);
}
}, []);
}, [
to,
target,
local,
outlet,
root,
replace,
push,
rollback,
onNavigate,
onError,
]);

const handleNavigate = async (e) => {
e.preventDefault();
Expand Down
23 changes: 4 additions & 19 deletions packages/react-server/client/ReactServerComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,7 @@ import {
useClient,
} from "./context.mjs";

function FlightComponent({
standalone = false,
remote = false,
defer = false,
request,
children,
}) {
function FlightComponent({ remote = false, defer = false, request, children }) {
const { url, outlet } = useContext(FlightContext);
const client = useClient();
const { registerOutlet, subscribe, getFlightResponse } = client;
Expand All @@ -24,7 +18,6 @@ function FlightComponent({
(outlet === PAGE_ROOT || remote
? getFlightResponse?.(url, {
outlet,
standalone,
remote,
defer,
request,
Expand All @@ -39,7 +32,6 @@ function FlightComponent({
const unsubscribe = subscribe(outlet || url, (to, options, callback) => {
const nextComponent = getFlightResponse(to, {
outlet,
standalone,
remote,
request,
});
Expand All @@ -59,7 +51,7 @@ function FlightComponent({
unregisterOutlet();
unsubscribe();
};
}, [url, outlet, standalone, remote, request, subscribe, getFlightResponse]);
}, [url, outlet, remote, request, subscribe, getFlightResponse]);

useEffect(() => {
if (children || (outlet !== PAGE_ROOT && Component)) {
Expand All @@ -71,7 +63,6 @@ function FlightComponent({
if (remote || defer) {
const nextComponent = getFlightResponse(url, {
outlet,
standalone,
remote,
defer,
request,
Expand All @@ -81,7 +72,7 @@ function FlightComponent({
startTransition(() => setComponent(nextComponent));
}
}
}, [url, outlet, standalone, remote, defer, request, getFlightResponse]);
}, [url, outlet, remote, defer, request, getFlightResponse]);

return (
<ClientContext.Provider value={{ ...client, error }}>
Expand All @@ -93,20 +84,14 @@ function FlightComponent({
export default function ReactServerComponent({
url,
outlet = null,
standalone,
remote,
defer,
request,
children,
}) {
return (
<FlightContext.Provider value={{ url, outlet }}>
<FlightComponent
standalone={standalone}
remote={remote}
defer={defer}
request={request}
>
<FlightComponent remote={remote} defer={defer} request={request}>
{children}
</FlightComponent>
</FlightContext.Provider>
Expand Down
Loading

0 comments on commit 138ef89

Please sign in to comment.