@canonical/react-ssr 0.22.0 → 0.24.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 (95) 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 +3 -2
  4. package/dist/esm/index.js.map +1 -1
  5. package/dist/esm/lib/adapter/index.js +2 -0
  6. package/dist/esm/lib/adapter/index.js.map +1 -0
  7. package/dist/esm/lib/adapter/mime.js +85 -0
  8. package/dist/esm/lib/adapter/mime.js.map +1 -0
  9. package/dist/esm/lib/adapter/types.js +10 -0
  10. package/dist/esm/lib/adapter/types.js.map +1 -0
  11. package/dist/esm/lib/index.js +4 -0
  12. package/dist/esm/lib/index.js.map +1 -0
  13. package/dist/esm/lib/renderer/Extractor.js +172 -0
  14. package/dist/esm/lib/renderer/Extractor.js.map +1 -0
  15. package/dist/esm/lib/renderer/JSXRenderer.js +283 -0
  16. package/dist/esm/lib/renderer/JSXRenderer.js.map +1 -0
  17. package/dist/esm/lib/renderer/SitemapRenderer.js +241 -0
  18. package/dist/esm/lib/renderer/SitemapRenderer.js.map +1 -0
  19. package/dist/esm/lib/renderer/TextRenderer.js +124 -0
  20. package/dist/esm/lib/renderer/TextRenderer.js.map +1 -0
  21. package/dist/esm/lib/renderer/constants.js.map +1 -0
  22. package/dist/esm/lib/renderer/index.js +6 -0
  23. package/dist/esm/lib/renderer/index.js.map +1 -0
  24. package/dist/esm/lib/renderer/types.js +10 -0
  25. package/dist/esm/lib/renderer/types.js.map +1 -0
  26. package/dist/esm/lib/server/index.js +3 -0
  27. package/dist/esm/lib/server/index.js.map +1 -0
  28. package/dist/esm/lib/server/serveStream.js +53 -0
  29. package/dist/esm/lib/server/serveStream.js.map +1 -0
  30. package/dist/esm/lib/server/serveString.js +49 -0
  31. package/dist/esm/lib/server/serveString.js.map +1 -0
  32. package/dist/types/bin/serve-express.d.ts +24 -0
  33. package/dist/types/bin/serve-express.d.ts.map +1 -0
  34. package/dist/types/index.d.ts +3 -2
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/dist/types/lib/adapter/index.d.ts +3 -0
  37. package/dist/types/lib/adapter/index.d.ts.map +1 -0
  38. package/dist/types/lib/adapter/mime.d.ts +43 -0
  39. package/dist/types/lib/adapter/mime.d.ts.map +1 -0
  40. package/dist/types/lib/adapter/types.d.ts +85 -0
  41. package/dist/types/lib/adapter/types.d.ts.map +1 -0
  42. package/dist/types/lib/index.d.ts +4 -0
  43. package/dist/types/lib/index.d.ts.map +1 -0
  44. package/dist/types/lib/renderer/Extractor.d.ts +93 -0
  45. package/dist/types/lib/renderer/Extractor.d.ts.map +1 -0
  46. package/dist/types/lib/renderer/JSXRenderer.d.ts +163 -0
  47. package/dist/types/lib/renderer/JSXRenderer.d.ts.map +1 -0
  48. package/dist/types/lib/renderer/SitemapRenderer.d.ts +153 -0
  49. package/dist/types/lib/renderer/SitemapRenderer.d.ts.map +1 -0
  50. package/dist/types/lib/renderer/TextRenderer.d.ts +83 -0
  51. package/dist/types/lib/renderer/TextRenderer.d.ts.map +1 -0
  52. package/dist/types/lib/renderer/constants.d.ts.map +1 -0
  53. package/dist/types/lib/renderer/index.d.ts +7 -0
  54. package/dist/types/lib/renderer/index.d.ts.map +1 -0
  55. package/dist/types/lib/renderer/types.d.ts +161 -0
  56. package/dist/types/lib/renderer/types.d.ts.map +1 -0
  57. package/dist/types/lib/server/index.d.ts +3 -0
  58. package/dist/types/lib/server/index.d.ts.map +1 -0
  59. package/dist/types/lib/server/serveStream.d.ts +41 -0
  60. package/dist/types/lib/server/serveStream.d.ts.map +1 -0
  61. package/dist/types/lib/server/serveString.d.ts +37 -0
  62. package/dist/types/lib/server/serveString.d.ts.map +1 -0
  63. package/package.json +37 -17
  64. package/dist/esm/renderer/Extractor.js +0 -127
  65. package/dist/esm/renderer/Extractor.js.map +0 -1
  66. package/dist/esm/renderer/JSXRenderer.js +0 -168
  67. package/dist/esm/renderer/JSXRenderer.js.map +0 -1
  68. package/dist/esm/renderer/constants.js.map +0 -1
  69. package/dist/esm/renderer/index.js +0 -4
  70. package/dist/esm/renderer/index.js.map +0 -1
  71. package/dist/esm/renderer/types.js +0 -2
  72. package/dist/esm/renderer/types.js.map +0 -1
  73. package/dist/esm/server/index.js +0 -2
  74. package/dist/esm/server/index.js.map +0 -1
  75. package/dist/esm/server/serve-express.js +0 -58
  76. package/dist/esm/server/serve-express.js.map +0 -1
  77. package/dist/esm/server/serve.js +0 -41
  78. package/dist/esm/server/serve.js.map +0 -1
  79. package/dist/types/renderer/Extractor.d.ts +0 -68
  80. package/dist/types/renderer/Extractor.d.ts.map +0 -1
  81. package/dist/types/renderer/JSXRenderer.d.ts +0 -71
  82. package/dist/types/renderer/JSXRenderer.d.ts.map +0 -1
  83. package/dist/types/renderer/constants.d.ts.map +0 -1
  84. package/dist/types/renderer/index.d.ts +0 -5
  85. package/dist/types/renderer/index.d.ts.map +0 -1
  86. package/dist/types/renderer/types.d.ts +0 -35
  87. package/dist/types/renderer/types.d.ts.map +0 -1
  88. package/dist/types/server/index.d.ts +0 -2
  89. package/dist/types/server/index.d.ts.map +0 -1
  90. package/dist/types/server/serve-express.d.ts +0 -3
  91. package/dist/types/server/serve-express.d.ts.map +0 -1
  92. package/dist/types/server/serve.d.ts +0 -30
  93. package/dist/types/server/serve.d.ts.map +0 -1
  94. /package/dist/esm/{renderer → lib/renderer}/constants.js +0 -0
  95. /package/dist/types/{renderer → lib/renderer}/constants.d.ts +0 -0
@@ -0,0 +1,283 @@
1
+ import { createElement } from "react";
2
+ import { renderToPipeableStream as reactRenderToPipeableStream, renderToReadableStream as reactRenderToReadableStream, renderToString as reactRenderToString, } from "react-dom/server";
3
+ import { INITIAL_DATA_KEY } from "./constants.js";
4
+ import Extractor from "./Extractor.js";
5
+ /**
6
+ * Server-side renderer for a React component.
7
+ *
8
+ * Accepts a React `ServerEntrypoint` component, optional initial data for
9
+ * hydration, and an optional HTML shell string (from a Vite build) whose
10
+ * `<head>` tags are extracted and injected into the rendered output.
11
+ *
12
+ * Three rendering strategies are available:
13
+ *
14
+ * - **ReadableStream** (`renderToReadableStream`) — returns a web `ReadableStream`.
15
+ * Works natively with Bun, Deno, Cloudflare Workers, and any runtime that
16
+ * supports the Web Streams API. Supports Suspense and progressive rendering.
17
+ *
18
+ * - **PipeableStream** (`renderToPipeableStream`) — returns a Node.js pipeable stream.
19
+ * Works with Express, Fastify, and Node's built-in `http` module. Supports Suspense
20
+ * and progressive rendering.
21
+ *
22
+ * - **String** (`renderToString`) — returns the full HTML as a string. All Suspense
23
+ * boundaries resolve synchronously. The output is cacheable and works with Vite
24
+ * HMR in dev mode.
25
+ *
26
+ * All strategies inject `<script>` and `<link>` tags from the HTML shell via
27
+ * React's `bootstrapScripts` / `bootstrapModules` mechanism, and embed
28
+ * `initialData` as a global `window.__INITIAL_DATA__` variable for client
29
+ * hydration.
30
+ *
31
+ * The renderer is transport-agnostic — it never writes to a response object.
32
+ * HTTP status codes and metadata are exposed via `statusCode` and `statusReady`
33
+ * for the consumer to use when constructing the response.
34
+ *
35
+ * @typeParam TComponent - The server entrypoint component type.
36
+ * @typeParam InitialData - Shape of the data embedded for client hydration.
37
+ */
38
+ export default class JSXRenderer {
39
+ Component;
40
+ initialData;
41
+ options;
42
+ extractor;
43
+ /**
44
+ * HTTP status code determined during rendering.
45
+ *
46
+ * Starts at 200 and is set to 500 if a shell error occurs during streaming.
47
+ * For `renderToString`, it is always 200 (errors throw instead).
48
+ *
49
+ * Read this after the render method returns (for `renderToReadableStream` and
50
+ * `renderToString`) or after awaiting `statusReady` (for `renderToPipeableStream`).
51
+ */
52
+ statusCode = 200;
53
+ /**
54
+ * Resolves when `statusCode` is determined.
55
+ *
56
+ * For `renderToReadableStream` and `renderToString`, this is already resolved
57
+ * by the time the method returns. For `renderToPipeableStream`, it resolves
58
+ * asynchronously when the shell is ready or errors.
59
+ */
60
+ statusReady = Promise.resolve();
61
+ /**
62
+ * Create a renderer bound to a specific component and initial data.
63
+ *
64
+ * If `options.htmlString` is provided, the HTML is parsed once to extract
65
+ * `<head>` elements. These elements are then available as React elements
66
+ * via `getComponentProps()` for injection during rendering.
67
+ *
68
+ * @param Component - The React server entrypoint component.
69
+ * @param initialData - Data to embed in `window.__INITIAL_DATA__` for client hydration.
70
+ * @param options - Renderer configuration: locale, HTML shell, and stream options.
71
+ */
72
+ constructor(Component, initialData = {}, options = {}) {
73
+ this.Component = Component;
74
+ this.initialData = initialData;
75
+ this.options = options;
76
+ this.extractor = this.options.htmlString
77
+ ? new Extractor(this.options.htmlString)
78
+ : undefined;
79
+ }
80
+ /**
81
+ * Return the locale for the rendered page.
82
+ *
83
+ * Defaults to `"en"` when no `defaultLocale` was provided in options.
84
+ * The locale is passed as the `lang` prop to the server entrypoint component,
85
+ * which typically sets it as the `<html lang>` attribute.
86
+ */
87
+ getLocale() {
88
+ return this.options.defaultLocale || "en";
89
+ }
90
+ /**
91
+ * Assemble the props passed to the server entrypoint component.
92
+ *
93
+ * Combines the locale, initial data, and (when an HTML shell was provided)
94
+ * the extracted script, link, and other head elements into a single props
95
+ * object conforming to `ServerEntrypointProps`.
96
+ */
97
+ getComponentProps() {
98
+ return {
99
+ lang: this.getLocale(),
100
+ scriptElements: this.extractor?.getScriptElements(),
101
+ linkElements: this.extractor?.getLinkElements(),
102
+ otherHeadElements: this.extractor?.getOtherHeadElements(),
103
+ initialData: this.initialData,
104
+ };
105
+ }
106
+ /**
107
+ * Extract `src` URLs from script elements that match a given loading strategy.
108
+ *
109
+ * Filters the provided React `<script>` elements by their `type` attribute:
110
+ * `"module"` selects ES module scripts, `"classic"` selects everything else.
111
+ * Returns only the `src` values, discarding inline scripts that have no `src`.
112
+ *
113
+ * @param scripts - React elements representing `<script>` tags.
114
+ * @param type - `"module"` for ES modules, `"classic"` for traditional scripts.
115
+ * @returns An array of script source URLs.
116
+ */
117
+ getScriptSourcesByType(scripts, type) {
118
+ return scripts
119
+ .map((script) => script)
120
+ .filter((script) => {
121
+ if (type === "module") {
122
+ return script.props.type === "module";
123
+ }
124
+ return script.props.type !== "module";
125
+ })
126
+ .map((script) => script.props.src)
127
+ .filter((src) => typeof src === "string");
128
+ }
129
+ /**
130
+ * Merge renderer-managed options into the user-provided stream options.
131
+ *
132
+ * Populates three React streaming options unless the caller already supplied them:
133
+ *
134
+ * - `bootstrapScriptContent` — a `<script>` body that assigns `initialData` to
135
+ * `window.__INITIAL_DATA__`. The JSON is escaped to prevent `</script>` injection.
136
+ * - `bootstrapScripts` — `src` URLs for classic (non-module) scripts extracted from
137
+ * the HTML shell. React strips `<script>` tags during streaming, so these must
138
+ * be re-injected through this mechanism.
139
+ * - `bootstrapModules` — same as above, for ES module scripts.
140
+ *
141
+ * @param props - The assembled component props (used to read `initialData` and `scriptElements`).
142
+ * @returns A merged options object safe to pass to either streaming API.
143
+ */
144
+ enrichRendererOptions(props) {
145
+ const enrichedOptions = {
146
+ ...this.options.renderToPipeableStreamOptions,
147
+ };
148
+ if (!enrichedOptions.bootstrapScriptContent) {
149
+ if (props.initialData) {
150
+ enrichedOptions.bootstrapScriptContent = `window.${INITIAL_DATA_KEY} = ${JSON.stringify(props.initialData).replace(/</g, "\\u003c")}`;
151
+ }
152
+ }
153
+ if (!enrichedOptions.bootstrapScripts) {
154
+ if (props.scriptElements) {
155
+ enrichedOptions.bootstrapScripts = this.getScriptSourcesByType(props.scriptElements, "classic");
156
+ }
157
+ }
158
+ if (!enrichedOptions.bootstrapModules) {
159
+ if (props.scriptElements) {
160
+ enrichedOptions.bootstrapModules = this.getScriptSourcesByType(props.scriptElements, "module");
161
+ }
162
+ }
163
+ return enrichedOptions;
164
+ }
165
+ /**
166
+ * Render the component to a web `ReadableStream`.
167
+ *
168
+ * Uses `react-dom/server.renderToReadableStream` for environments that support
169
+ * the Web Streams API (Bun, Deno, Cloudflare Workers, browsers). Supports
170
+ * Suspense and progressive rendering.
171
+ *
172
+ * On shell error, `statusCode` is set to 500 and a fallback HTML stream is
173
+ * returned. On success, `statusCode` is 200.
174
+ *
175
+ * @note This method is impure — it mutates `statusCode` and `statusReady`.
176
+ *
177
+ * @param signal - Optional `AbortSignal` for request cancellation.
178
+ * @returns A `ReadableStream` of the rendered HTML.
179
+ */
180
+ renderToReadableStream = async (signal) => {
181
+ const props = this.getComponentProps();
182
+ const jsx = createElement(this.Component, props);
183
+ const { onError: onErrorCallback,
184
+ // Strip pipeable-only callbacks — they don't exist on RenderToReadableStreamOptions
185
+ onShellReady: _onShellReady, onShellError: _onShellError, onAllReady: _onAllReady, ...options } = this.enrichRendererOptions(props);
186
+ try {
187
+ const stream = await reactRenderToReadableStream(jsx, {
188
+ ...options,
189
+ ...(this.options.renderToReadableStreamOptions ?? {}),
190
+ signal,
191
+ onError: (error, errorInfo) => {
192
+ onErrorCallback?.(error, errorInfo);
193
+ console.error(error);
194
+ },
195
+ });
196
+ this.statusCode = 200;
197
+ this.statusReady = Promise.resolve();
198
+ return stream;
199
+ }
200
+ catch (error) {
201
+ console.error(error);
202
+ this.statusCode = 500;
203
+ this.statusReady = Promise.resolve();
204
+ return new ReadableStream({
205
+ start(controller) {
206
+ controller.enqueue(new TextEncoder().encode("<h1>Something went wrong</h1>"));
207
+ controller.close();
208
+ },
209
+ });
210
+ }
211
+ };
212
+ /**
213
+ * Render the component to a Node.js pipeable stream.
214
+ *
215
+ * Uses `react-dom/server.renderToPipeableStream` for Node.js environments
216
+ * (Express, Fastify, plain `http.createServer`). Supports Suspense and
217
+ * progressive rendering.
218
+ *
219
+ * Returns `{ pipe, abort }` synchronously. The `statusCode` is set
220
+ * asynchronously when the shell is ready or errors — await `statusReady`
221
+ * before reading it.
222
+ *
223
+ * @note This method is impure — it mutates `statusCode` and `statusReady`.
224
+ *
225
+ * @returns The pipe/abort handles for the rendered stream.
226
+ */
227
+ renderToPipeableStream = () => {
228
+ const props = this.getComponentProps();
229
+ const jsx = createElement(this.Component, props);
230
+ const { onShellError: onShellErrorCallback, onShellReady: onShellReadyCallback, onAllReady: onAllReadyCallback, onError: onErrorCallback, ...options } = this.enrichRendererOptions(props);
231
+ const errorRef = { current: undefined };
232
+ let resolveStatus;
233
+ this.statusReady = new Promise((resolve) => {
234
+ resolveStatus = resolve;
235
+ });
236
+ const jsxStream = reactRenderToPipeableStream(jsx, {
237
+ ...options,
238
+ onError(error, errorInfo) {
239
+ onErrorCallback?.(error, errorInfo);
240
+ errorRef.current = error;
241
+ console.error(error);
242
+ },
243
+ onShellError: (error) => {
244
+ onShellErrorCallback?.(error);
245
+ this.statusCode = 500;
246
+ resolveStatus();
247
+ console.error(error);
248
+ },
249
+ onShellReady: () => {
250
+ onShellReadyCallback?.();
251
+ /* v8 ignore next -- errorRef.current is set by onError which fires before onShellReady in edge cases */
252
+ this.statusCode = errorRef.current ? 500 : 200;
253
+ resolveStatus();
254
+ },
255
+ onAllReady() {
256
+ onAllReadyCallback?.();
257
+ },
258
+ });
259
+ return { pipe: jsxStream.pipe, abort: jsxStream.abort };
260
+ };
261
+ /**
262
+ * Render the component to a complete HTML string.
263
+ *
264
+ * Uses `react-dom/server.renderToString` to produce the full HTML
265
+ * synchronously. All Suspense boundaries resolve before the method returns.
266
+ * The output is cacheable and compatible with Vite's HMR in dev mode.
267
+ *
268
+ * Sets `statusCode` to 200 on success. On error, throws (consumer catches).
269
+ *
270
+ * @note This method is impure — it mutates `statusCode`.
271
+ *
272
+ * @returns The complete HTML string.
273
+ */
274
+ renderToString = () => {
275
+ const props = this.getComponentProps();
276
+ const jsx = createElement(this.Component, props);
277
+ const html = reactRenderToString(jsx);
278
+ this.statusCode = 200;
279
+ this.statusReady = Promise.resolve();
280
+ return html;
281
+ };
282
+ }
283
+ //# sourceMappingURL=JSXRenderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JSXRenderer.js","sourceRoot":"","sources":["../../../../src/lib/renderer/JSXRenderer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,241 @@
1
+ import { Readable } from "node:stream";
2
+ /**
3
+ * Renders an XML sitemap from a set of async data sources.
4
+ *
5
+ * Unlike `JSXRenderer`, this renderer produces XML (not HTML) and does not use
6
+ * React. The render pipeline is functional: each stage returns data rather than
7
+ * mutating instance state, so calling render methods multiple times produces
8
+ * identical results for the same underlying data.
9
+ *
10
+ * Implements the same three render methods as `JSXRenderer` — `renderToReadableStream`,
11
+ * `renderToPipeableStream`, and `renderToString` — with the same `statusCode` /
12
+ * `statusReady` metadata contract. This allows consumers to use either renderer
13
+ * interchangeably.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const renderer = new SitemapRenderer(
18
+ * [() => fetchPages(), () => fetchPosts()],
19
+ * { baseUrl: "https://example.com", defaultChangefreq: "weekly" },
20
+ * );
21
+ *
22
+ * const stream = await renderer.renderToReadableStream();
23
+ * return new Response(stream, {
24
+ * status: renderer.statusCode,
25
+ * headers: { "Content-Type": "application/xml; charset=utf-8" },
26
+ * });
27
+ * ```
28
+ */
29
+ export default class SitemapRenderer {
30
+ getters;
31
+ config;
32
+ /**
33
+ * HTTP status code determined during rendering.
34
+ *
35
+ * Set to 200 on successful render. Errors from getters propagate as
36
+ * thrown exceptions rather than setting this to 500 — the consumer's
37
+ * error handler decides the status code for unexpected failures.
38
+ */
39
+ statusCode = 200;
40
+ /**
41
+ * Resolves when `statusCode` is determined.
42
+ *
43
+ * For all three render methods on `SitemapRenderer`, this resolves by
44
+ * the time the method's returned Promise settles (or synchronously for
45
+ * the portions that are sync).
46
+ */
47
+ statusReady = Promise.resolve();
48
+ /**
49
+ * Create a sitemap renderer.
50
+ *
51
+ * @param getters - Async functions that each return a batch of sitemap items.
52
+ * Called concurrently via `Promise.all` during rendering.
53
+ * @param config - Base URL and optional defaults for changefreq / priority.
54
+ */
55
+ constructor(getters, config) {
56
+ this.getters = getters;
57
+ this.config = config;
58
+ }
59
+ /**
60
+ * Load sitemap items from all configured getters.
61
+ *
62
+ * Calls every getter concurrently and flattens the results into a single
63
+ * array. This is the only async step in the pipeline.
64
+ *
65
+ * @note This method is impure — it calls external async data sources.
66
+ * @returns A flat array of raw sitemap items from all getters.
67
+ */
68
+ async loadItems() {
69
+ const results = await Promise.all(this.getters.map((getter) => getter()));
70
+ return results.flat();
71
+ }
72
+ /**
73
+ * Resolve URLs and apply defaults to raw sitemap items.
74
+ *
75
+ * For each item:
76
+ * - Relative `loc` values are resolved against `config.baseUrl`. An empty
77
+ * `loc` resolves to the base URL itself.
78
+ * - `lastmod` dates are formatted to `YYYY-MM-DD` (ISO 8601 date-only).
79
+ * - Missing `changefreq` and `priority` are filled from `config` defaults.
80
+ *
81
+ * @param items - Raw items as returned by `loadItems`.
82
+ * @returns A new array of items with resolved URLs and applied defaults.
83
+ */
84
+ formatItems(items) {
85
+ return items.map((item) => ({
86
+ loc: item.loc.length
87
+ ? new URL(item.loc, this.config.baseUrl).href
88
+ : this.config.baseUrl,
89
+ lastmod: item.lastmod != null
90
+ ? SitemapRenderer.formatDate(item.lastmod)
91
+ : undefined,
92
+ changefreq: item.changefreq ?? this.config.defaultChangefreq,
93
+ priority: item.priority ?? this.config.defaultPriority,
94
+ }));
95
+ }
96
+ /**
97
+ * Format a `Date` object or ISO string to a `YYYY-MM-DD` date string.
98
+ *
99
+ * The Sitemaps protocol specifies W3C Datetime format; the date-only
100
+ * variant (`YYYY-MM-DD`) is the most common form used in practice.
101
+ *
102
+ * @param date - A `Date` instance or an ISO 8601 date/datetime string.
103
+ * @returns The date formatted as `YYYY-MM-DD`.
104
+ */
105
+ static formatDate(date) {
106
+ const d = typeof date === "string" ? new Date(date) : date;
107
+ return d.toISOString().slice(0, 10);
108
+ }
109
+ /**
110
+ * Escape the five XML special characters in a string.
111
+ *
112
+ * Prevents malformed XML when interpolating user-supplied URLs that may
113
+ * contain `&` (common in query strings), `<`, `>`, `"`, or `'`.
114
+ *
115
+ * @param value - The raw string to escape.
116
+ * @returns The string with `&`, `<`, `>`, `"`, and `'` replaced by their XML entities.
117
+ */
118
+ static escapeXml(value) {
119
+ return value
120
+ .replace(/&/g, "&amp;")
121
+ .replace(/</g, "&lt;")
122
+ .replace(/>/g, "&gt;")
123
+ .replace(/"/g, "&quot;")
124
+ .replace(/'/g, "&apos;");
125
+ }
126
+ /**
127
+ * Serialise a list of formatted sitemap items into an XML sitemap string.
128
+ *
129
+ * Produces a complete `<?xml>` document with a `<urlset>` root element
130
+ * conforming to the Sitemaps 0.9 schema. Only non-null optional fields
131
+ * (`lastmod`, `changefreq`, `priority`) are included in the output.
132
+ *
133
+ * @param items - Formatted items (URLs resolved, dates formatted).
134
+ * @returns The complete XML sitemap as a string.
135
+ */
136
+ toXml(items) {
137
+ const urlEntries = items
138
+ .map((item) => {
139
+ const parts = [
140
+ ` <loc>${SitemapRenderer.escapeXml(String(item.loc))}</loc>`,
141
+ ];
142
+ if (item.lastmod != null) {
143
+ parts.push(` <lastmod>${SitemapRenderer.escapeXml(String(item.lastmod))}</lastmod>`);
144
+ }
145
+ if (item.changefreq != null) {
146
+ parts.push(` <changefreq>${item.changefreq}</changefreq>`);
147
+ }
148
+ if (item.priority != null) {
149
+ parts.push(` <priority>${item.priority}</priority>`);
150
+ }
151
+ return ` <url>\n${parts.join("\n")}\n </url>`;
152
+ })
153
+ .join("\n");
154
+ return [
155
+ '<?xml version="1.0" encoding="UTF-8"?>',
156
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
157
+ urlEntries,
158
+ "</urlset>",
159
+ ].join("\n");
160
+ }
161
+ /**
162
+ * Build the full XML string by loading items, applying formatting, and
163
+ * serialising to XML.
164
+ */
165
+ async buildXml() {
166
+ const rawItems = await this.loadItems();
167
+ const items = this.formatItems(rawItems);
168
+ return this.toXml(items);
169
+ }
170
+ /**
171
+ * Render the sitemap to a web `ReadableStream`.
172
+ *
173
+ * Loads items from all getters, formats them, serialises to XML, and
174
+ * wraps the result in a `ReadableStream`. Sets `statusCode` to 200.
175
+ *
176
+ * @note This method is impure — it calls external data sources and mutates `statusCode`.
177
+ *
178
+ * @param _signal - Accepted for API compatibility with `JSXRenderer`. Not used.
179
+ * @returns A `ReadableStream` of the XML sitemap.
180
+ */
181
+ renderToReadableStream = async (_signal) => {
182
+ const xml = await this.buildXml();
183
+ this.statusCode = 200;
184
+ this.statusReady = Promise.resolve();
185
+ return new ReadableStream({
186
+ start(controller) {
187
+ controller.enqueue(new TextEncoder().encode(xml));
188
+ controller.close();
189
+ },
190
+ });
191
+ };
192
+ /**
193
+ * Render the sitemap to a Node.js pipeable stream.
194
+ *
195
+ * Returns `{ pipe, abort }` synchronously. The actual XML generation is
196
+ * async (getters are called), so data is pushed to the stream when ready.
197
+ * `statusReady` resolves when the XML is built and `statusCode` is set.
198
+ *
199
+ * @note This method is impure — it calls external data sources and mutates `statusCode`.
200
+ *
201
+ * @returns The pipe/abort handles for the XML stream.
202
+ */
203
+ renderToPipeableStream = () => {
204
+ /* v8 ignore next -- read() is a required no-op for push-based Readable streams */
205
+ const readable = new Readable({ read() { } });
206
+ let aborted = false;
207
+ this.statusReady = this.buildXml().then((xml) => {
208
+ this.statusCode = 200;
209
+ if (!aborted) {
210
+ readable.push(xml);
211
+ readable.push(null);
212
+ }
213
+ });
214
+ return {
215
+ pipe: (destination) => readable.pipe(destination),
216
+ abort: () => {
217
+ aborted = true;
218
+ readable.destroy();
219
+ },
220
+ };
221
+ };
222
+ /**
223
+ * Render the sitemap to a complete XML string.
224
+ *
225
+ * Loads items from all getters, formats them, and returns the serialised
226
+ * XML. Sets `statusCode` to 200.
227
+ *
228
+ * Async because the getters are async.
229
+ *
230
+ * @note This method is impure — it calls external data sources and mutates `statusCode`.
231
+ *
232
+ * @returns The complete XML sitemap string.
233
+ */
234
+ renderToString = async () => {
235
+ const xml = await this.buildXml();
236
+ this.statusCode = 200;
237
+ this.statusReady = Promise.resolve();
238
+ return xml;
239
+ };
240
+ }
241
+ //# sourceMappingURL=SitemapRenderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SitemapRenderer.js","sourceRoot":"","sources":["../../../../src/lib/renderer/SitemapRenderer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ import { Readable } from "node:stream";
2
+ /**
3
+ * Renders a plain-text document from async data sources.
4
+ *
5
+ * Produces output suitable for `llms.txt`, `humans.txt`, `security.txt`, or
6
+ * any text file that requires dynamic data (e.g. fetching page descriptions
7
+ * from a CMS for an LLM context file).
8
+ *
9
+ * The renderer is intentionally minimal — it accepts an array of async getters,
10
+ * each returning a string, and concatenates the results. No structure is imposed.
11
+ * The consumer controls formatting entirely.
12
+ *
13
+ * Implements the same three render methods and `statusCode` / `statusReady`
14
+ * contract as `JSXRenderer` and `SitemapRenderer`.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const renderer = new TextRenderer([
19
+ * async () => "# My App\n\nContext for LLMs about this application.\n",
20
+ * async () => {
21
+ * const pages = await fetchPages();
22
+ * return pages.map(p => `- ${p.title}: ${p.url}`).join("\n");
23
+ * },
24
+ * ]);
25
+ *
26
+ * const stream = await renderer.renderToReadableStream();
27
+ * return new Response(stream, {
28
+ * status: renderer.statusCode,
29
+ * headers: { "Content-Type": "text/plain; charset=utf-8" },
30
+ * });
31
+ * ```
32
+ */
33
+ export default class TextRenderer {
34
+ getters;
35
+ /**
36
+ * HTTP status code determined during rendering.
37
+ *
38
+ * Set to 200 on successful render. Errors from getters propagate as
39
+ * thrown exceptions — the consumer's error handler decides the status code.
40
+ */
41
+ statusCode = 200;
42
+ /**
43
+ * Resolves when `statusCode` is determined.
44
+ */
45
+ statusReady = Promise.resolve();
46
+ /**
47
+ * Create a text renderer.
48
+ *
49
+ * @param getters - Async functions that each return a string. Called
50
+ * sequentially (order matters) and concatenated into the final document.
51
+ */
52
+ constructor(getters) {
53
+ this.getters = getters;
54
+ }
55
+ /**
56
+ * Build the full text by calling all getters sequentially and concatenating.
57
+ */
58
+ async buildText() {
59
+ const parts = [];
60
+ for (const getter of this.getters) {
61
+ parts.push(await getter());
62
+ }
63
+ return parts.join("");
64
+ }
65
+ /**
66
+ * Render to a web `ReadableStream`.
67
+ *
68
+ * @note This method is impure — it calls external data sources and mutates `statusCode`.
69
+ *
70
+ * @param _signal - Accepted for API compatibility. Not used.
71
+ * @returns A `ReadableStream` of the plain-text document.
72
+ */
73
+ renderToReadableStream = async (_signal) => {
74
+ const text = await this.buildText();
75
+ this.statusCode = 200;
76
+ this.statusReady = Promise.resolve();
77
+ return new ReadableStream({
78
+ start(controller) {
79
+ controller.enqueue(new TextEncoder().encode(text));
80
+ controller.close();
81
+ },
82
+ });
83
+ };
84
+ /**
85
+ * Render to a Node.js pipeable stream.
86
+ *
87
+ * Returns `{ pipe, abort }` synchronously. Data is pushed when ready.
88
+ *
89
+ * @note This method is impure — it calls external data sources and mutates `statusCode`.
90
+ */
91
+ renderToPipeableStream = () => {
92
+ /* v8 ignore next -- read() is a required no-op for push-based Readable streams */
93
+ const readable = new Readable({ read() { } });
94
+ let aborted = false;
95
+ this.statusReady = this.buildText().then((text) => {
96
+ this.statusCode = 200;
97
+ if (!aborted) {
98
+ readable.push(text);
99
+ readable.push(null);
100
+ }
101
+ });
102
+ return {
103
+ pipe: (destination) => readable.pipe(destination),
104
+ abort: () => {
105
+ aborted = true;
106
+ readable.destroy();
107
+ },
108
+ };
109
+ };
110
+ /**
111
+ * Render to a complete string.
112
+ *
113
+ * Async because the getters are async.
114
+ *
115
+ * @note This method is impure — it calls external data sources and mutates `statusCode`.
116
+ */
117
+ renderToString = async () => {
118
+ const text = await this.buildText();
119
+ this.statusCode = 200;
120
+ this.statusReady = Promise.resolve();
121
+ return text;
122
+ };
123
+ }
124
+ //# sourceMappingURL=TextRenderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TextRenderer.js","sourceRoot":"","sources":["../../../../src/lib/renderer/TextRenderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,OAAO,OAAO,YAAY;IAoBA;IAnB/B;;;;;OAKG;IACI,UAAU,GAAG,GAAG,CAAC;IAExB;;OAEG;IACI,WAAW,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAEtD;;;;;OAKG;IACH,YAA+B,OAA8B;QAA9B,YAAO,GAAP,OAAO,CAAuB;IAAG,CAAC;IAEjE;;OAEG;IACO,KAAK,CAAC,SAAS;QACvB,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,KAAK,CAAC,IAAI,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC;QAC7B,CAAC;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACxB,CAAC;IAED;;;;;;;OAOG;IACH,sBAAsB,GAAG,KAAK,EAC5B,OAAqB,EACI,EAAE;QAC3B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QACpC,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;QACtB,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;QAErC,OAAO,IAAI,cAAc,CAAC;YACxB,KAAK,CAAC,UAAU;gBACd,UAAU,CAAC,OAAO,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;gBACnD,UAAU,CAAC,KAAK,EAAE,CAAC;YACrB,CAAC;SACF,CAAC,CAAC;IACL,CAAC,CAAC;IAEF;;;;;;OAMG;IACH,sBAAsB,GAAG,GAAyB,EAAE;QAClD,kFAAkF;QAClF,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC,EAAE,IAAI,KAAI,CAAC,EAAE,CAAC,CAAC;QAC7C,IAAI,OAAO,GAAG,KAAK,CAAC;QAEpB,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;YAChD,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;YACtB,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACpB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO;YACL,IAAI,EAAE,CAAkC,WAAc,EAAE,EAAE,CACxD,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC;YAC5B,KAAK,EAAE,GAAG,EAAE;gBACV,OAAO,GAAG,IAAI,CAAC;gBACf,QAAQ,CAAC,OAAO,EAAE,CAAC;YACrB,CAAC;SACF,CAAC;IACJ,CAAC,CAAC;IAEF;;;;;;OAMG;IACH,cAAc,GAAG,KAAK,IAAqB,EAAE;QAC3C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;QACpC,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;QACtB,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;QACrC,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;CACH"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../../../src/lib/renderer/constants.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC"}