@canonical/react-ssr 0.22.0 → 0.23.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/dist/esm/bin/serve-express.js +86 -0
- package/dist/esm/bin/serve-express.js.map +1 -0
- package/dist/esm/index.js +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/index.js +3 -0
- package/dist/esm/lib/index.js.map +1 -0
- package/dist/esm/lib/renderer/Extractor.js +172 -0
- package/dist/esm/lib/renderer/Extractor.js.map +1 -0
- package/dist/esm/lib/renderer/JSXRenderer.js +283 -0
- package/dist/esm/lib/renderer/JSXRenderer.js.map +1 -0
- package/dist/esm/lib/renderer/SitemapRenderer.js +241 -0
- package/dist/esm/lib/renderer/SitemapRenderer.js.map +1 -0
- package/dist/esm/lib/renderer/TextRenderer.js +124 -0
- package/dist/esm/lib/renderer/TextRenderer.js.map +1 -0
- package/dist/esm/lib/renderer/constants.js.map +1 -0
- package/dist/esm/lib/renderer/index.js +6 -0
- package/dist/esm/lib/renderer/index.js.map +1 -0
- package/dist/esm/lib/renderer/types.js +10 -0
- package/dist/esm/lib/renderer/types.js.map +1 -0
- package/dist/esm/lib/server/index.js +3 -0
- package/dist/esm/lib/server/index.js.map +1 -0
- package/dist/esm/lib/server/serveStream.js +53 -0
- package/dist/esm/lib/server/serveStream.js.map +1 -0
- package/dist/esm/lib/server/serveString.js +49 -0
- package/dist/esm/lib/server/serveString.js.map +1 -0
- package/dist/types/bin/serve-express.d.ts +24 -0
- package/dist/types/bin/serve-express.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/index.d.ts +3 -0
- package/dist/types/lib/index.d.ts.map +1 -0
- package/dist/types/lib/renderer/Extractor.d.ts +93 -0
- package/dist/types/lib/renderer/Extractor.d.ts.map +1 -0
- package/dist/types/lib/renderer/JSXRenderer.d.ts +163 -0
- package/dist/types/lib/renderer/JSXRenderer.d.ts.map +1 -0
- package/dist/types/lib/renderer/SitemapRenderer.d.ts +153 -0
- package/dist/types/lib/renderer/SitemapRenderer.d.ts.map +1 -0
- package/dist/types/lib/renderer/TextRenderer.d.ts +83 -0
- package/dist/types/lib/renderer/TextRenderer.d.ts.map +1 -0
- package/dist/types/lib/renderer/constants.d.ts.map +1 -0
- package/dist/types/lib/renderer/index.d.ts +7 -0
- package/dist/types/lib/renderer/index.d.ts.map +1 -0
- package/dist/types/lib/renderer/types.d.ts +161 -0
- package/dist/types/lib/renderer/types.d.ts.map +1 -0
- package/dist/types/lib/server/index.d.ts +3 -0
- package/dist/types/lib/server/index.d.ts.map +1 -0
- package/dist/types/lib/server/serveStream.d.ts +41 -0
- package/dist/types/lib/server/serveStream.d.ts.map +1 -0
- package/dist/types/lib/server/serveString.d.ts +37 -0
- package/dist/types/lib/server/serveString.d.ts.map +1 -0
- package/package.json +32 -17
- package/dist/esm/renderer/Extractor.js +0 -127
- package/dist/esm/renderer/Extractor.js.map +0 -1
- package/dist/esm/renderer/JSXRenderer.js +0 -168
- package/dist/esm/renderer/JSXRenderer.js.map +0 -1
- package/dist/esm/renderer/constants.js.map +0 -1
- package/dist/esm/renderer/index.js +0 -4
- package/dist/esm/renderer/index.js.map +0 -1
- package/dist/esm/renderer/types.js +0 -2
- package/dist/esm/renderer/types.js.map +0 -1
- package/dist/esm/server/index.js +0 -2
- package/dist/esm/server/index.js.map +0 -1
- package/dist/esm/server/serve-express.js +0 -58
- package/dist/esm/server/serve-express.js.map +0 -1
- package/dist/esm/server/serve.js +0 -41
- package/dist/esm/server/serve.js.map +0 -1
- package/dist/types/renderer/Extractor.d.ts +0 -68
- package/dist/types/renderer/Extractor.d.ts.map +0 -1
- package/dist/types/renderer/JSXRenderer.d.ts +0 -71
- package/dist/types/renderer/JSXRenderer.d.ts.map +0 -1
- package/dist/types/renderer/constants.d.ts.map +0 -1
- package/dist/types/renderer/index.d.ts +0 -5
- package/dist/types/renderer/index.d.ts.map +0 -1
- package/dist/types/renderer/types.d.ts +0 -35
- package/dist/types/renderer/types.d.ts.map +0 -1
- package/dist/types/server/index.d.ts +0 -2
- package/dist/types/server/index.d.ts.map +0 -1
- package/dist/types/server/serve-express.d.ts +0 -3
- package/dist/types/server/serve-express.d.ts.map +0 -1
- package/dist/types/server/serve.d.ts +0 -30
- package/dist/types/server/serve.d.ts.map +0 -1
- /package/dist/esm/{renderer → lib/renderer}/constants.js +0 -0
- /package/dist/types/{renderer → lib/renderer}/constants.d.ts +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type contracts for the SSR renderer domain.
|
|
3
|
+
*
|
|
4
|
+
* These types define the interface between renderers (which produce HTML or XML
|
|
5
|
+
* content and record metadata like status codes) and server adapters (which
|
|
6
|
+
* deliver that content over HTTP). Renderers are transport-agnostic — they
|
|
7
|
+
* never write to a response object.
|
|
8
|
+
*/
|
|
9
|
+
import type * as React from "react";
|
|
10
|
+
import type { RenderToPipeableStreamOptions, RenderToReadableStreamOptions } from "react-dom/server";
|
|
11
|
+
/**
|
|
12
|
+
* The pipe/abort handles returned by `renderToPipeableStream`.
|
|
13
|
+
*
|
|
14
|
+
* Mirrors the shape of React's `PipeableStream` but decoupled from the
|
|
15
|
+
* `react-dom/server` import so consumers don't need to depend on it directly.
|
|
16
|
+
*/
|
|
17
|
+
export interface PipeableStreamResult {
|
|
18
|
+
/** Pipe the rendered HTML to a Node.js writable stream (e.g. `ServerResponse`). */
|
|
19
|
+
pipe: <W extends NodeJS.WritableStream>(destination: W) => W;
|
|
20
|
+
/** Abort the in-progress render. */
|
|
21
|
+
abort: (reason?: unknown) => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Configuration for a `JSXRenderer` instance.
|
|
25
|
+
*
|
|
26
|
+
* Controls locale, HTML shell extraction, and options forwarded to
|
|
27
|
+
* React's streaming APIs.
|
|
28
|
+
*/
|
|
29
|
+
export interface RendererOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Locale for the rendered page, passed as the `lang` prop to the server
|
|
32
|
+
* entrypoint component. Defaults to `"en"` when omitted.
|
|
33
|
+
*/
|
|
34
|
+
defaultLocale?: string;
|
|
35
|
+
/**
|
|
36
|
+
* A full HTML string (typically from a Vite build) whose `<head>` tags
|
|
37
|
+
* are extracted and injected into the rendered output. When omitted,
|
|
38
|
+
* the renderer produces output without extracted head elements.
|
|
39
|
+
*/
|
|
40
|
+
htmlString?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Options forwarded to `react-dom/server.renderToPipeableStream`.
|
|
43
|
+
*
|
|
44
|
+
* The renderer merges its own `bootstrapScriptContent`, `bootstrapScripts`,
|
|
45
|
+
* and `bootstrapModules` into these options, but user-provided values take
|
|
46
|
+
* priority and are never overwritten.
|
|
47
|
+
*/
|
|
48
|
+
renderToPipeableStreamOptions?: RenderToPipeableStreamOptions;
|
|
49
|
+
/**
|
|
50
|
+
* Options forwarded to `react-dom/server.renderToReadableStream`.
|
|
51
|
+
*
|
|
52
|
+
* Same merge semantics as `renderToPipeableStreamOptions`. When omitted,
|
|
53
|
+
* the shared bootstrap options from `renderToPipeableStreamOptions` are
|
|
54
|
+
* used as a fallback (the bootstrap fields are structurally identical
|
|
55
|
+
* between the two option types).
|
|
56
|
+
*/
|
|
57
|
+
renderToReadableStreamOptions?: RenderToReadableStreamOptions;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Props received by the server entrypoint component during SSR.
|
|
61
|
+
*
|
|
62
|
+
* The renderer assembles these from the locale, the extracted HTML head elements
|
|
63
|
+
* (when an HTML shell is provided), and the initial data for hydration.
|
|
64
|
+
*
|
|
65
|
+
* @typeParam InitialData - Shape of the hydration data embedded in the page.
|
|
66
|
+
*/
|
|
67
|
+
export interface ServerEntrypointProps<InitialData extends Record<string, unknown>> {
|
|
68
|
+
/** BCP 47 language tag for the page (e.g. `"en"`, `"fr-CA"`). */
|
|
69
|
+
lang?: string;
|
|
70
|
+
/**
|
|
71
|
+
* `<script>` elements extracted from the HTML shell, as React elements.
|
|
72
|
+
* Undefined when no HTML shell was provided.
|
|
73
|
+
*/
|
|
74
|
+
scriptElements?: React.ReactElement[];
|
|
75
|
+
/**
|
|
76
|
+
* `<link>` elements extracted from the HTML shell, as React elements.
|
|
77
|
+
* Undefined when no HTML shell was provided.
|
|
78
|
+
*/
|
|
79
|
+
linkElements?: React.ReactElement[];
|
|
80
|
+
/**
|
|
81
|
+
* `<title>`, `<meta>`, `<style>`, and `<base>` elements from the HTML shell,
|
|
82
|
+
* as React elements. Undefined when no HTML shell was provided.
|
|
83
|
+
*/
|
|
84
|
+
otherHeadElements?: React.ReactElement[];
|
|
85
|
+
/**
|
|
86
|
+
* Data to embed in `window.__INITIAL_DATA__` for client hydration.
|
|
87
|
+
*
|
|
88
|
+
* The renderer serialises this object as JSON in a `<script>` tag so that the
|
|
89
|
+
* client can read it during hydration without a second network request. The
|
|
90
|
+
* JSON is escaped to prevent `</script>` injection.
|
|
91
|
+
*/
|
|
92
|
+
initialData?: InitialData;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A React component used as the server-side rendering entry point.
|
|
96
|
+
*
|
|
97
|
+
* Receives `ServerEntrypointProps` and is expected to render the full `<html>`
|
|
98
|
+
* document, including the extracted head elements and initial data.
|
|
99
|
+
*
|
|
100
|
+
* @typeParam InitialData - Shape of the hydration data embedded in the page.
|
|
101
|
+
*/
|
|
102
|
+
export type ServerEntrypoint<InitialData extends Record<string, unknown>> = React.ComponentType<ServerEntrypointProps<InitialData>>;
|
|
103
|
+
/**
|
|
104
|
+
* A single URL entry in an XML sitemap.
|
|
105
|
+
*
|
|
106
|
+
* Follows the Sitemaps XML protocol: https://www.sitemaps.org/protocol.html.
|
|
107
|
+
* All fields except `loc` are optional — the renderer applies defaults from
|
|
108
|
+
* `SitemapConfig` for `changefreq` and `priority`.
|
|
109
|
+
*/
|
|
110
|
+
export interface SitemapItem {
|
|
111
|
+
/**
|
|
112
|
+
* URL of the page. Can be absolute (`https://example.com/about`) or relative
|
|
113
|
+
* (`/about`). Relative URLs are resolved against `SitemapConfig.baseUrl`.
|
|
114
|
+
* An empty string resolves to the base URL itself.
|
|
115
|
+
*/
|
|
116
|
+
loc: string;
|
|
117
|
+
/**
|
|
118
|
+
* Date of last modification. Accepts a `Date` object or an ISO 8601 string.
|
|
119
|
+
* Formatted to `YYYY-MM-DD` in the output XML.
|
|
120
|
+
*/
|
|
121
|
+
lastmod?: Date | string;
|
|
122
|
+
/** How frequently the page is likely to change. */
|
|
123
|
+
changefreq?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
124
|
+
/**
|
|
125
|
+
* Priority of this URL relative to other URLs on the site.
|
|
126
|
+
* Valid range is `0.0` to `1.0`. Default is `0.5` per the protocol.
|
|
127
|
+
*/
|
|
128
|
+
priority?: number;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* An async function that produces a batch of sitemap items.
|
|
132
|
+
*
|
|
133
|
+
* The `SitemapRenderer` accepts an array of getters, calls them concurrently
|
|
134
|
+
* via `Promise.all`, and flattens the results into a single item list.
|
|
135
|
+
*/
|
|
136
|
+
export type SitemapGetter = () => Promise<SitemapItem[]>;
|
|
137
|
+
/**
|
|
138
|
+
* Configuration for the `SitemapRenderer`.
|
|
139
|
+
*
|
|
140
|
+
* Defines the canonical base URL used to resolve relative `loc` values,
|
|
141
|
+
* and optional defaults applied to items that omit `changefreq` or `priority`.
|
|
142
|
+
*/
|
|
143
|
+
export interface SitemapConfig {
|
|
144
|
+
/**
|
|
145
|
+
* The canonical base URL for the site (e.g. `"https://example.com"`).
|
|
146
|
+
* Used to resolve relative `loc` values in sitemap items.
|
|
147
|
+
*/
|
|
148
|
+
baseUrl: string;
|
|
149
|
+
/** Default `changefreq` applied to items that do not specify one. */
|
|
150
|
+
defaultChangefreq?: SitemapItem["changefreq"];
|
|
151
|
+
/** Default `priority` applied to items that do not specify one (0.0 to 1.0). */
|
|
152
|
+
defaultPriority?: number;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* An async function that produces a string of text content.
|
|
156
|
+
*
|
|
157
|
+
* The `TextRenderer` accepts an array of getters, calls them sequentially
|
|
158
|
+
* (order matters for document structure), and concatenates the results.
|
|
159
|
+
*/
|
|
160
|
+
export type TextGetter = () => Promise<string>;
|
|
161
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/lib/renderer/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,KAAK,KAAK,MAAM,OAAO,CAAC;AACpC,OAAO,KAAK,EACV,6BAA6B,EAC7B,6BAA6B,EAC9B,MAAM,kBAAkB,CAAC;AAI1B;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,IAAI,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,cAAc,EAAE,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC;IAC7D,oCAAoC;IACpC,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CACnC;AAID;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;OAMG;IACH,6BAA6B,CAAC,EAAE,6BAA6B,CAAC;IAE9D;;;;;;;OAOG;IACH,6BAA6B,CAAC,EAAE,6BAA6B,CAAC;CAC/D;AAID;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB,CACpC,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAE3C,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,cAAc,CAAC,EAAE,KAAK,CAAC,YAAY,EAAE,CAAC;IAEtC;;;OAGG;IACH,YAAY,CAAC,EAAE,KAAK,CAAC,YAAY,EAAE,CAAC;IAEpC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,KAAK,CAAC,YAAY,EAAE,CAAC;IAEzC;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,CAAC,WAAW,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IACtE,KAAK,CAAC,aAAa,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAC,CAAC;AAI1D;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;OAGG;IACH,OAAO,CAAC,EAAE,IAAI,GAAG,MAAM,CAAC;IAExB,mDAAmD;IACnD,UAAU,CAAC,EACP,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,OAAO,CAAC;IAEZ;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;IAE9C,gFAAgF;IAChF,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAID;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* Convenience wrapper that adapts a renderer factory into a Node.js `(req, res)` handler
|
|
4
|
+
* for pipeable stream rendering.
|
|
5
|
+
*
|
|
6
|
+
* Calls the factory with each incoming request. The factory is expected to construct
|
|
7
|
+
* a renderer (with per-request context like locale, auth, theme) and call
|
|
8
|
+
* `renderToPipeableStream()` on it. This wrapper then awaits `statusReady`, writes
|
|
9
|
+
* headers with the renderer's `statusCode`, and pipes the stream to the response.
|
|
10
|
+
*
|
|
11
|
+
* Does not set `Content-Type` — the consumer controls headers through the factory
|
|
12
|
+
* or by wrapping this handler. Defaults to `text/html; charset=utf-8`.
|
|
13
|
+
*
|
|
14
|
+
* @note This function is impure — it writes to the HTTP response.
|
|
15
|
+
*
|
|
16
|
+
* @param factory - A function that receives the request and returns a renderer.
|
|
17
|
+
* The renderer must have `renderToPipeableStream()`, `statusCode`, and `statusReady`.
|
|
18
|
+
* @returns A Node.js request handler suitable for `app.use()` or `http.createServer()`.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { JSXRenderer } from "@canonical/react-ssr/renderer";
|
|
23
|
+
* import { serveStream } from "@canonical/react-ssr/server";
|
|
24
|
+
*
|
|
25
|
+
* app.use(serveStream((req) => {
|
|
26
|
+
* return new JSXRenderer(
|
|
27
|
+
* EntryServer,
|
|
28
|
+
* { locale: getLocale(req), user: getUser(req) },
|
|
29
|
+
* { htmlString },
|
|
30
|
+
* );
|
|
31
|
+
* }));
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare function serveStream(factory: (req: IncomingMessage) => {
|
|
35
|
+
renderToPipeableStream: () => {
|
|
36
|
+
pipe: <W extends NodeJS.WritableStream>(destination: W) => W;
|
|
37
|
+
};
|
|
38
|
+
statusCode: number;
|
|
39
|
+
statusReady: Promise<void>;
|
|
40
|
+
}): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
41
|
+
//# sourceMappingURL=serveStream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serveStream.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/serveStream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK;IACjC,sBAAsB,EAAE,MAAM;QAC5B,IAAI,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,cAAc,EAAE,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC;KAC9D,CAAC;IACF,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5B,IAEa,KAAK,eAAe,EAAE,KAAK,cAAc,mBAiBxD"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* Convenience wrapper that adapts a renderer factory into a Node.js `(req, res)` handler
|
|
4
|
+
* for string rendering.
|
|
5
|
+
*
|
|
6
|
+
* Calls the factory with each incoming request. The factory is expected to construct
|
|
7
|
+
* a renderer (with per-request context like locale, auth, theme) and call
|
|
8
|
+
* `renderToString()` on it. This wrapper then writes headers with the renderer's
|
|
9
|
+
* `statusCode` and sends the HTML string as the response body.
|
|
10
|
+
*
|
|
11
|
+
* Does not set `Content-Type` — defaults to `text/html; charset=utf-8`.
|
|
12
|
+
*
|
|
13
|
+
* @note This function is impure — it writes to the HTTP response.
|
|
14
|
+
*
|
|
15
|
+
* @param factory - A function that receives the request and returns a renderer.
|
|
16
|
+
* The renderer must have `renderToString()` and `statusCode`.
|
|
17
|
+
* @returns A Node.js request handler suitable for `app.use()` or `http.createServer()`.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { JSXRenderer } from "@canonical/react-ssr/renderer";
|
|
22
|
+
* import { serveString } from "@canonical/react-ssr/server";
|
|
23
|
+
*
|
|
24
|
+
* app.use(serveString((req) => {
|
|
25
|
+
* return new JSXRenderer(
|
|
26
|
+
* EntryServer,
|
|
27
|
+
* { locale: getLocale(req), user: getUser(req) },
|
|
28
|
+
* { htmlString },
|
|
29
|
+
* );
|
|
30
|
+
* }));
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function serveString(factory: (req: IncomingMessage) => {
|
|
34
|
+
renderToString: () => string;
|
|
35
|
+
statusCode: number;
|
|
36
|
+
}): (req: IncomingMessage, res: ServerResponse) => void;
|
|
37
|
+
//# sourceMappingURL=serveString.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serveString.d.ts","sourceRoot":"","sources":["../../../../src/lib/server/serveString.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK;IACjC,cAAc,EAAE,MAAM,MAAM,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;CACpB,IAEO,KAAK,eAAe,EAAE,KAAK,cAAc,UAclD"}
|
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.23.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/types/index.d.ts",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"name": "Canonical Webteam"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"serve-express": "./dist/esm/
|
|
16
|
+
"serve-express": "./dist/esm/bin/serve-express.js",
|
|
17
|
+
"serve-bun": "./dist/esm/bin/serve-bun.js"
|
|
17
18
|
},
|
|
18
19
|
"repository": {
|
|
19
20
|
"type": "git",
|
|
@@ -24,6 +25,10 @@
|
|
|
24
25
|
"url": "https://github.com/canonical/pragma/issues"
|
|
25
26
|
},
|
|
26
27
|
"homepage": "https://github.com/canonical/pragma#readme",
|
|
28
|
+
"imports": {
|
|
29
|
+
"#renderer": "./src/lib/renderer/index.ts",
|
|
30
|
+
"#server": "./src/lib/server/index.ts"
|
|
31
|
+
},
|
|
27
32
|
"scripts": {
|
|
28
33
|
"build": "tsc -p tsconfig.build.json",
|
|
29
34
|
"build:all": "tsc -p tsconfig.build.json",
|
|
@@ -33,7 +38,9 @@
|
|
|
33
38
|
"check:biome": "biome check",
|
|
34
39
|
"check:biome:fix": "biome check --write",
|
|
35
40
|
"check:ts": "tsc --noEmit",
|
|
36
|
-
"test": "
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"test:coverage": "vitest run --coverage"
|
|
37
44
|
},
|
|
38
45
|
"exports": {
|
|
39
46
|
".": {
|
|
@@ -41,36 +48,44 @@
|
|
|
41
48
|
"types": "./dist/types/index.d.ts"
|
|
42
49
|
},
|
|
43
50
|
"./renderer": {
|
|
44
|
-
"import": "./dist/esm/renderer/index.js",
|
|
45
|
-
"types": "./dist/types/renderer/index.d.ts"
|
|
51
|
+
"import": "./dist/esm/lib/renderer/index.js",
|
|
52
|
+
"types": "./dist/types/lib/renderer/index.d.ts"
|
|
46
53
|
},
|
|
47
54
|
"./renderer/constants": {
|
|
48
|
-
"import": "./dist/esm/renderer/constants.js",
|
|
49
|
-
"types": "./dist/types/renderer/constants.d.ts"
|
|
55
|
+
"import": "./dist/esm/lib/renderer/constants.js",
|
|
56
|
+
"types": "./dist/types/lib/renderer/constants.d.ts"
|
|
50
57
|
},
|
|
51
58
|
"./server": {
|
|
52
|
-
"import": "./dist/esm/server/index.js",
|
|
53
|
-
"types": "./dist/types/server/index.d.ts"
|
|
59
|
+
"import": "./dist/esm/lib/server/index.js",
|
|
60
|
+
"types": "./dist/types/lib/server/index.d.ts"
|
|
54
61
|
}
|
|
55
62
|
},
|
|
56
63
|
"devDependencies": {
|
|
57
64
|
"@biomejs/biome": "2.4.9",
|
|
58
|
-
"@canonical/biome-config": "^0.
|
|
59
|
-
"@canonical/typescript-config-react": "^0.
|
|
60
|
-
"@canonical/webarchitect": "^0.
|
|
65
|
+
"@canonical/biome-config": "^0.23.0",
|
|
66
|
+
"@canonical/typescript-config-react": "^0.23.0",
|
|
67
|
+
"@canonical/webarchitect": "^0.23.0",
|
|
61
68
|
"@types/express": "^5.0.6",
|
|
62
69
|
"@types/node": "^24.12.0",
|
|
63
70
|
"@types/react": "^19.2.14",
|
|
64
71
|
"@types/react-dom": "^19.2.3",
|
|
65
|
-
"
|
|
72
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
73
|
+
"typescript": "^5.9.3",
|
|
74
|
+
"vitest": "^4.0.18"
|
|
66
75
|
},
|
|
67
76
|
"dependencies": {
|
|
68
|
-
"@canonical/utils": "^0.
|
|
69
|
-
"domhandler": "^6.0.0",
|
|
70
|
-
"express": "^5.2.1",
|
|
77
|
+
"@canonical/utils": "^0.23.0",
|
|
71
78
|
"htmlparser2": "^10.1.0",
|
|
72
79
|
"react": "^19.2.4",
|
|
73
80
|
"react-dom": "^19.2.4"
|
|
74
81
|
},
|
|
75
|
-
"
|
|
82
|
+
"peerDependencies": {
|
|
83
|
+
"express": "^5.2.1"
|
|
84
|
+
},
|
|
85
|
+
"peerDependenciesMeta": {
|
|
86
|
+
"express": {
|
|
87
|
+
"optional": true
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"gitHead": "742f5396bc3f9ca01a49646ed5b67acfc9d001d2"
|
|
76
91
|
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { toCamelCase } from "@canonical/utils";
|
|
2
|
-
import { NodeWithChildren } from "domhandler";
|
|
3
|
-
import { parseDocument } from "htmlparser2";
|
|
4
|
-
import React from "react";
|
|
5
|
-
const REACT_KEYS_DICTIONARY = {
|
|
6
|
-
class: "className",
|
|
7
|
-
for: "htmlFor",
|
|
8
|
-
crossorigin: "crossOrigin",
|
|
9
|
-
charset: "charSet",
|
|
10
|
-
};
|
|
11
|
-
/**
|
|
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
|
|
20
|
-
*/
|
|
21
|
-
class Extractor {
|
|
22
|
-
/**
|
|
23
|
-
* A document object representing the DOM of a page.
|
|
24
|
-
*/
|
|
25
|
-
document;
|
|
26
|
-
/**
|
|
27
|
-
* Creates an Extractor object for a given HTML string.
|
|
28
|
-
*/
|
|
29
|
-
constructor(html) {
|
|
30
|
-
this.document = parseDocument(html);
|
|
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
|
-
*/
|
|
39
|
-
getElementsByTagName(tagName) {
|
|
40
|
-
const elements = [];
|
|
41
|
-
const stack = [...this.document.children];
|
|
42
|
-
while (stack.length) {
|
|
43
|
-
const node = stack.pop();
|
|
44
|
-
if (!node)
|
|
45
|
-
continue;
|
|
46
|
-
if (node.type === "tag" && node.name === tagName) {
|
|
47
|
-
elements.push(node);
|
|
48
|
-
}
|
|
49
|
-
// Check for script tags specifically
|
|
50
|
-
if (node.type === "script" && tagName === "script") {
|
|
51
|
-
elements.push(node);
|
|
52
|
-
}
|
|
53
|
-
if (node instanceof NodeWithChildren) {
|
|
54
|
-
stack.push(...node.children);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return elements;
|
|
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
|
-
*/
|
|
65
|
-
convertKeyToReactKey(key) {
|
|
66
|
-
const reactKey = REACT_KEYS_DICTIONARY[key.toLowerCase()];
|
|
67
|
-
return reactKey ? reactKey : toCamelCase(key);
|
|
68
|
-
}
|
|
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) {
|
|
77
|
-
const props = {};
|
|
78
|
-
for (const [key, value] of Object.entries(element.attribs)) {
|
|
79
|
-
props[this.convertKeyToReactKey(key)] = value;
|
|
80
|
-
}
|
|
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);
|
|
88
|
-
}
|
|
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() {
|
|
96
|
-
const linkElements = this.getElementsByTagName("link");
|
|
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);
|
|
100
|
-
}
|
|
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() {
|
|
108
|
-
const scriptElements = this.getElementsByTagName("script");
|
|
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);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
export default Extractor;
|
|
127
|
-
//# sourceMappingURL=Extractor.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Extractor.js","sourceRoot":"","sources":["../../../src/renderer/Extractor.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { createElement } from "react";
|
|
2
|
-
import { renderToPipeableStream, renderToString, } from "react-dom/server";
|
|
3
|
-
import { INITIAL_DATA_KEY } from "./constants.js";
|
|
4
|
-
import Extractor from "./Extractor.js";
|
|
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 {
|
|
13
|
-
Component;
|
|
14
|
-
initialData;
|
|
15
|
-
options;
|
|
16
|
-
extractor;
|
|
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 = {}) {
|
|
22
|
-
this.Component = Component;
|
|
23
|
-
this.initialData = initialData;
|
|
24
|
-
this.options = options;
|
|
25
|
-
this.extractor = this.options.htmlString
|
|
26
|
-
? new Extractor(this.options.htmlString)
|
|
27
|
-
: undefined;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
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.
|
|
38
|
-
*/
|
|
39
|
-
getComponentProps() {
|
|
40
|
-
return {
|
|
41
|
-
lang: this.getLocale(),
|
|
42
|
-
scriptElements: this.extractor?.getScriptElements(),
|
|
43
|
-
linkElements: this.extractor?.getLinkElements(),
|
|
44
|
-
otherHeadElements: this.extractor?.getOtherHeadElements(),
|
|
45
|
-
initialData: this.initialData,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
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.
|
|
105
|
-
*/
|
|
106
|
-
renderToStream = (_req, res) => {
|
|
107
|
-
const errorRef = { current: undefined };
|
|
108
|
-
const props = this.getComponentProps();
|
|
109
|
-
const jsx = createElement(this.Component, props);
|
|
110
|
-
const { onShellError: onShellErrorCallback, onShellReady: onShellReadyCallback, onAllReady: onAllReadyCallback, onError: onErrorCallback, ...options } = this.enrichRendererOptions(props);
|
|
111
|
-
const jsxStream = renderToPipeableStream(jsx, {
|
|
112
|
-
...options,
|
|
113
|
-
// Error occurred during rendering, after the shell & headers were sent - store the error for usage after stream is sent
|
|
114
|
-
onError(error, errorInfo) {
|
|
115
|
-
onErrorCallback?.(error, errorInfo);
|
|
116
|
-
errorRef.current = error;
|
|
117
|
-
console.error(error);
|
|
118
|
-
},
|
|
119
|
-
// Early error, before the shell is prepared
|
|
120
|
-
onShellError(error) {
|
|
121
|
-
onShellErrorCallback?.(error);
|
|
122
|
-
if (!res.headersSent) {
|
|
123
|
-
res
|
|
124
|
-
.writeHead(500, { "Content-Type": "text/html; charset=utf-8" })
|
|
125
|
-
.end("<h1>Something went wrong</h1>");
|
|
126
|
-
}
|
|
127
|
-
console.error(error);
|
|
128
|
-
},
|
|
129
|
-
onShellReady() {
|
|
130
|
-
onShellReadyCallback?.();
|
|
131
|
-
if (!res.headersSent) {
|
|
132
|
-
res.writeHead(errorRef.current ? 500 : 200, {
|
|
133
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
jsxStream.pipe(res);
|
|
137
|
-
res.on("finish", () => {
|
|
138
|
-
res.end();
|
|
139
|
-
});
|
|
140
|
-
},
|
|
141
|
-
onAllReady() {
|
|
142
|
-
onAllReadyCallback?.();
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
};
|
|
146
|
-
/**
|
|
147
|
-
* Renders this renderer's JSX component as a string and writes it to the given
|
|
148
|
-
* {@link node:http#ServerResponse | ServerResponse}.
|
|
149
|
-
*
|
|
150
|
-
* @remark See the README to understand the difference between rendering options.
|
|
151
|
-
*
|
|
152
|
-
* Rendering to string means all <Suspense> components are loaded synchronously and the response
|
|
153
|
-
* won't be sent to the client until all components have finished loading data and processing.
|
|
154
|
-
*
|
|
155
|
-
* renderToString is useful in Vite Dev mode, as the HMR doesn't work well with Suspense
|
|
156
|
-
* and the Pipeable Stream rendering. Also if the resulting document needs to be cached.
|
|
157
|
-
*/
|
|
158
|
-
renderToString = (_req, res) => {
|
|
159
|
-
const props = this.getComponentProps();
|
|
160
|
-
const jsx = createElement(this.Component, props);
|
|
161
|
-
const html = renderToString(jsx);
|
|
162
|
-
res
|
|
163
|
-
.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
|
|
164
|
-
.write(html);
|
|
165
|
-
res.end();
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
//# sourceMappingURL=JSXRenderer.js.map
|