@canonical/react-ssr 0.4.0-experimental.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 +152 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/renderer/Extractor.js +63 -0
- package/dist/esm/renderer/Extractor.js.map +1 -0
- package/dist/esm/renderer/JSXRenderer.js +79 -0
- package/dist/esm/renderer/JSXRenderer.js.map +1 -0
- package/dist/esm/renderer/index.js +3 -0
- package/dist/esm/renderer/index.js.map +1 -0
- package/dist/esm/server/index.js +2 -0
- package/dist/esm/server/index.js.map +1 -0
- package/dist/esm/server/serve-express.js +47 -0
- package/dist/esm/server/serve-express.js.map +1 -0
- package/dist/esm/server/serve.js +41 -0
- package/dist/esm/server/serve.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/renderer/Extractor.d.ts +15 -0
- package/dist/types/renderer/Extractor.d.ts.map +1 -0
- package/dist/types/renderer/JSXRenderer.d.ts +51 -0
- package/dist/types/renderer/JSXRenderer.d.ts.map +1 -0
- package/dist/types/renderer/index.d.ts +4 -0
- package/dist/types/renderer/index.d.ts.map +1 -0
- package/dist/types/server/index.d.ts +2 -0
- package/dist/types/server/index.d.ts.map +1 -0
- package/dist/types/server/serve-express.d.ts +3 -0
- package/dist/types/server/serve-express.d.ts.map +1 -0
- package/dist/types/server/serve.d.ts +30 -0
- package/dist/types/server/serve.d.ts.map +1 -0
- package/package.json +60 -0
package/README.md
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
# Canonical React SSR: No-Hassle Server-Side Rendering for React
|
2
|
+
|
3
|
+
This guide demonstrates how to set up server-side rendering (SSR) for React applications using `@canonical/react-ssr`. It covers everything from installation to building and handling SSR requests with client and server entry points.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
1. [Installation](#installation)
|
7
|
+
2. [Quick Start](#quick-start)
|
8
|
+
- [Server-Side Entry Point](#server-side-entry-point)
|
9
|
+
- [Client-Side Entry Point](#client-side-entry-point)
|
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)
|
15
|
+
|
16
|
+
---
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
First, install the `@canonical/react-ssr` package:
|
21
|
+
|
22
|
+
```bash
|
23
|
+
npm install @canonical/react-ssr
|
24
|
+
```
|
25
|
+
|
26
|
+
## Quick Start
|
27
|
+
|
28
|
+
This section walks you through setting up SSR for your React app, including creating entry points, building your app, and handling SSR requests.
|
29
|
+
|
30
|
+
### Entrypoints
|
31
|
+
|
32
|
+
You will notice that this setup encourages two entry points: one for the server, and one for the client.
|
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.
|
35
|
+
|
36
|
+
### Server-Side Entry Point
|
37
|
+
|
38
|
+
Create a server-side entry point to wrap your application and inject the necessary scripts and links into the HTML.
|
39
|
+
|
40
|
+
```tsx
|
41
|
+
// src/ssr/entry-server.tsx
|
42
|
+
import Application from "../Application.js";
|
43
|
+
import React from "react";
|
44
|
+
import type {ReactServerEntrypointComponent, RendererServerEntrypointProps} from "@canonical/react-ssr/renderer";
|
45
|
+
|
46
|
+
// Define your server-side entry point component
|
47
|
+
const EntryServer: ReactServerEntrypointComponent<RendererServerEntrypointProps> = ({ lang = "en", scriptTags, linkTags }) => (
|
48
|
+
<html lang={lang}>
|
49
|
+
<head>
|
50
|
+
<title>Canonical React SSR</title>
|
51
|
+
{scriptTags}
|
52
|
+
{linkTags}
|
53
|
+
</head>
|
54
|
+
<body>
|
55
|
+
<div id="root">
|
56
|
+
<Application />
|
57
|
+
</div>
|
58
|
+
</body>
|
59
|
+
</html>
|
60
|
+
);
|
61
|
+
|
62
|
+
export default EntryServer;
|
63
|
+
```
|
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
|
+
|
66
|
+
### Client-Side Entry Point
|
67
|
+
Set up client-side hydration to rehydrate your app after the SSR content has been rendered.
|
68
|
+
```tsx
|
69
|
+
// src/ssr/entry-client.tsx
|
70
|
+
import { hydrateRoot } from "react-dom/client";
|
71
|
+
import Application from "../Application.js";
|
72
|
+
|
73
|
+
// Hydrate the client-side React app after the server-rendered HTML is loaded
|
74
|
+
hydrateRoot(document.getElementById("root") as HTMLElement, <Application />);
|
75
|
+
```
|
76
|
+
|
77
|
+
### Building your application
|
78
|
+
To build your SSR React app, use a tool like Vite, Webpack, or Next.
|
79
|
+
The build process should include both client and server bundles. First, build the client-side app, then the server-side entry point.
|
80
|
+
The example below uses Vite.
|
81
|
+
|
82
|
+
```json5
|
83
|
+
// package.json
|
84
|
+
{
|
85
|
+
"scripts": {
|
86
|
+
"build": "bun run build:client && bun run build:server",
|
87
|
+
"build:client": "vite build --ssrManifest --outDir dist/client",
|
88
|
+
"build:server": "vite build --ssr src/ssr/server.ts --outDir dist/server"
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
```
|
93
|
+
|
94
|
+
### Server Request Handling
|
95
|
+
|
96
|
+
Once your app is built, you can set up an Express server to handle SSR requests.
|
97
|
+
See [this file](../../../apps/boilerplate-react-vite/src/ssr/server.ts) as an example.
|
98
|
+
|
99
|
+
### Injecting the Client Application
|
100
|
+
|
101
|
+
Your client-side entry point must be executed by the client upon page load to rehydrate the server-rendered app.
|
102
|
+
|
103
|
+
Example for injecting the client application into your HTML with Vite:
|
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.
|
132
|
+
|
133
|
+
```ts
|
134
|
+
import { JSXRenderer } from "@canonical/react-ssr/renderer";
|
135
|
+
// Pass custom bootstrap scripts to the renderer
|
136
|
+
const Renderer = new JSXRenderer(
|
137
|
+
EntryServer,
|
138
|
+
{
|
139
|
+
htmlString,
|
140
|
+
renderToPipeableStreamOptions: {
|
141
|
+
bootstrapModules: ["src/ssr/entry-client.tsx"] // Adds the client-side entry module to the page
|
142
|
+
}
|
143
|
+
}
|
144
|
+
);
|
145
|
+
```
|
146
|
+
|
147
|
+
The `JSXRenderer` also accepts `renderToPipeableStreamOptions`, which are passed to react-dom/server`'s `renderToPipeableStream()`.
|
148
|
+
|
149
|
+
For further information, refer to [React's `renderToPipeableStream()` documentation](https://react.dev/reference/react-dom/server/renderToPipeableStream#parameters).
|
150
|
+
|
151
|
+
|
152
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC"}
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import { casing } from "@canonical/utils";
|
2
|
+
import { parseDocument } from "htmlparser2";
|
3
|
+
import React from "react";
|
4
|
+
/**
|
5
|
+
* Parses an HTML string to extract and convert script and link tags to React.createElement calls.
|
6
|
+
*/
|
7
|
+
class Extractor {
|
8
|
+
// biome-ignore lint/suspicious/noExplicitAny: explicit any needed for the document type
|
9
|
+
document;
|
10
|
+
constructor(html) {
|
11
|
+
this.document = parseDocument(html);
|
12
|
+
}
|
13
|
+
getElementsByTagName(tagName) {
|
14
|
+
const elements = [];
|
15
|
+
const stack = [this.document];
|
16
|
+
while (stack.length) {
|
17
|
+
const node = stack.pop();
|
18
|
+
if (!node)
|
19
|
+
continue;
|
20
|
+
if (node.type === "tag" && node.name === tagName) {
|
21
|
+
elements.push(node);
|
22
|
+
}
|
23
|
+
// Check for script tags specifically
|
24
|
+
if (node.type === "script" && tagName === "script") {
|
25
|
+
elements.push(node);
|
26
|
+
}
|
27
|
+
if (node.children) {
|
28
|
+
stack.push(...node.children);
|
29
|
+
}
|
30
|
+
}
|
31
|
+
console.log(`Found ${elements.length} <${tagName}> tags`);
|
32
|
+
return elements;
|
33
|
+
}
|
34
|
+
convertKeyToReactKey(key) {
|
35
|
+
switch (key.toLowerCase()) {
|
36
|
+
case "class":
|
37
|
+
return "className";
|
38
|
+
case "for":
|
39
|
+
return "htmlFor";
|
40
|
+
case "crossorigin":
|
41
|
+
return "crossOrigin";
|
42
|
+
default:
|
43
|
+
return casing.toCamelCase(key);
|
44
|
+
}
|
45
|
+
}
|
46
|
+
convertToReactElement(element) {
|
47
|
+
const props = {};
|
48
|
+
for (const [key, value] of Object.entries(element.attribs)) {
|
49
|
+
props[this.convertKeyToReactKey(key)] = value;
|
50
|
+
}
|
51
|
+
return React.createElement(element.name, props);
|
52
|
+
}
|
53
|
+
getLinkTags() {
|
54
|
+
const linkElements = this.getElementsByTagName("link");
|
55
|
+
return linkElements.map(this.convertToReactElement, this);
|
56
|
+
}
|
57
|
+
getScriptTags() {
|
58
|
+
const scriptElements = this.getElementsByTagName("script");
|
59
|
+
return scriptElements.map(this.convertToReactElement, this);
|
60
|
+
}
|
61
|
+
}
|
62
|
+
export default Extractor;
|
63
|
+
//# sourceMappingURL=Extractor.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"Extractor.js","sourceRoot":"","sources":["../../../src/renderer/Extractor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B;;GAEG;AACH,MAAM,SAAS;IACb,wFAAwF;IACvE,QAAQ,CAAM;IAE/B,YAAY,IAAY;QACtB,IAAI,CAAC,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;IAEO,oBAAoB,CAAC,OAAe;QAC1C,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAE9B,OAAO,KAAK,CAAC,MAAM,EAAE,CAAC;YACpB,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC;YACzB,IAAI,CAAC,IAAI;gBAAE,SAAS;YAEpB,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;gBACjD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtB,CAAC;YAED,qCAAqC;YACrC,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;gBACnD,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtB,CAAC;YAED,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAClB,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,SAAS,QAAQ,CAAC,MAAM,KAAK,OAAO,QAAQ,CAAC,CAAC;QAC1D,OAAO,QAAQ,CAAC;IAClB,CAAC;IAEO,oBAAoB,CAAC,GAAW;QACtC,QAAQ,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC;YAC1B,KAAK,OAAO;gBACV,OAAO,WAAW,CAAC;YACrB,KAAK,KAAK;gBACR,OAAO,SAAS,CAAC;YACnB,KAAK,aAAa;gBAChB,OAAO,aAAa,CAAC;YACvB;gBACE,OAAO,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IAEO,qBAAqB,CAAC,OAAgB;QAC5C,MAAM,KAAK,GAA8B,EAAE,CAAC;QAE5C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAC3D,KAAK,CAAC,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC;QAChD,CAAC;QAED,OAAO,KAAK,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAClD,CAAC;IAEM,WAAW;QAChB,MAAM,YAAY,GAAG,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC;QACvD,OAAO,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,qBAAqB,EAAE,IAAI,CAAC,CAAC;IAC5D,CAAC;IAEM,aAAa;QAClB,MAAM,cAAc,GAAG,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC;QAC3D,OAAO,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,qBAAqB,EAAE,IAAI,CAAC,CAAC;IAC9D,CAAC;CACF;AAED,eAAe,SAAS,CAAC"}
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import { createElement } from "react";
|
2
|
+
import { renderToPipeableStream, } from "react-dom/server";
|
3
|
+
import Extractor from "./Extractor.js";
|
4
|
+
// This class is responsible for rendering a React component to a readable stream.
|
5
|
+
export default class Renderer {
|
6
|
+
Component;
|
7
|
+
options;
|
8
|
+
locale;
|
9
|
+
// private messages: any;
|
10
|
+
extractor;
|
11
|
+
constructor(Component, options = {}) {
|
12
|
+
this.Component = Component;
|
13
|
+
this.options = options;
|
14
|
+
// this.prepareLocale = this.prepareLocale.bind(this);
|
15
|
+
this.render = this.render.bind(this);
|
16
|
+
this.extractor = this.options.htmlString
|
17
|
+
? new Extractor(this.options.htmlString)
|
18
|
+
: undefined;
|
19
|
+
}
|
20
|
+
//async prepareLocale(header: string | undefined) {
|
21
|
+
// if (this.options.loadMessages) {
|
22
|
+
// this.locale = header
|
23
|
+
// ? header.slice(0, 2)
|
24
|
+
// : this.options.defaultLocale || "en";
|
25
|
+
// //this.messages = await this.options.loadMessages(this.locale);
|
26
|
+
// }
|
27
|
+
//}
|
28
|
+
/**
|
29
|
+
* Gets the props needed to render the component
|
30
|
+
* @return The props needed to render the component
|
31
|
+
* @private
|
32
|
+
*/
|
33
|
+
getComponentProps() {
|
34
|
+
return {
|
35
|
+
lang: this.locale,
|
36
|
+
scriptTags: this.extractor?.getScriptTags(),
|
37
|
+
linkTags: this.extractor?.getLinkTags(),
|
38
|
+
// todo implement message passing
|
39
|
+
// messages: this.messages,
|
40
|
+
};
|
41
|
+
}
|
42
|
+
/**
|
43
|
+
* Renders this renderer's JSX component as a transmittable stream and sends it to the client
|
44
|
+
* TODO add a render function for ReadableStream, and rename this to be focused on PipeableStream
|
45
|
+
* @param req Client's request
|
46
|
+
* @param res Response object that will be sent to the client
|
47
|
+
* @return {RenderResult} The stream that was sent to the client
|
48
|
+
*/
|
49
|
+
render = (req, res) => {
|
50
|
+
// await this.prepareLocale(req.headers.get("accept-language") || undefined);
|
51
|
+
const jsx = createElement(this.Component, this.getComponentProps());
|
52
|
+
let renderingError;
|
53
|
+
const jsxStream = renderToPipeableStream(jsx, {
|
54
|
+
...this.options.renderToPipeableStreamOptions,
|
55
|
+
// Early error, before the shell is prepared
|
56
|
+
onShellError() {
|
57
|
+
res
|
58
|
+
.writeHead(500, { "Content-Type": "text/html; charset=utf-8" })
|
59
|
+
.end("<h1>Something went wrong</h1>");
|
60
|
+
throw new Error("An error occurred while constructing the SSR shell");
|
61
|
+
},
|
62
|
+
onShellReady() {
|
63
|
+
res.writeHead(renderingError ? 500 : 200, {
|
64
|
+
"Content-Type": "text/html; charset=utf-8",
|
65
|
+
});
|
66
|
+
jsxStream.pipe(res);
|
67
|
+
res.on("finish", () => {
|
68
|
+
res.end();
|
69
|
+
});
|
70
|
+
},
|
71
|
+
// Error occurred during rendering, after the shell & headers were sent - store the error for usage after stream is sent
|
72
|
+
onError(error) {
|
73
|
+
renderingError = error;
|
74
|
+
},
|
75
|
+
});
|
76
|
+
return jsxStream;
|
77
|
+
};
|
78
|
+
}
|
79
|
+
//# sourceMappingURL=JSXRenderer.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"JSXRenderer.js","sourceRoot":"","sources":["../../../src/renderer/JSXRenderer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAqB,aAAa,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAGL,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,SAAS,MAAM,gBAAgB,CAAC;AAyCvC,kFAAkF;AAClF,MAAM,CAAC,OAAO,OAAO,QAAQ;IAUR;IACA;IANX,MAAM,CAAqB;IACnC,yBAAyB;IACjB,SAAS,CAAwB;IAEzC,YACmB,SAAqB,EACrB,UAA2B,EAAE;QAD7B,cAAS,GAAT,SAAS,CAAY;QACrB,YAAO,GAAP,OAAO,CAAsB;QAE9C,sDAAsD;QACtD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU;YACtC,CAAC,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;YACxC,CAAC,CAAC,SAAS,CAAC;IAChB,CAAC;IAED,mDAAmD;IACnD,oCAAoC;IACpC,0BAA0B;IAC1B,4BAA4B;IAC5B,6CAA6C;IAC7C,qEAAqE;IACrE,KAAK;IACL,GAAG;IAEH;;;;OAIG;IACK,iBAAiB;QACvB,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,MAAM;YACjB,UAAU,EAAE,IAAI,CAAC,SAAS,EAAE,aAAa,EAAE;YAC3C,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,WAAW,EAAE;YACvC,iCAAiC;YACjC,2BAA2B;SACT,CAAC;IACvB,CAAC;IAED;;;;;;OAMG;IACH,MAAM,GAAkB,CACtB,GAAoB,EACpB,GAAmB,EACL,EAAE;QAChB,6EAA6E;QAC7E,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,iBAAiB,EAAE,CAAC,CAAC;QAEpE,IAAI,cAAqB,CAAC;QAE1B,MAAM,SAAS,GAAG,sBAAsB,CAAC,GAAG,EAAE;YAC5C,GAAG,IAAI,CAAC,OAAO,CAAC,6BAA6B;YAC7C,4CAA4C;YAC5C,YAAY;gBACV,GAAG;qBACA,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,0BAA0B,EAAE,CAAC;qBAC9D,GAAG,CAAC,+BAA+B,CAAC,CAAC;gBAExC,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;YACxE,CAAC;YACD,YAAY;gBACV,GAAG,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE;oBACxC,cAAc,EAAE,0BAA0B;iBAC3C,CAAC,CAAC;gBAEH,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAEpB,GAAG,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;oBACpB,GAAG,CAAC,GAAG,EAAE,CAAC;gBACZ,CAAC,CAAC,CAAC;YACL,CAAC;YACD,wHAAwH;YACxH,OAAO,CAAC,KAAK;gBACX,cAAc,GAAG,KAAc,CAAC;YAClC,CAAC;SACF,CAAC,CAAC;QACH,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;CACH"}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/renderer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAQ1D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
import path from "node:path";
|
3
|
+
import { parseArgs } from "node:util";
|
4
|
+
import express from "express";
|
5
|
+
import { serveStream } from "./serve.js";
|
6
|
+
const { values, positionals } = parseArgs({
|
7
|
+
args: process.argv.slice(2),
|
8
|
+
options: {
|
9
|
+
port: {
|
10
|
+
type: "string",
|
11
|
+
alias: "p",
|
12
|
+
default: "5173",
|
13
|
+
},
|
14
|
+
staticFilepath: {
|
15
|
+
type: "string",
|
16
|
+
alias: "s",
|
17
|
+
default: "dist/client/assets",
|
18
|
+
},
|
19
|
+
staticRoute: {
|
20
|
+
type: "string",
|
21
|
+
alias: "r",
|
22
|
+
default: "assets",
|
23
|
+
},
|
24
|
+
},
|
25
|
+
strict: true,
|
26
|
+
allowPositionals: true,
|
27
|
+
});
|
28
|
+
const port = values.port || 5173;
|
29
|
+
const cwd = process.cwd();
|
30
|
+
const rendererFilePath = path.join(cwd, positionals[0]);
|
31
|
+
const staticDir = path.join(cwd, values.staticFilepath || "assets");
|
32
|
+
const staticRoute = values.staticRoute || "assets";
|
33
|
+
if (!rendererFilePath) {
|
34
|
+
console.error("Usage: node server.js <renderer-path>");
|
35
|
+
process.exit(1);
|
36
|
+
}
|
37
|
+
const handler = await import(rendererFilePath).then((module) => module.default);
|
38
|
+
if (typeof handler !== "function") {
|
39
|
+
throw new Error("Renderer file must default-export a renderer function.");
|
40
|
+
}
|
41
|
+
const app = express();
|
42
|
+
app.use(`/${staticRoute}`, express.static(staticDir));
|
43
|
+
app.use(serveStream(handler));
|
44
|
+
app.listen(port, () => {
|
45
|
+
console.log(`Server started on http://localhost:${port}/`);
|
46
|
+
});
|
47
|
+
//# sourceMappingURL=serve-express.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"serve-express.js","sourceRoot":"","sources":["../../../src/server/serve-express.ts"],"names":[],"mappings":";AAEA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,OAAO,MAAM,SAAS,CAAC;AAE9B,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,SAAS,CAAC;IACxC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3B,OAAO,EAAE;QACP,IAAI,EAAE;YACJ,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,GAAG;YACV,OAAO,EAAE,MAAM;SAChB;QACD,cAAc,EAAE;YACd,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,GAAG;YACV,OAAO,EAAE,oBAAoB;SAC9B;QACD,WAAW,EAAE;YACX,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,GAAG;YACV,OAAO,EAAE,QAAQ;SAClB;KACF;IACD,MAAM,EAAE,IAAI;IACZ,gBAAgB,EAAE,IAAI;CACvB,CAAC,CAAC;AAEH,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC;AACjC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;AAC1B,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;AACxD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,cAAc,IAAI,QAAQ,CAAC,CAAC;AACpE,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,QAAQ,CAAC;AAEnD,IAAI,CAAC,gBAAgB,EAAE,CAAC;IACtB,OAAO,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IACvD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,OAAO,GAAkB,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAChE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAC3B,CAAC;AAEF,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;IAClC,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;AAEtB,GAAG,CAAC,GAAG,CAAC,IAAI,WAAW,EAAE,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;AACtD,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;AAE9B,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC,sCAAsC,IAAI,GAAG,CAAC,CAAC;AAC7D,CAAC,CAAC,CAAC"}
|
@@ -0,0 +1,41 @@
|
|
1
|
+
/**
|
2
|
+
* Execute a request handler that renders a React component to a stream, then streams it back to the client.
|
3
|
+
* This function should be used as a request controller in an HTTP server.
|
4
|
+
* @param handler A request handler that renders a React component to a stream.
|
5
|
+
* @throws Error if the handler throws an error during rendering.
|
6
|
+
* @returns The stream generated by the handler
|
7
|
+
* @example ```ts
|
8
|
+
* import express from "express";
|
9
|
+
* import { serveStream } from "@canonical/react-ssr/server";
|
10
|
+
* import { JSXRenderer } from "@canonical/react-ssr/renderer";
|
11
|
+
* // htmlString is created by some build process that bundles the client code
|
12
|
+
* import htmlString from "../../dist/client/index.html?raw";
|
13
|
+
* import EntryServer from "./entry-server.js";
|
14
|
+
*
|
15
|
+
* // `EntryServer` is an instance of `@canonical/react-ssr/renderer/ReactServerEntrypointComponent`
|
16
|
+
* const Renderer = new JSXRenderer(EntryServer, {
|
17
|
+
* htmlString,
|
18
|
+
* });
|
19
|
+
*
|
20
|
+
* const ssrHandler = Renderer.render;
|
21
|
+
*
|
22
|
+
* const app = express();
|
23
|
+
*
|
24
|
+
* app.use("/(assets|public)", express.static("dist/client/assets"));
|
25
|
+
* app.use(serveStream(ssrHandler));
|
26
|
+
*/
|
27
|
+
export function serveStream(handler) {
|
28
|
+
return (req, res) => {
|
29
|
+
try {
|
30
|
+
res.setHeader("Content-Type", "text/html");
|
31
|
+
res.setHeader("Transfer-Encoding", "chunked");
|
32
|
+
handler(req, res);
|
33
|
+
}
|
34
|
+
catch (error) {
|
35
|
+
console.error("Error during rendering:", error);
|
36
|
+
res.statusCode = 500;
|
37
|
+
res.end("Internal server error");
|
38
|
+
}
|
39
|
+
};
|
40
|
+
}
|
41
|
+
//# sourceMappingURL=serve.js.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../../../src/server/serve.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,WAAW,CAAC,OAAsB;IAChD,OAAO,CAAC,GAAoB,EAAE,GAAmB,EAAE,EAAE;QACnD,IAAI,CAAC;YACH,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;YAC3C,GAAG,CAAC,SAAS,CAAC,mBAAmB,EAAE,SAAS,CAAC,CAAC;YAC9C,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACpB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,KAAK,CAAC,CAAC;YAChD,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;YACrB,GAAG,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;QACnC,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC"}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import React from "react";
|
2
|
+
/**
|
3
|
+
* Parses an HTML string to extract and convert script and link tags to React.createElement calls.
|
4
|
+
*/
|
5
|
+
declare class Extractor {
|
6
|
+
private readonly document;
|
7
|
+
constructor(html: string);
|
8
|
+
private getElementsByTagName;
|
9
|
+
private convertKeyToReactKey;
|
10
|
+
private convertToReactElement;
|
11
|
+
getLinkTags(): React.ReactElement[];
|
12
|
+
getScriptTags(): React.ReactElement[];
|
13
|
+
}
|
14
|
+
export default Extractor;
|
15
|
+
//# sourceMappingURL=Extractor.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"Extractor.d.ts","sourceRoot":"","sources":["../../../src/renderer/Extractor.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B;;GAEG;AACH,cAAM,SAAS;IAEb,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAM;gBAEnB,IAAI,EAAE,MAAM;IAIxB,OAAO,CAAC,oBAAoB;IA0B5B,OAAO,CAAC,oBAAoB;IAa5B,OAAO,CAAC,qBAAqB;IAUtB,WAAW,IAAI,KAAK,CAAC,YAAY,EAAE;IAKnC,aAAa,IAAI,KAAK,CAAC,YAAY,EAAE;CAI7C;AAED,eAAe,SAAS,CAAC"}
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
2
|
+
import type * as React from "react";
|
3
|
+
import { type PipeableStream, type RenderToPipeableStreamOptions } from "react-dom/server";
|
4
|
+
export interface RendererOptions {
|
5
|
+
defaultLocale?: string;
|
6
|
+
loadMessages?: (locale: string) => string;
|
7
|
+
/** The HTML string to extract the script and link tags from */
|
8
|
+
htmlString?: string;
|
9
|
+
/**
|
10
|
+
* Options to pass to `react-dom/server.renderToPipeableStream`
|
11
|
+
* We specifically exclude `onShellReady()`, `onError()`, and `onShellError()` as they are implemented by `JSXRenderer.render().`
|
12
|
+
* See https://react.dev/reference/react-dom/server/renderToPipeableStream#parameters
|
13
|
+
*/
|
14
|
+
renderToPipeableStreamOptions?: Omit<RenderToPipeableStreamOptions, "onShellReady" | "onError" | "onShellError">;
|
15
|
+
}
|
16
|
+
/** The props that the server entrypoint component will receive */
|
17
|
+
export interface RendererServerEntrypointProps {
|
18
|
+
/** The language of the page. This is typically read from the request headers. */
|
19
|
+
lang?: string;
|
20
|
+
/** The script tags to include in the page */
|
21
|
+
scriptTags?: string;
|
22
|
+
/** The link tags to include in the page */
|
23
|
+
linkTags?: string;
|
24
|
+
}
|
25
|
+
/** The result of rendering a React component */
|
26
|
+
export type RenderResult = PipeableStream;
|
27
|
+
/** A function that renders a React component */
|
28
|
+
export type RenderHandler = (req: IncomingMessage, res: ServerResponse) => RenderResult;
|
29
|
+
export type ReactServerEntrypointComponent<TComponentProps extends RendererServerEntrypointProps> = React.ComponentType<TComponentProps>;
|
30
|
+
export default class Renderer<TComponent extends ReactServerEntrypointComponent<TComponentProps>, TComponentProps extends RendererServerEntrypointProps = RendererServerEntrypointProps> {
|
31
|
+
private readonly Component;
|
32
|
+
private readonly options;
|
33
|
+
private locale;
|
34
|
+
private extractor;
|
35
|
+
constructor(Component: TComponent, options?: RendererOptions);
|
36
|
+
/**
|
37
|
+
* Gets the props needed to render the component
|
38
|
+
* @return The props needed to render the component
|
39
|
+
* @private
|
40
|
+
*/
|
41
|
+
private getComponentProps;
|
42
|
+
/**
|
43
|
+
* Renders this renderer's JSX component as a transmittable stream and sends it to the client
|
44
|
+
* TODO add a render function for ReadableStream, and rename this to be focused on PipeableStream
|
45
|
+
* @param req Client's request
|
46
|
+
* @param res Response object that will be sent to the client
|
47
|
+
* @return {RenderResult} The stream that was sent to the client
|
48
|
+
*/
|
49
|
+
render: RenderHandler;
|
50
|
+
}
|
51
|
+
//# sourceMappingURL=JSXRenderer.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"JSXRenderer.d.ts","sourceRoot":"","sources":["../../../src/renderer/JSXRenderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AAEpC,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,6BAA6B,EAEnC,MAAM,kBAAkB,CAAC;AAG1B,MAAM,WAAW,eAAe;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1C,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,6BAA6B,CAAC,EAAE,IAAI,CAClC,6BAA6B,EAC7B,cAAc,GAAG,SAAS,GAAG,cAAc,CAC5C,CAAC;CACH;AAED,kEAAkE;AAClE,MAAM,WAAW,6BAA6B;IAC5C,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,gDAAgD;AAChD,MAAM,MAAM,YAAY,GAAG,cAAc,CAAC;AAC1C,gDAAgD;AAChD,MAAM,MAAM,aAAa,GAAG,CAC1B,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,KAChB,YAAY,CAAC;AAElB,MAAM,MAAM,8BAA8B,CACxC,eAAe,SAAS,6BAA6B,IACnD,KAAK,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC;AAGzC,MAAM,CAAC,OAAO,OAAO,QAAQ,CAC3B,UAAU,SAAS,8BAA8B,CAAC,eAAe,CAAC,EAClE,eAAe,SACb,6BAA6B,GAAG,6BAA6B;IAO7D,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,OAAO,CAAC,MAAM,CAAqB;IAEnC,OAAO,CAAC,SAAS,CAAwB;gBAGtB,SAAS,EAAE,UAAU,EACrB,OAAO,GAAE,eAAoB;IAkBhD;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAUzB;;;;;;OAMG;IACH,MAAM,EAAE,aAAa,CAoCnB;CACH"}
|
@@ -0,0 +1,4 @@
|
|
1
|
+
export { default as JSXRenderer } from "./JSXRenderer.js";
|
2
|
+
export type { RenderHandler, RenderResult, RendererServerEntrypointProps, ReactServerEntrypointComponent, RendererOptions, } from "./JSXRenderer.js";
|
3
|
+
export { default as Extractor } from "./Extractor.js";
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/renderer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC1D,YAAY,EACV,aAAa,EACb,YAAY,EACZ,6BAA6B,EAC7B,8BAA8B,EAC9B,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"serve-express.d.ts","sourceRoot":"","sources":["../../../src/server/serve-express.ts"],"names":[],"mappings":""}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
2
|
+
import type { RenderHandler } from "../renderer/index.js";
|
3
|
+
/**
|
4
|
+
* Execute a request handler that renders a React component to a stream, then streams it back to the client.
|
5
|
+
* This function should be used as a request controller in an HTTP server.
|
6
|
+
* @param handler A request handler that renders a React component to a stream.
|
7
|
+
* @throws Error if the handler throws an error during rendering.
|
8
|
+
* @returns The stream generated by the handler
|
9
|
+
* @example ```ts
|
10
|
+
* import express from "express";
|
11
|
+
* import { serveStream } from "@canonical/react-ssr/server";
|
12
|
+
* import { JSXRenderer } from "@canonical/react-ssr/renderer";
|
13
|
+
* // htmlString is created by some build process that bundles the client code
|
14
|
+
* import htmlString from "../../dist/client/index.html?raw";
|
15
|
+
* import EntryServer from "./entry-server.js";
|
16
|
+
*
|
17
|
+
* // `EntryServer` is an instance of `@canonical/react-ssr/renderer/ReactServerEntrypointComponent`
|
18
|
+
* const Renderer = new JSXRenderer(EntryServer, {
|
19
|
+
* htmlString,
|
20
|
+
* });
|
21
|
+
*
|
22
|
+
* const ssrHandler = Renderer.render;
|
23
|
+
*
|
24
|
+
* const app = express();
|
25
|
+
*
|
26
|
+
* app.use("/(assets|public)", express.static("dist/client/assets"));
|
27
|
+
* app.use(serveStream(ssrHandler));
|
28
|
+
*/
|
29
|
+
export declare function serveStream(handler: RenderHandler): (req: IncomingMessage, res: ServerResponse) => void;
|
30
|
+
//# sourceMappingURL=serve.d.ts.map
|
@@ -0,0 +1 @@
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,SACnC,eAAe,OAAO,cAAc,UAWlD"}
|
package/package.json
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
{
|
2
|
+
"name": "@canonical/react-ssr",
|
3
|
+
"description": "TBD",
|
4
|
+
"version": "0.4.0-experimental.0",
|
5
|
+
"type": "module",
|
6
|
+
"module": "dist/esm/index.js",
|
7
|
+
"types": "dist/types/index.d.ts",
|
8
|
+
"files": ["dist"],
|
9
|
+
"author": {
|
10
|
+
"email": "webteam@canonical.com",
|
11
|
+
"name": "Canonical Webteam"
|
12
|
+
},
|
13
|
+
"repository": {
|
14
|
+
"type": "git",
|
15
|
+
"url": "https://github.com/canonical/ds25"
|
16
|
+
},
|
17
|
+
"license": "LGPL-3.0",
|
18
|
+
"bugs": {
|
19
|
+
"url": "https://github.com/canonical/ds25/issues"
|
20
|
+
},
|
21
|
+
"homepage": "https://github.com/canonical/ds25#readme",
|
22
|
+
"scripts": {
|
23
|
+
"build": "tsc -p tsconfig.build.json",
|
24
|
+
"check": "bun run check:biome && bun run check:ts",
|
25
|
+
"check:fix": "bun run check:biome:fix && bun run check:ts",
|
26
|
+
"check:biome": "biome check src *.json",
|
27
|
+
"check:biome:fix": "biome check --write src *.json",
|
28
|
+
"check:ts": "tsc --noEmit"
|
29
|
+
},
|
30
|
+
"exports": {
|
31
|
+
".": {
|
32
|
+
"import": "./dist/esm/index.js",
|
33
|
+
"types": "./dist/types/index.d.ts"
|
34
|
+
},
|
35
|
+
"./renderer": {
|
36
|
+
"import": "./dist/esm/renderer/index.js",
|
37
|
+
"types": "./dist/types/renderer/index.d.ts"
|
38
|
+
},
|
39
|
+
"./server": {
|
40
|
+
"import": "./dist/esm/server/index.js",
|
41
|
+
"types": "./dist/types/server/index.d.ts"
|
42
|
+
}
|
43
|
+
},
|
44
|
+
"devDependencies": {
|
45
|
+
"@biomejs/biome": "^1.9.4",
|
46
|
+
"@canonical/biome-config": "^0.4.0-experimental.0",
|
47
|
+
"@canonical/typescript-config-base": "^0.4.0-experimental.0",
|
48
|
+
"@types/express": "^5.0.0",
|
49
|
+
"@types/react": "^19.0.1",
|
50
|
+
"@types/react-dom": "^19.0.2",
|
51
|
+
"typescript": "^5.7.2"
|
52
|
+
},
|
53
|
+
"dependencies": {
|
54
|
+
"@canonical/utils": "^0.4.0-experimental.0",
|
55
|
+
"domhandler": "^5.0.3",
|
56
|
+
"express": "^4.21.2",
|
57
|
+
"htmlparser2": "^9.1.0",
|
58
|
+
"react-dom": "^19.0.0"
|
59
|
+
}
|
60
|
+
}
|