@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.
Files changed (83) hide show
  1. package/dist/esm/bin/serve-express.js +86 -0
  2. package/dist/esm/bin/serve-express.js.map +1 -0
  3. package/dist/esm/index.js +2 -2
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/esm/lib/index.js +3 -0
  6. package/dist/esm/lib/index.js.map +1 -0
  7. package/dist/esm/lib/renderer/Extractor.js +172 -0
  8. package/dist/esm/lib/renderer/Extractor.js.map +1 -0
  9. package/dist/esm/lib/renderer/JSXRenderer.js +283 -0
  10. package/dist/esm/lib/renderer/JSXRenderer.js.map +1 -0
  11. package/dist/esm/lib/renderer/SitemapRenderer.js +241 -0
  12. package/dist/esm/lib/renderer/SitemapRenderer.js.map +1 -0
  13. package/dist/esm/lib/renderer/TextRenderer.js +124 -0
  14. package/dist/esm/lib/renderer/TextRenderer.js.map +1 -0
  15. package/dist/esm/lib/renderer/constants.js.map +1 -0
  16. package/dist/esm/lib/renderer/index.js +6 -0
  17. package/dist/esm/lib/renderer/index.js.map +1 -0
  18. package/dist/esm/lib/renderer/types.js +10 -0
  19. package/dist/esm/lib/renderer/types.js.map +1 -0
  20. package/dist/esm/lib/server/index.js +3 -0
  21. package/dist/esm/lib/server/index.js.map +1 -0
  22. package/dist/esm/lib/server/serveStream.js +53 -0
  23. package/dist/esm/lib/server/serveStream.js.map +1 -0
  24. package/dist/esm/lib/server/serveString.js +49 -0
  25. package/dist/esm/lib/server/serveString.js.map +1 -0
  26. package/dist/types/bin/serve-express.d.ts +24 -0
  27. package/dist/types/bin/serve-express.d.ts.map +1 -0
  28. package/dist/types/index.d.ts +2 -2
  29. package/dist/types/index.d.ts.map +1 -1
  30. package/dist/types/lib/index.d.ts +3 -0
  31. package/dist/types/lib/index.d.ts.map +1 -0
  32. package/dist/types/lib/renderer/Extractor.d.ts +93 -0
  33. package/dist/types/lib/renderer/Extractor.d.ts.map +1 -0
  34. package/dist/types/lib/renderer/JSXRenderer.d.ts +163 -0
  35. package/dist/types/lib/renderer/JSXRenderer.d.ts.map +1 -0
  36. package/dist/types/lib/renderer/SitemapRenderer.d.ts +153 -0
  37. package/dist/types/lib/renderer/SitemapRenderer.d.ts.map +1 -0
  38. package/dist/types/lib/renderer/TextRenderer.d.ts +83 -0
  39. package/dist/types/lib/renderer/TextRenderer.d.ts.map +1 -0
  40. package/dist/types/lib/renderer/constants.d.ts.map +1 -0
  41. package/dist/types/lib/renderer/index.d.ts +7 -0
  42. package/dist/types/lib/renderer/index.d.ts.map +1 -0
  43. package/dist/types/lib/renderer/types.d.ts +161 -0
  44. package/dist/types/lib/renderer/types.d.ts.map +1 -0
  45. package/dist/types/lib/server/index.d.ts +3 -0
  46. package/dist/types/lib/server/index.d.ts.map +1 -0
  47. package/dist/types/lib/server/serveStream.d.ts +41 -0
  48. package/dist/types/lib/server/serveStream.d.ts.map +1 -0
  49. package/dist/types/lib/server/serveString.d.ts +37 -0
  50. package/dist/types/lib/server/serveString.d.ts.map +1 -0
  51. package/package.json +32 -17
  52. package/dist/esm/renderer/Extractor.js +0 -127
  53. package/dist/esm/renderer/Extractor.js.map +0 -1
  54. package/dist/esm/renderer/JSXRenderer.js +0 -168
  55. package/dist/esm/renderer/JSXRenderer.js.map +0 -1
  56. package/dist/esm/renderer/constants.js.map +0 -1
  57. package/dist/esm/renderer/index.js +0 -4
  58. package/dist/esm/renderer/index.js.map +0 -1
  59. package/dist/esm/renderer/types.js +0 -2
  60. package/dist/esm/renderer/types.js.map +0 -1
  61. package/dist/esm/server/index.js +0 -2
  62. package/dist/esm/server/index.js.map +0 -1
  63. package/dist/esm/server/serve-express.js +0 -58
  64. package/dist/esm/server/serve-express.js.map +0 -1
  65. package/dist/esm/server/serve.js +0 -41
  66. package/dist/esm/server/serve.js.map +0 -1
  67. package/dist/types/renderer/Extractor.d.ts +0 -68
  68. package/dist/types/renderer/Extractor.d.ts.map +0 -1
  69. package/dist/types/renderer/JSXRenderer.d.ts +0 -71
  70. package/dist/types/renderer/JSXRenderer.d.ts.map +0 -1
  71. package/dist/types/renderer/constants.d.ts.map +0 -1
  72. package/dist/types/renderer/index.d.ts +0 -5
  73. package/dist/types/renderer/index.d.ts.map +0 -1
  74. package/dist/types/renderer/types.d.ts +0 -35
  75. package/dist/types/renderer/types.d.ts.map +0 -1
  76. package/dist/types/server/index.d.ts +0 -2
  77. package/dist/types/server/index.d.ts.map +0 -1
  78. package/dist/types/server/serve-express.d.ts +0 -3
  79. package/dist/types/server/serve-express.d.ts.map +0 -1
  80. package/dist/types/server/serve.d.ts +0 -30
  81. package/dist/types/server/serve.d.ts.map +0 -1
  82. /package/dist/esm/{renderer → lib/renderer}/constants.js +0 -0
  83. /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,3 @@
1
+ export { serveStream } from "./serveStream.js";
2
+ export { serveString } from "./serveString.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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.22.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/server/serve-express.js"
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": "echo 'No tests defined yet'"
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.22.0",
59
- "@canonical/typescript-config-react": "^0.22.0",
60
- "@canonical/webarchitect": "^0.22.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
- "typescript": "^5.9.3"
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.22.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
- "gitHead": "99e187cf46d8bd7f3933705322fa4ab2d4a711e7"
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