@canonical/react-ssr 0.13.0 → 0.15.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 +29 -5
- package/dist/esm/renderer/Extractor.js +85 -21
- package/dist/esm/renderer/Extractor.js.map +1 -1
- package/dist/esm/renderer/JSXRenderer.js +129 -47
- package/dist/esm/renderer/JSXRenderer.js.map +1 -1
- package/dist/esm/renderer/constants.js +2 -0
- package/dist/esm/renderer/constants.js.map +1 -0
- package/dist/esm/renderer/index.js +1 -0
- package/dist/esm/renderer/index.js.map +1 -1
- package/dist/esm/renderer/types.js +2 -0
- package/dist/esm/renderer/types.js.map +1 -0
- package/dist/esm/server/serve-express.js +12 -1
- package/dist/esm/server/serve-express.js.map +1 -1
- package/dist/esm/server/serve.js +1 -1
- package/dist/types/renderer/Extractor.d.ts +60 -7
- package/dist/types/renderer/Extractor.d.ts.map +1 -1
- package/dist/types/renderer/JSXRenderer.d.ts +63 -43
- package/dist/types/renderer/JSXRenderer.d.ts.map +1 -1
- package/dist/types/renderer/constants.d.ts +2 -0
- package/dist/types/renderer/constants.d.ts.map +1 -0
- package/dist/types/renderer/index.d.ts +2 -1
- package/dist/types/renderer/index.d.ts.map +1 -1
- package/dist/types/renderer/types.d.ts +37 -0
- package/dist/types/renderer/types.d.ts.map +1 -0
- package/dist/types/server/serve.d.ts +1 -1
- package/package.json +10 -6
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @canonical/react-ssr
|
|
2
2
|
|
|
3
|
-
Server-side rendering utilities for React applications. Provides streaming HTML rendering
|
|
3
|
+
Server-side rendering utilities for React applications. Provides streaming HTML rendering, Express middleware with `serveStream`,
|
|
4
|
+
and automatic script/link tag injection from your build output.
|
|
4
5
|
|
|
5
6
|
## Installation
|
|
6
7
|
|
|
@@ -10,6 +11,28 @@ bun add @canonical/react-ssr
|
|
|
10
11
|
|
|
11
12
|
Peer dependencies: `react`, `react-dom`, `express` (for Express usage).
|
|
12
13
|
|
|
14
|
+
## SSR flavors
|
|
15
|
+
|
|
16
|
+
There are usually two different flavors of SSR to be considered.
|
|
17
|
+
|
|
18
|
+
Source: [Tanstack Router SSR Guide](https://tanstack.com/router/latest/docs/framework/react/guide/ssr)
|
|
19
|
+
|
|
20
|
+
### Non-streaming SSR
|
|
21
|
+
|
|
22
|
+
The entire page is rendered on the server and sent to the client in one single HTML request,
|
|
23
|
+
including the serialized data the application needs to hydrate on the client.
|
|
24
|
+
|
|
25
|
+
This is what `JSXRenderer.renderToString()` offers.
|
|
26
|
+
|
|
27
|
+
### Streaming SSR
|
|
28
|
+
|
|
29
|
+
The critical first paint of the page is rendered on the server and sent to the client in one single HTML request,
|
|
30
|
+
including the serialized data the application needs to hydrate on the client.
|
|
31
|
+
|
|
32
|
+
The rest of the page is then streamed to the client as it is rendered on the server.
|
|
33
|
+
|
|
34
|
+
This is accomplished by using `JSXRenderer.renderToStream()`.
|
|
35
|
+
|
|
13
36
|
## Express Server
|
|
14
37
|
|
|
15
38
|
Create a renderer that wraps your server entry component:
|
|
@@ -20,7 +43,8 @@ import { JSXRenderer } from "@canonical/react-ssr/renderer";
|
|
|
20
43
|
import htmlString from "../../dist/client/index.html?raw";
|
|
21
44
|
import EntryServer from "./entry-server.js";
|
|
22
45
|
|
|
23
|
-
const
|
|
46
|
+
const initialData: Record<string, unknown> = {};
|
|
47
|
+
const Renderer = new JSXRenderer(EntryServer, initialData, { htmlString });
|
|
24
48
|
export default Renderer.render;
|
|
25
49
|
```
|
|
26
50
|
|
|
@@ -156,12 +180,12 @@ The client build produces `dist/client/index.html` with bundled script/link tags
|
|
|
156
180
|
Pass options to React's `renderToPipeableStream`:
|
|
157
181
|
|
|
158
182
|
```ts
|
|
159
|
-
const
|
|
183
|
+
const initialData: Record<string, unknown> = {};
|
|
184
|
+
const Renderer = new JSXRenderer(EntryServer, initialData, {
|
|
160
185
|
htmlString,
|
|
186
|
+
initialData,
|
|
161
187
|
renderToPipeableStreamOptions: {
|
|
162
188
|
bootstrapModules: ["src/ssr/entry-client.tsx"],
|
|
163
|
-
onShellReady() { console.log("Shell ready"); },
|
|
164
|
-
onError(err) { console.error(err); },
|
|
165
189
|
},
|
|
166
190
|
});
|
|
167
191
|
```
|
|
@@ -1,18 +1,44 @@
|
|
|
1
1
|
import { casing } from "@canonical/utils";
|
|
2
|
+
import { NodeWithChildren } from "domhandler";
|
|
2
3
|
import { parseDocument } from "htmlparser2";
|
|
3
4
|
import React from "react";
|
|
5
|
+
const REACT_KEYS_DICTIONARY = {
|
|
6
|
+
class: "className",
|
|
7
|
+
for: "htmlFor",
|
|
8
|
+
crossorigin: "crossOrigin",
|
|
9
|
+
charset: "charSet",
|
|
10
|
+
};
|
|
4
11
|
/**
|
|
5
|
-
* Parses an HTML string to extract and convert
|
|
12
|
+
* Parses an HTML string to extract and convert the <head> tags to React.createElement calls.
|
|
13
|
+
* The tags extracted are:
|
|
14
|
+
* - title
|
|
15
|
+
* - style
|
|
16
|
+
* - meta
|
|
17
|
+
* - link
|
|
18
|
+
* - script
|
|
19
|
+
* - base
|
|
6
20
|
*/
|
|
7
21
|
class Extractor {
|
|
8
|
-
|
|
22
|
+
/**
|
|
23
|
+
* A document object representing the DOM of a page.
|
|
24
|
+
*/
|
|
9
25
|
document;
|
|
26
|
+
/**
|
|
27
|
+
* Creates an Extractor object for a given HTML string.
|
|
28
|
+
*/
|
|
10
29
|
constructor(html) {
|
|
11
30
|
this.document = parseDocument(html);
|
|
12
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Searches elements with the specified tag in the document.
|
|
34
|
+
*
|
|
35
|
+
* @remark The method uses the parsed {@link Extractor.document | document} to navigate the
|
|
36
|
+
* whole DOM (usinig a stack) and checks for the elements with the tag name that matches
|
|
37
|
+
* the given parameter.
|
|
38
|
+
*/
|
|
13
39
|
getElementsByTagName(tagName) {
|
|
14
40
|
const elements = [];
|
|
15
|
-
const stack = [this.document];
|
|
41
|
+
const stack = [...this.document.children];
|
|
16
42
|
while (stack.length) {
|
|
17
43
|
const node = stack.pop();
|
|
18
44
|
if (!node)
|
|
@@ -24,39 +50,77 @@ class Extractor {
|
|
|
24
50
|
if (node.type === "script" && tagName === "script") {
|
|
25
51
|
elements.push(node);
|
|
26
52
|
}
|
|
27
|
-
if (node
|
|
53
|
+
if (node instanceof NodeWithChildren) {
|
|
28
54
|
stack.push(...node.children);
|
|
29
55
|
}
|
|
30
56
|
}
|
|
31
|
-
console.log(`Found ${elements.length} <${tagName}> tags`);
|
|
32
57
|
return elements;
|
|
33
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Converts HTML keys to React keys.
|
|
61
|
+
*
|
|
62
|
+
* @remark There are some HTML attributes that don't map exactly to React with the same name.
|
|
63
|
+
* For example, class -> className.
|
|
64
|
+
*/
|
|
34
65
|
convertKeyToReactKey(key) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return "className";
|
|
38
|
-
case "for":
|
|
39
|
-
return "htmlFor";
|
|
40
|
-
case "crossorigin":
|
|
41
|
-
return "crossOrigin";
|
|
42
|
-
default:
|
|
43
|
-
return casing.toCamelCase(key);
|
|
44
|
-
}
|
|
66
|
+
const reactKey = REACT_KEYS_DICTIONARY[key.toLowerCase()];
|
|
67
|
+
return reactKey ? reactKey : casing.toCamelCase(key);
|
|
45
68
|
}
|
|
46
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Converts a parsed {@link domhandler#Element | DOM Element} into a {@link react#React.ReactElement | ReactElement}.
|
|
71
|
+
*
|
|
72
|
+
* @remark The method takes into account the attributes of the parsed {@link domhandler#Element | Element}
|
|
73
|
+
* and passes them as props when creating the {@link react#React.ReactElement | ReactElement}.
|
|
74
|
+
* It only handles children of type "text".
|
|
75
|
+
*/
|
|
76
|
+
convertToReactElement(element, index) {
|
|
47
77
|
const props = {};
|
|
48
78
|
for (const [key, value] of Object.entries(element.attribs)) {
|
|
49
79
|
props[this.convertKeyToReactKey(key)] = value;
|
|
50
80
|
}
|
|
51
|
-
|
|
81
|
+
// some tags from <head> have one children of type text
|
|
82
|
+
let elementChildren;
|
|
83
|
+
if (element.children.length === 1 && element.firstChild?.type === "text") {
|
|
84
|
+
elementChildren = element.firstChild.data;
|
|
85
|
+
}
|
|
86
|
+
props.key = `${element.name}_${index}`;
|
|
87
|
+
return React.createElement(element.name, props, elementChildren);
|
|
52
88
|
}
|
|
53
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Finds all <link> elements in the {@link Extractor.document | document} and converts them
|
|
91
|
+
* into {@link react#React.ReactElement | ReactElements}.
|
|
92
|
+
*
|
|
93
|
+
* @remark The list of elements returned will be in order of appearance in the DOM.
|
|
94
|
+
*/
|
|
95
|
+
getLinkElements() {
|
|
54
96
|
const linkElements = this.getElementsByTagName("link");
|
|
55
|
-
|
|
97
|
+
// reverse keeps the original order in the HTML (they are extracted with a stack in reverse)
|
|
98
|
+
// the order might be important for some scripts (i.e. in Vite Dev mode)
|
|
99
|
+
return linkElements.reverse().map(this.convertToReactElement, this);
|
|
56
100
|
}
|
|
57
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Finds all <script> elements in the {@link Extractor.document | document} and converts them
|
|
103
|
+
* into {@link react#React.ReactElement | ReactElements}.
|
|
104
|
+
*
|
|
105
|
+
* @remark The list of elements returned will be in order of appearance in the DOM.
|
|
106
|
+
*/
|
|
107
|
+
getScriptElements() {
|
|
58
108
|
const scriptElements = this.getElementsByTagName("script");
|
|
59
|
-
|
|
109
|
+
// reverse keeps the original order in the HTML (they are extracted with a stack in reverse)
|
|
110
|
+
// the order might be important for some scripts (i.e. in Vite Dev mode)
|
|
111
|
+
return scriptElements.reverse().map(this.convertToReactElement, this);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Finds all the <head> elements which are not "script" or "link" in the {@link Extractor.document | document}
|
|
115
|
+
* and converts them into {@link react#React.ReactElement | ReactElements}.
|
|
116
|
+
*
|
|
117
|
+
* @remark The list of elements returned will be in order of appearance in the DOM.
|
|
118
|
+
*/
|
|
119
|
+
getOtherHeadElements() {
|
|
120
|
+
const otherHeadElements = ["title", "style", "meta", "base"].flatMap((elementName) => this.getElementsByTagName(elementName));
|
|
121
|
+
// reverse keeps the original order in the HTML (they are extracted with a stack in reverse)
|
|
122
|
+
// the order might be important for some scripts (i.e. in Vite Dev mode)
|
|
123
|
+
return otherHeadElements.reverse().map(this.convertToReactElement, this);
|
|
60
124
|
}
|
|
61
125
|
}
|
|
62
126
|
export default Extractor;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Extractor.js","sourceRoot":"","sources":["../../../src/renderer/Extractor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"Extractor.js","sourceRoot":"","sources":["../../../src/renderer/Extractor.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,79 +1,161 @@
|
|
|
1
1
|
import { createElement } from "react";
|
|
2
|
-
import { renderToPipeableStream, } from "react-dom/server";
|
|
2
|
+
import { renderToPipeableStream, renderToString, } from "react-dom/server";
|
|
3
|
+
import { INITIAL_DATA_KEY } from "./constants.js";
|
|
3
4
|
import Extractor from "./Extractor.js";
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* This class is responsible for rendering a React JSX component and sending it as response to a client.
|
|
7
|
+
* It offers 2 ways of doing it:
|
|
8
|
+
* - As string
|
|
9
|
+
* - As stream
|
|
10
|
+
* Each way has its advantages and inconveniences. You can read more about them in the package README.
|
|
11
|
+
*/
|
|
12
|
+
export default class JSXRenderer {
|
|
6
13
|
Component;
|
|
14
|
+
initialData;
|
|
7
15
|
options;
|
|
8
|
-
locale;
|
|
9
|
-
// private messages: any;
|
|
10
16
|
extractor;
|
|
11
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Creates a renderer instance which can be used to write Server Side Rendered HTML
|
|
19
|
+
* into a {@link node:http#ServerResponse | ServerResponse}.
|
|
20
|
+
*/
|
|
21
|
+
constructor(Component, initialData = {}, options = {}) {
|
|
12
22
|
this.Component = Component;
|
|
23
|
+
this.initialData = initialData;
|
|
13
24
|
this.options = options;
|
|
14
|
-
// this.prepareLocale = this.prepareLocale.bind(this);
|
|
15
|
-
this.render = this.render.bind(this);
|
|
16
25
|
this.extractor = this.options.htmlString
|
|
17
26
|
? new Extractor(this.options.htmlString)
|
|
18
27
|
: undefined;
|
|
19
28
|
}
|
|
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
|
/**
|
|
29
|
-
* Gets the
|
|
30
|
-
*
|
|
31
|
-
|
|
30
|
+
* Gets the locale to be used for the rendered page.
|
|
31
|
+
* Default if there was no locale passed as option is "en".
|
|
32
|
+
*/
|
|
33
|
+
getLocale() {
|
|
34
|
+
return this.options.defaultLocale || "en";
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Gets the props needed to render the component.
|
|
32
38
|
*/
|
|
33
39
|
getComponentProps() {
|
|
34
40
|
return {
|
|
35
|
-
lang: this.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
lang: this.getLocale(),
|
|
42
|
+
scriptElements: this.extractor?.getScriptElements(),
|
|
43
|
+
linkElements: this.extractor?.getLinkElements(),
|
|
44
|
+
otherHeadElements: this.extractor?.getOtherHeadElements(),
|
|
45
|
+
initialData: this.initialData,
|
|
40
46
|
};
|
|
41
47
|
}
|
|
42
48
|
/**
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
49
|
+
* Gets a list of all the "src" attributes of the given scripts that match the passed type.
|
|
50
|
+
*/
|
|
51
|
+
getScriptSourcesByType(scripts, type) {
|
|
52
|
+
return (scripts
|
|
53
|
+
.map((script) => script)
|
|
54
|
+
.filter((script) => {
|
|
55
|
+
if (type === "module") {
|
|
56
|
+
return script.props.type === "module";
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
return script.props.type !== "module";
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.map((script) => script.props.src)
|
|
63
|
+
.filter((src) => typeof src === "string") || []);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Adds some properties to the options that are passed to {@link react-dom#renderToPipeableStream | renderToPipeableStream}.
|
|
67
|
+
*
|
|
68
|
+
* @remark The options that are added are:
|
|
69
|
+
* - bootstrapScriptContent: includes the initial data passed as prop to the component in a <script> so that it
|
|
70
|
+
* is available when rendering the page in the browser (to avoid hydration mismatches).
|
|
71
|
+
* - bootstrapScripts: classic scripts which react strips out of the page. The only way to add them is to include them
|
|
72
|
+
* in this property.
|
|
73
|
+
* - bootstrapModules: module scripts which react also strips out of the page and need to be added like this.
|
|
74
|
+
*/
|
|
75
|
+
enrichRendererOptions(props) {
|
|
76
|
+
const enrichedOptions = { ...this.options.renderToPipeableStreamOptions };
|
|
77
|
+
// options passed by the user always take priority
|
|
78
|
+
if (!enrichedOptions.bootstrapScriptContent) {
|
|
79
|
+
if (props.initialData) {
|
|
80
|
+
enrichedOptions.bootstrapScriptContent = `window.${INITIAL_DATA_KEY} = ${JSON.stringify(props.initialData)}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!enrichedOptions.bootstrapScripts) {
|
|
84
|
+
if (props.scriptElements) {
|
|
85
|
+
enrichedOptions.bootstrapScripts = this.getScriptSourcesByType(props.scriptElements, "classic");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!enrichedOptions.bootstrapModules) {
|
|
89
|
+
if (props.scriptElements) {
|
|
90
|
+
enrichedOptions.bootstrapModules = this.getScriptSourcesByType(props.scriptElements, "module");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return enrichedOptions;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* This function is responsible for rendering a React component and sending it to the client through
|
|
97
|
+
* a pipeable stream.
|
|
98
|
+
*
|
|
99
|
+
* @remark See the README to understand the difference between rendering options.
|
|
100
|
+
*
|
|
101
|
+
* The streaming might improve the time taken for the page to be rendered and interactive
|
|
102
|
+
* (at least in part), using React's Suspense/lazy API and pipeable streams.
|
|
103
|
+
*
|
|
104
|
+
* CAUTION: The resulting HTML rendered this way is not cacheable.
|
|
48
105
|
*/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
106
|
+
renderToStream = (_req, res) => {
|
|
107
|
+
const errorRef = { current: undefined };
|
|
108
|
+
const props = this.getComponentProps();
|
|
109
|
+
const jsx = createElement(this.Component, props);
|
|
53
110
|
const jsxStream = renderToPipeableStream(jsx, {
|
|
54
|
-
...this.
|
|
111
|
+
...this.enrichRendererOptions(props),
|
|
112
|
+
// Error occurred during rendering, after the shell & headers were sent - store the error for usage after stream is sent
|
|
113
|
+
onError(error) {
|
|
114
|
+
errorRef.current = error;
|
|
115
|
+
console.error(error);
|
|
116
|
+
},
|
|
55
117
|
// Early error, before the shell is prepared
|
|
56
|
-
onShellError() {
|
|
57
|
-
res
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
118
|
+
onShellError(error) {
|
|
119
|
+
if (!res.headersSent) {
|
|
120
|
+
res
|
|
121
|
+
.writeHead(500, { "Content-Type": "text/html; charset=utf-8" })
|
|
122
|
+
.end("<h1>Something went wrong</h1>");
|
|
123
|
+
}
|
|
124
|
+
console.error(error);
|
|
61
125
|
},
|
|
62
126
|
onShellReady() {
|
|
63
|
-
res.
|
|
64
|
-
|
|
65
|
-
|
|
127
|
+
if (!res.headersSent) {
|
|
128
|
+
res.writeHead(errorRef.current ? 500 : 200, {
|
|
129
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
66
132
|
jsxStream.pipe(res);
|
|
67
133
|
res.on("finish", () => {
|
|
68
134
|
res.end();
|
|
69
135
|
});
|
|
70
136
|
},
|
|
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
137
|
});
|
|
76
|
-
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Renders this renderer's JSX component as a string and writes it to the given
|
|
141
|
+
* {@link node:http#ServerResponse | ServerResponse}.
|
|
142
|
+
*
|
|
143
|
+
* @remark See the README to understand the difference between rendering options.
|
|
144
|
+
*
|
|
145
|
+
* Rendering to string means all <Suspense> components are loaded synchronously and the response
|
|
146
|
+
* won't be sent to the client until all components have finished loading data and processing.
|
|
147
|
+
*
|
|
148
|
+
* renderToString is useful in Vite Dev mode, as the HMR doesn't work well with Suspense
|
|
149
|
+
* and the Pipeable Stream rendering. Also if the resulting document needs to be cached.
|
|
150
|
+
*/
|
|
151
|
+
renderToString = (_req, res) => {
|
|
152
|
+
const props = this.getComponentProps();
|
|
153
|
+
const jsx = createElement(this.Component, props);
|
|
154
|
+
const html = renderToString(jsx);
|
|
155
|
+
res
|
|
156
|
+
.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
|
|
157
|
+
.write(html);
|
|
158
|
+
res.end();
|
|
77
159
|
};
|
|
78
160
|
}
|
|
79
161
|
//# sourceMappingURL=JSXRenderer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JSXRenderer.js","sourceRoot":"","sources":["../../../src/renderer/JSXRenderer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"JSXRenderer.js","sourceRoot":"","sources":["../../../src/renderer/JSXRenderer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../src/renderer/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/renderer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/renderer/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/renderer/types.ts"],"names":[],"mappings":""}
|
|
@@ -21,6 +21,10 @@ const { values, positionals } = parseArgs({
|
|
|
21
21
|
alias: "r",
|
|
22
22
|
default: "assets",
|
|
23
23
|
},
|
|
24
|
+
streaming: {
|
|
25
|
+
type: "boolean",
|
|
26
|
+
default: false,
|
|
27
|
+
},
|
|
24
28
|
},
|
|
25
29
|
strict: true,
|
|
26
30
|
allowPositionals: true,
|
|
@@ -34,7 +38,14 @@ if (!rendererFilePath) {
|
|
|
34
38
|
console.error("Usage: node server.js <renderer-path>");
|
|
35
39
|
process.exit(1);
|
|
36
40
|
}
|
|
37
|
-
const handler = await import(rendererFilePath).then((module) =>
|
|
41
|
+
const handler = await import(rendererFilePath).then((module) => {
|
|
42
|
+
if (values.streaming) {
|
|
43
|
+
return module.default.renderToStream;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
return module.default.renderToString;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
38
49
|
if (typeof handler !== "function") {
|
|
39
50
|
throw new Error("Renderer file must default-export a renderer function.");
|
|
40
51
|
}
|
|
@@ -1 +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,
|
|
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;QACD,SAAS,EAAE;YACT,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,KAAK;SACf;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,CAAC,CAAC,MAAM,EAAE,EAAE;IAC5E,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,OAAO,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC;IACvC,CAAC;SAAM,CAAC;QACN,OAAO,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC;IACvC,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,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"}
|
package/dist/esm/server/serve.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* import htmlString from "../../dist/client/index.html?raw";
|
|
13
13
|
* import EntryServer from "./entry-server.js";
|
|
14
14
|
*
|
|
15
|
-
* // `EntryServer` is an instance of `@canonical/react-ssr/renderer/
|
|
15
|
+
* // `EntryServer` is an instance of `@canonical/react-ssr/renderer/ServerEntrypoint`
|
|
16
16
|
* const Renderer = new JSXRenderer(EntryServer, {
|
|
17
17
|
* htmlString,
|
|
18
18
|
* });
|
|
@@ -1,15 +1,68 @@
|
|
|
1
|
+
import { type Document, type Element } from "domhandler";
|
|
1
2
|
import React from "react";
|
|
2
3
|
/**
|
|
3
|
-
* Parses an HTML string to extract and convert
|
|
4
|
+
* Parses an HTML string to extract and convert the <head> tags to React.createElement calls.
|
|
5
|
+
* The tags extracted are:
|
|
6
|
+
* - title
|
|
7
|
+
* - style
|
|
8
|
+
* - meta
|
|
9
|
+
* - link
|
|
10
|
+
* - script
|
|
11
|
+
* - base
|
|
4
12
|
*/
|
|
5
13
|
declare class Extractor {
|
|
6
|
-
|
|
14
|
+
/**
|
|
15
|
+
* A document object representing the DOM of a page.
|
|
16
|
+
*/
|
|
17
|
+
protected readonly document: Document;
|
|
18
|
+
/**
|
|
19
|
+
* Creates an Extractor object for a given HTML string.
|
|
20
|
+
*/
|
|
7
21
|
constructor(html: string);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Searches elements with the specified tag in the document.
|
|
24
|
+
*
|
|
25
|
+
* @remark The method uses the parsed {@link Extractor.document | document} to navigate the
|
|
26
|
+
* whole DOM (usinig a stack) and checks for the elements with the tag name that matches
|
|
27
|
+
* the given parameter.
|
|
28
|
+
*/
|
|
29
|
+
protected getElementsByTagName(tagName: string): Element[];
|
|
30
|
+
/**
|
|
31
|
+
* Converts HTML keys to React keys.
|
|
32
|
+
*
|
|
33
|
+
* @remark There are some HTML attributes that don't map exactly to React with the same name.
|
|
34
|
+
* For example, class -> className.
|
|
35
|
+
*/
|
|
36
|
+
protected convertKeyToReactKey(key: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Converts a parsed {@link domhandler#Element | DOM Element} into a {@link react#React.ReactElement | ReactElement}.
|
|
39
|
+
*
|
|
40
|
+
* @remark The method takes into account the attributes of the parsed {@link domhandler#Element | Element}
|
|
41
|
+
* and passes them as props when creating the {@link react#React.ReactElement | ReactElement}.
|
|
42
|
+
* It only handles children of type "text".
|
|
43
|
+
*/
|
|
44
|
+
protected convertToReactElement(element: Element, index: number): React.ReactElement;
|
|
45
|
+
/**
|
|
46
|
+
* Finds all <link> elements in the {@link Extractor.document | document} and converts them
|
|
47
|
+
* into {@link react#React.ReactElement | ReactElements}.
|
|
48
|
+
*
|
|
49
|
+
* @remark The list of elements returned will be in order of appearance in the DOM.
|
|
50
|
+
*/
|
|
51
|
+
getLinkElements(): React.ReactElement[];
|
|
52
|
+
/**
|
|
53
|
+
* Finds all <script> elements in the {@link Extractor.document | document} and converts them
|
|
54
|
+
* into {@link react#React.ReactElement | ReactElements}.
|
|
55
|
+
*
|
|
56
|
+
* @remark The list of elements returned will be in order of appearance in the DOM.
|
|
57
|
+
*/
|
|
58
|
+
getScriptElements(): React.ReactElement[];
|
|
59
|
+
/**
|
|
60
|
+
* Finds all the <head> elements which are not "script" or "link" in the {@link Extractor.document | document}
|
|
61
|
+
* and converts them into {@link react#React.ReactElement | ReactElements}.
|
|
62
|
+
*
|
|
63
|
+
* @remark The list of elements returned will be in order of appearance in the DOM.
|
|
64
|
+
*/
|
|
65
|
+
getOtherHeadElements(): React.ReactElement[];
|
|
13
66
|
}
|
|
14
67
|
export default Extractor;
|
|
15
68
|
//# sourceMappingURL=Extractor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Extractor.d.ts","sourceRoot":"","sources":["../../../src/renderer/Extractor.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Extractor.d.ts","sourceRoot":"","sources":["../../../src/renderer/Extractor.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,OAAO,EAAoB,MAAM,YAAY,CAAC;AAE3E,OAAO,KAAK,MAAM,OAAO,CAAC;AAS1B;;;;;;;;;GASG;AACH,cAAM,SAAS;IACb;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAEtC;;OAEG;gBACS,IAAI,EAAE,MAAM;IAIxB;;;;;;OAMG;IACH,SAAS,CAAC,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,EAAE;IAwB1D;;;;;OAKG;IACH,SAAS,CAAC,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAKnD;;;;;;OAMG;IACH,SAAS,CAAC,qBAAqB,CAC7B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,GACZ,KAAK,CAAC,YAAY;IAiBrB;;;;;OAKG;IACI,eAAe,IAAI,KAAK,CAAC,YAAY,EAAE;IAO9C;;;;;OAKG;IACI,iBAAiB,IAAI,KAAK,CAAC,YAAY,EAAE;IAOhD;;;;;OAKG;IACI,oBAAoB,IAAI,KAAK,CAAC,YAAY,EAAE;CAQpD;AAED,eAAe,SAAS,CAAC"}
|
|
@@ -1,51 +1,71 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import { type RenderToPipeableStreamOptions } from "react-dom/server";
|
|
2
|
+
import Extractor from "./Extractor.js";
|
|
3
|
+
import type { RendererOptions, RenderHandler, ServerEntrypoint, ServerEntrypointProps } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* This class is responsible for rendering a React JSX component and sending it as response to a client.
|
|
6
|
+
* It offers 2 ways of doing it:
|
|
7
|
+
* - As string
|
|
8
|
+
* - As stream
|
|
9
|
+
* Each way has its advantages and inconveniences. You can read more about them in the package README.
|
|
10
|
+
*/
|
|
11
|
+
export default class JSXRenderer<TComponent extends ServerEntrypoint<InitialData>, InitialData extends Record<string, unknown>> {
|
|
12
|
+
protected readonly Component: TComponent;
|
|
13
|
+
protected readonly initialData: InitialData;
|
|
14
|
+
protected readonly options: RendererOptions;
|
|
15
|
+
protected extractor: Extractor | undefined;
|
|
9
16
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* See https://react.dev/reference/react-dom/server/renderToPipeableStream#parameters
|
|
17
|
+
* Creates a renderer instance which can be used to write Server Side Rendered HTML
|
|
18
|
+
* into a {@link node:http#ServerResponse | ServerResponse}.
|
|
13
19
|
*/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
20
|
+
constructor(Component: TComponent, initialData?: InitialData, options?: RendererOptions);
|
|
21
|
+
/**
|
|
22
|
+
* Gets the locale to be used for the rendered page.
|
|
23
|
+
* Default if there was no locale passed as option is "en".
|
|
24
|
+
*/
|
|
25
|
+
getLocale(): string;
|
|
26
|
+
/**
|
|
27
|
+
* Gets the props needed to render the component.
|
|
28
|
+
*/
|
|
29
|
+
protected getComponentProps(): ServerEntrypointProps<InitialData>;
|
|
30
|
+
/**
|
|
31
|
+
* Gets a list of all the "src" attributes of the given scripts that match the passed type.
|
|
32
|
+
*/
|
|
33
|
+
protected getScriptSourcesByType(scripts: React.ReactElement[], type: "module" | "classic"): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Adds some properties to the options that are passed to {@link react-dom#renderToPipeableStream | renderToPipeableStream}.
|
|
36
|
+
*
|
|
37
|
+
* @remark The options that are added are:
|
|
38
|
+
* - bootstrapScriptContent: includes the initial data passed as prop to the component in a <script> so that it
|
|
39
|
+
* is available when rendering the page in the browser (to avoid hydration mismatches).
|
|
40
|
+
* - bootstrapScripts: classic scripts which react strips out of the page. The only way to add them is to include them
|
|
41
|
+
* in this property.
|
|
42
|
+
* - bootstrapModules: module scripts which react also strips out of the page and need to be added like this.
|
|
43
|
+
*/
|
|
44
|
+
protected enrichRendererOptions(props: ServerEntrypointProps<InitialData>): RenderToPipeableStreamOptions;
|
|
36
45
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
46
|
+
* This function is responsible for rendering a React component and sending it to the client through
|
|
47
|
+
* a pipeable stream.
|
|
48
|
+
*
|
|
49
|
+
* @remark See the README to understand the difference between rendering options.
|
|
50
|
+
*
|
|
51
|
+
* The streaming might improve the time taken for the page to be rendered and interactive
|
|
52
|
+
* (at least in part), using React's Suspense/lazy API and pipeable streams.
|
|
53
|
+
*
|
|
54
|
+
* CAUTION: The resulting HTML rendered this way is not cacheable.
|
|
40
55
|
*/
|
|
41
|
-
|
|
56
|
+
renderToStream: RenderHandler;
|
|
42
57
|
/**
|
|
43
|
-
* Renders this renderer's JSX component as a
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* @
|
|
47
|
-
*
|
|
58
|
+
* Renders this renderer's JSX component as a string and writes it to the given
|
|
59
|
+
* {@link node:http#ServerResponse | ServerResponse}.
|
|
60
|
+
*
|
|
61
|
+
* @remark See the README to understand the difference between rendering options.
|
|
62
|
+
*
|
|
63
|
+
* Rendering to string means all <Suspense> components are loaded synchronously and the response
|
|
64
|
+
* won't be sent to the client until all components have finished loading data and processing.
|
|
65
|
+
*
|
|
66
|
+
* renderToString is useful in Vite Dev mode, as the HMR doesn't work well with Suspense
|
|
67
|
+
* and the Pipeable Stream rendering. Also if the resulting document needs to be cached.
|
|
48
68
|
*/
|
|
49
|
-
|
|
69
|
+
renderToString: RenderHandler;
|
|
50
70
|
}
|
|
51
71
|
//# sourceMappingURL=JSXRenderer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JSXRenderer.d.ts","sourceRoot":"","sources":["../../../src/renderer/JSXRenderer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"JSXRenderer.d.ts","sourceRoot":"","sources":["../../../src/renderer/JSXRenderer.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,6BAA6B,EAGnC,MAAM,kBAAkB,CAAC;AAE1B,OAAO,SAAS,MAAM,gBAAgB,CAAC;AACvC,OAAO,KAAK,EACV,eAAe,EACf,aAAa,EAEb,gBAAgB,EAChB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAEpB;;;;;;GAMG;AACH,MAAM,CAAC,OAAO,OAAO,WAAW,CAC9B,UAAU,SAAS,gBAAgB,CAAC,WAAW,CAAC,EAChD,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IASzC,SAAS,CAAC,QAAQ,CAAC,SAAS,EAAE,UAAU;IACxC,SAAS,CAAC,QAAQ,CAAC,WAAW,EAAE,WAAW;IAC3C,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,eAAe;IAT7C,SAAS,CAAC,SAAS,EAAE,SAAS,GAAG,SAAS,CAAC;IAE3C;;;OAGG;gBAEkB,SAAS,EAAE,UAAU,EACrB,WAAW,GAAE,WAA+B,EAC5C,OAAO,GAAE,eAAoB;IAOlD;;;OAGG;IACI,SAAS,IAAI,MAAM;IAI1B;;OAEG;IACH,SAAS,CAAC,iBAAiB,IAAI,qBAAqB,CAAC,WAAW,CAAC;IAUjE;;OAEG;IACH,SAAS,CAAC,sBAAsB,CAC9B,OAAO,EAAE,KAAK,CAAC,YAAY,EAAE,EAC7B,IAAI,EAAE,QAAQ,GAAG,SAAS,GACzB,MAAM,EAAE;IAsBX;;;;;;;;;OASG;IACH,SAAS,CAAC,qBAAqB,CAC7B,KAAK,EAAE,qBAAqB,CAAC,WAAW,CAAC,GACxC,6BAA6B;IA6BhC;;;;;;;;;;OAUG;IACH,cAAc,EAAE,aAAa,CAqC3B;IAEF;;;;;;;;;;;OAWG;IACH,cAAc,EAAE,aAAa,CAW3B;CACH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/renderer/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,qBAAqB,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
+
export * from "./constants.js";
|
|
1
2
|
export { default as Extractor } from "./Extractor.js";
|
|
2
|
-
export type { ReactServerEntrypointComponent, RendererOptions, RendererServerEntrypointProps, RenderHandler, RenderResult, } from "./JSXRenderer.js";
|
|
3
3
|
export { default as JSXRenderer } from "./JSXRenderer.js";
|
|
4
|
+
export type { RendererOptions, RenderHandler, RenderResult, ServerEntrypoint, ServerEntrypointProps, } from "./types.js";
|
|
4
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/renderer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACtD,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/renderer/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC1D,YAAY,EACV,eAAe,EACf,aAAa,EACb,YAAY,EACZ,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type * as React from "react";
|
|
3
|
+
import type { PipeableStream, RenderToPipeableStreamOptions } from "react-dom/server";
|
|
4
|
+
export interface RendererOptions {
|
|
5
|
+
defaultLocale?: string;
|
|
6
|
+
/** An HTML string to extract the head tags from */
|
|
7
|
+
htmlString?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Options to pass to `react-dom/server.renderToPipeableStream`
|
|
10
|
+
* We specifically exclude `onShellReady()`, `onError()`, `onShellError()` and `onAllReady()` as they are
|
|
11
|
+
* implemented by `JSXRenderer.renderToString()` and `JSXRenderer.renderToStream()`.
|
|
12
|
+
* See https://react.dev/reference/react-dom/server/renderToPipeableStream#parameters
|
|
13
|
+
*/
|
|
14
|
+
renderToPipeableStreamOptions?: Omit<RenderToPipeableStreamOptions, "onShellReady" | "onError" | "onShellError" | "onAllReady">;
|
|
15
|
+
}
|
|
16
|
+
/** The props that the server entrypoint component will receive */
|
|
17
|
+
export interface ServerEntrypointProps<InitialData extends Record<string, unknown>> {
|
|
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
|
+
scriptElements?: React.ReactElement[];
|
|
22
|
+
/** The link tags to include in the page */
|
|
23
|
+
linkElements?: React.ReactElement[];
|
|
24
|
+
/** Other head elements: title, base, style & meta */
|
|
25
|
+
otherHeadElements?: React.ReactElement[];
|
|
26
|
+
/**
|
|
27
|
+
* Initial data used in the server to render the React application, which needs to be
|
|
28
|
+
* embedded in the resulting HTML so that the hydration in the client matches that of the server.
|
|
29
|
+
*/
|
|
30
|
+
initialData?: InitialData;
|
|
31
|
+
}
|
|
32
|
+
export type ServerEntrypoint<InitialData extends Record<string, unknown>> = React.ComponentType<ServerEntrypointProps<InitialData>>;
|
|
33
|
+
/** The result of rendering a React component */
|
|
34
|
+
export type RenderResult = PipeableStream;
|
|
35
|
+
/** A function that renders a React component */
|
|
36
|
+
export type RenderHandler = (req: IncomingMessage, res: ServerResponse) => void;
|
|
37
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/renderer/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AACpC,OAAO,KAAK,EACV,cAAc,EACd,6BAA6B,EAC9B,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;OAKG;IACH,6BAA6B,CAAC,EAAE,IAAI,CAClC,6BAA6B,EAC7B,cAAc,GAAG,SAAS,GAAG,cAAc,GAAG,YAAY,CAC3D,CAAC;CACH;AAED,kEAAkE;AAClE,MAAM,WAAW,qBAAqB,CACpC,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAE3C,iFAAiF;IACjF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,cAAc,CAAC,EAAE,KAAK,CAAC,YAAY,EAAE,CAAC;IACtC,2CAA2C;IAC3C,YAAY,CAAC,EAAE,KAAK,CAAC,YAAY,EAAE,CAAC;IACpC,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,KAAK,CAAC,YAAY,EAAE,CAAC;IACzC;;;OAGG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,MAAM,MAAM,gBAAgB,CAAC,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACtE,KAAK,CAAC,aAAa,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAC,CAAC;AAI1D,gDAAgD;AAChD,MAAM,MAAM,YAAY,GAAG,cAAc,CAAC;AAC1C,gDAAgD;AAChD,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC"}
|
|
@@ -14,7 +14,7 @@ import type { RenderHandler } from "../renderer/index.js";
|
|
|
14
14
|
* import htmlString from "../../dist/client/index.html?raw";
|
|
15
15
|
* import EntryServer from "./entry-server.js";
|
|
16
16
|
*
|
|
17
|
-
* // `EntryServer` is an instance of `@canonical/react-ssr/renderer/
|
|
17
|
+
* // `EntryServer` is an instance of `@canonical/react-ssr/renderer/ServerEntrypoint`
|
|
18
18
|
* const Renderer = new JSXRenderer(EntryServer, {
|
|
19
19
|
* htmlString,
|
|
20
20
|
* });
|
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.15.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/types/index.d.ts",
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"import": "./dist/esm/renderer/index.js",
|
|
45
45
|
"types": "./dist/types/renderer/index.d.ts"
|
|
46
46
|
},
|
|
47
|
+
"./renderer/constants": {
|
|
48
|
+
"import": "./dist/esm/renderer/constants.js",
|
|
49
|
+
"types": "./dist/types/renderer/constants.d.ts"
|
|
50
|
+
},
|
|
47
51
|
"./server": {
|
|
48
52
|
"import": "./dist/esm/server/index.js",
|
|
49
53
|
"types": "./dist/types/server/index.d.ts"
|
|
@@ -51,9 +55,9 @@
|
|
|
51
55
|
},
|
|
52
56
|
"devDependencies": {
|
|
53
57
|
"@biomejs/biome": "2.3.14",
|
|
54
|
-
"@canonical/biome-config": "^0.
|
|
55
|
-
"@canonical/typescript-config-react": "^0.
|
|
56
|
-
"@canonical/webarchitect": "^0.
|
|
58
|
+
"@canonical/biome-config": "^0.15.0",
|
|
59
|
+
"@canonical/typescript-config-react": "^0.15.0",
|
|
60
|
+
"@canonical/webarchitect": "^0.15.0",
|
|
57
61
|
"@types/express": "^5.0.6",
|
|
58
62
|
"@types/node": "^24.10.13",
|
|
59
63
|
"@types/react": "^19.2.13",
|
|
@@ -61,12 +65,12 @@
|
|
|
61
65
|
"typescript": "^5.9.3"
|
|
62
66
|
},
|
|
63
67
|
"dependencies": {
|
|
64
|
-
"@canonical/utils": "^0.
|
|
68
|
+
"@canonical/utils": "^0.15.0",
|
|
65
69
|
"domhandler": "^5.0.3",
|
|
66
70
|
"express": "^5.2.1",
|
|
67
71
|
"htmlparser2": "^10.1.0",
|
|
68
72
|
"react": "^19.2.4",
|
|
69
73
|
"react-dom": "^19.2.4"
|
|
70
74
|
},
|
|
71
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "60c6f6f355d4a80ce110d8dd44c429695301ad86"
|
|
72
76
|
}
|