@canonical/react-ssr 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -92
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,86 +1,145 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @canonical/react-ssr
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Server-side rendering utilities for React applications. Provides streaming HTML rendering with `JSXRenderer`, Express middleware with `serveStream`, and automatic script/link tag injection from your build output.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- [Building Your Application](#building-your-application)
|
|
11
|
-
- [Server Request Handling](#server-request-handling)
|
|
12
|
-
- [Injecting the Client Application](#injecting-the-client-application)
|
|
13
|
-
3. [Customization](#customization)
|
|
14
|
-
- [Bootstrap Scripts](#bootstrap-scripts)
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @canonical/react-ssr
|
|
9
|
+
```
|
|
15
10
|
|
|
16
|
-
|
|
11
|
+
Peer dependencies: `react`, `react-dom`, `express` (for Express usage).
|
|
17
12
|
|
|
18
|
-
##
|
|
13
|
+
## Express Server
|
|
14
|
+
|
|
15
|
+
Create a renderer that wraps your server entry component:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// src/ssr/renderer.tsx
|
|
19
|
+
import { JSXRenderer } from "@canonical/react-ssr/renderer";
|
|
20
|
+
import htmlString from "../../dist/client/index.html?raw";
|
|
21
|
+
import EntryServer from "./entry-server.js";
|
|
22
|
+
|
|
23
|
+
const Renderer = new JSXRenderer(EntryServer, { htmlString });
|
|
24
|
+
export default Renderer.render;
|
|
25
|
+
```
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
Create an Express server using `serveStream`:
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// src/ssr/server.ts
|
|
31
|
+
import { serveStream } from "@canonical/react-ssr/server";
|
|
32
|
+
import express from "express";
|
|
33
|
+
import render from "./renderer.js";
|
|
34
|
+
|
|
35
|
+
const app = express();
|
|
36
|
+
app.use("/assets", express.static("dist/client/assets"));
|
|
37
|
+
app.use(serveStream(render));
|
|
38
|
+
app.listen(5173);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Build and run:
|
|
21
42
|
|
|
22
43
|
```bash
|
|
23
|
-
|
|
44
|
+
vite build --ssrManifest --outDir dist/client
|
|
45
|
+
vite build --ssr src/ssr/server.ts --outDir dist/server
|
|
46
|
+
node dist/server/server.js
|
|
24
47
|
```
|
|
25
48
|
|
|
26
|
-
##
|
|
49
|
+
## Bun Server
|
|
50
|
+
|
|
51
|
+
The renderer works the same way. For Bun's native server, convert the pipeable stream:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// src/ssr/server-bun.ts
|
|
55
|
+
import render from "./renderer.js";
|
|
56
|
+
import { Readable } from "node:stream";
|
|
57
|
+
|
|
58
|
+
Bun.serve({
|
|
59
|
+
port: 5173,
|
|
60
|
+
async fetch(req) {
|
|
61
|
+
const url = new URL(req.url);
|
|
62
|
+
|
|
63
|
+
// Serve static assets
|
|
64
|
+
if (url.pathname.startsWith("/assets")) {
|
|
65
|
+
return new Response(Bun.file(`dist/client${url.pathname}`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// SSR render
|
|
69
|
+
const { pipe } = render(req, null);
|
|
70
|
+
const readable = Readable.toWeb(Readable.from(pipeToIterable(pipe)));
|
|
71
|
+
return new Response(readable, {
|
|
72
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function pipeToIterable(pipe: (dest: NodeJS.WritableStream) => void) {
|
|
78
|
+
const { Readable } = require("node:stream");
|
|
79
|
+
const passthrough = new (require("node:stream").PassThrough)();
|
|
80
|
+
pipe(passthrough);
|
|
81
|
+
return passthrough;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
27
84
|
|
|
28
|
-
|
|
85
|
+
Or use Express compatibility mode with Bun:
|
|
29
86
|
|
|
30
|
-
|
|
87
|
+
```ts
|
|
88
|
+
// Bun can run Express directly
|
|
89
|
+
import app from "./server.js"; // Your Express server
|
|
90
|
+
export default app;
|
|
91
|
+
```
|
|
31
92
|
|
|
32
|
-
|
|
33
|
-
The server entry point includes the full application HTML for compatibility with streams.
|
|
34
|
-
The client entry point includes just the application component, which is hydrated on the client.
|
|
93
|
+
## Entry Points
|
|
35
94
|
|
|
36
|
-
### Server
|
|
95
|
+
### Server Entry
|
|
37
96
|
|
|
38
|
-
|
|
97
|
+
The server entry renders the full HTML document. `scriptTags` and `linkTags` are extracted from your build output and injected automatically:
|
|
39
98
|
|
|
40
99
|
```tsx
|
|
41
100
|
// src/ssr/entry-server.tsx
|
|
101
|
+
import type { ReactServerEntrypointComponent, RendererServerEntrypointProps } from "@canonical/react-ssr/renderer";
|
|
42
102
|
import Application from "../Application.js";
|
|
43
|
-
import React from "react";
|
|
44
|
-
import type {ReactServerEntrypointComponent, RendererServerEntrypointProps} from "@canonical/react-ssr/renderer";
|
|
45
103
|
|
|
46
|
-
|
|
47
|
-
|
|
104
|
+
const EntryServer: ReactServerEntrypointComponent<RendererServerEntrypointProps> = ({
|
|
105
|
+
lang = "en",
|
|
106
|
+
scriptTags,
|
|
107
|
+
linkTags,
|
|
108
|
+
}) => (
|
|
48
109
|
<html lang={lang}>
|
|
49
110
|
<head>
|
|
50
|
-
<title>
|
|
51
|
-
{scriptTags}
|
|
111
|
+
<title>My App</title>
|
|
52
112
|
{linkTags}
|
|
53
113
|
</head>
|
|
54
114
|
<body>
|
|
55
115
|
<div id="root">
|
|
56
116
|
<Application />
|
|
57
117
|
</div>
|
|
118
|
+
{scriptTags}
|
|
58
119
|
</body>
|
|
59
120
|
</html>
|
|
60
121
|
);
|
|
61
122
|
|
|
62
123
|
export default EntryServer;
|
|
63
124
|
```
|
|
64
|
-
This component is responsible for rendering the HTML structure and injecting the necessary script and link tags that are required for hydration on the client.
|
|
65
125
|
|
|
66
|
-
### Client
|
|
67
|
-
|
|
126
|
+
### Client Entry
|
|
127
|
+
|
|
128
|
+
The client entry hydrates the server-rendered HTML:
|
|
129
|
+
|
|
68
130
|
```tsx
|
|
69
131
|
// src/ssr/entry-client.tsx
|
|
70
132
|
import { hydrateRoot } from "react-dom/client";
|
|
71
133
|
import Application from "../Application.js";
|
|
72
134
|
|
|
73
|
-
|
|
74
|
-
hydrateRoot(document.getElementById("root") as HTMLElement, <Application />);
|
|
135
|
+
hydrateRoot(document.getElementById("root")!, <Application />);
|
|
75
136
|
```
|
|
76
137
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
The example below uses Vite.
|
|
138
|
+
## Building
|
|
139
|
+
|
|
140
|
+
Two-phase build with Vite:
|
|
81
141
|
|
|
82
|
-
```
|
|
83
|
-
// package.json
|
|
142
|
+
```json
|
|
84
143
|
{
|
|
85
144
|
"scripts": {
|
|
86
145
|
"build": "bun run build:client && bun run build:server",
|
|
@@ -88,65 +147,34 @@ The example below uses Vite.
|
|
|
88
147
|
"build:server": "vite build --ssr src/ssr/server.ts --outDir dist/server"
|
|
89
148
|
}
|
|
90
149
|
}
|
|
91
|
-
|
|
92
150
|
```
|
|
93
151
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
Once your app is built, you can set up an Express server to handle SSR requests.
|
|
97
|
-
See [this file](../../../apps/react/boilerplate-vite/src/ssr/server.ts) as an example.
|
|
98
|
-
|
|
99
|
-
### Injecting the Client Application
|
|
152
|
+
The client build produces `dist/client/index.html` with bundled script/link tags. The server build imports this HTML string to extract those tags for injection.
|
|
100
153
|
|
|
101
|
-
|
|
154
|
+
## Customization
|
|
102
155
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
```html
|
|
106
|
-
<html lang="en">
|
|
107
|
-
<head>
|
|
108
|
-
<meta charset="UTF-8" />
|
|
109
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
110
|
-
<title>Vite + React + TS</title>
|
|
111
|
-
</head>
|
|
112
|
-
<body>
|
|
113
|
-
<div id="root"></div>
|
|
114
|
-
<!-- Inject the client-side entry point -->
|
|
115
|
-
<script type="module" src="/src/ssr/entry-client.tsx"></script>
|
|
116
|
-
</body>
|
|
117
|
-
</html>
|
|
118
|
-
```
|
|
119
|
-
This script will hydrate the app on the client, connecting the React app to the server-rendered HTML.
|
|
120
|
-
|
|
121
|
-
#### Customization
|
|
122
|
-
You can inject additional bootstrapping scripts to customize the client-side setup.
|
|
123
|
-
This is useful if you need more control over how the client app boots.
|
|
124
|
-
|
|
125
|
-
##### Bootstrap Scripts
|
|
126
|
-
You can pass custom modules, scripts, or inline script content to be executed on the client before the app is hydrated.
|
|
127
|
-
|
|
128
|
-
###### Options
|
|
129
|
-
- `bootstrapModules`: An array of module paths. Generates `<script type="module" src="{SCRIPT_LINK}"></script>` elements.
|
|
130
|
-
- `bootstrapScripts`: An array of script paths. Generates `<script type="text/javascript" src="{SCRIPT_LINK}"></script>` elements.
|
|
131
|
-
- `bootstrapScriptContent`: Raw script content. Generates `<script type="text/javascript">{SCRIPT_CONTENT}</script>` elements.
|
|
156
|
+
Pass options to React's `renderToPipeableStream`:
|
|
132
157
|
|
|
133
158
|
```ts
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
);
|
|
159
|
+
const Renderer = new JSXRenderer(EntryServer, {
|
|
160
|
+
htmlString,
|
|
161
|
+
renderToPipeableStreamOptions: {
|
|
162
|
+
bootstrapModules: ["src/ssr/entry-client.tsx"],
|
|
163
|
+
onShellReady() { console.log("Shell ready"); },
|
|
164
|
+
onError(err) { console.error(err); },
|
|
165
|
+
},
|
|
166
|
+
});
|
|
145
167
|
```
|
|
146
168
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
169
|
+
Options include:
|
|
170
|
+
- `bootstrapModules` - ES modules to load (`<script type="module">`)
|
|
171
|
+
- `bootstrapScripts` - Scripts to load (`<script>`)
|
|
172
|
+
- `bootstrapScriptContent` - Inline script content
|
|
150
173
|
|
|
174
|
+
See [React's renderToPipeableStream documentation](https://react.dev/reference/react-dom/server/renderToPipeableStream) for all options.
|
|
151
175
|
|
|
176
|
+
## Examples
|
|
152
177
|
|
|
178
|
+
See working examples in the monorepo:
|
|
179
|
+
- [`apps/react/boilerplate-vite`](../../../apps/react/boilerplate-vite) - Vite + Express
|
|
180
|
+
- [`apps/react/demo`](../../../apps/react/demo) - Full application example
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonical/react-ssr",
|
|
3
3
|
"description": "TBD",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.12.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/types/index.d.ts",
|
|
@@ -51,20 +51,20 @@
|
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@biomejs/biome": "2.3.11",
|
|
54
|
-
"@canonical/biome-config": "^0.
|
|
55
|
-
"@canonical/typescript-config-base": "^0.
|
|
56
|
-
"@canonical/webarchitect": "^0.
|
|
54
|
+
"@canonical/biome-config": "^0.12.0",
|
|
55
|
+
"@canonical/typescript-config-base": "^0.12.0",
|
|
56
|
+
"@canonical/webarchitect": "^0.12.0",
|
|
57
57
|
"@types/express": "^5.0.6",
|
|
58
58
|
"@types/react": "^19.2.8",
|
|
59
59
|
"@types/react-dom": "^19.2.3",
|
|
60
60
|
"typescript": "^5.9.3"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@canonical/utils": "^0.
|
|
63
|
+
"@canonical/utils": "^0.12.0",
|
|
64
64
|
"domhandler": "^5.0.3",
|
|
65
65
|
"express": "^5.2.1",
|
|
66
66
|
"htmlparser2": "^10.0.0",
|
|
67
67
|
"react-dom": "^19.2.3"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "0b491caff8f797fef4ba4b7f5514a7c5b915a481"
|
|
70
70
|
}
|