@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.
- package/dist/esm/bin/serve-express.js +86 -0
- package/dist/esm/bin/serve-express.js.map +1 -0
- package/dist/esm/index.js +3 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/adapter/index.js +2 -0
- package/dist/esm/lib/adapter/index.js.map +1 -0
- package/dist/esm/lib/adapter/mime.js +85 -0
- package/dist/esm/lib/adapter/mime.js.map +1 -0
- package/dist/esm/lib/adapter/types.js +10 -0
- package/dist/esm/lib/adapter/types.js.map +1 -0
- package/dist/esm/lib/index.js +4 -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 +3 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/lib/adapter/index.d.ts +3 -0
- package/dist/types/lib/adapter/index.d.ts.map +1 -0
- package/dist/types/lib/adapter/mime.d.ts +43 -0
- package/dist/types/lib/adapter/mime.d.ts.map +1 -0
- package/dist/types/lib/adapter/types.d.ts +85 -0
- package/dist/types/lib/adapter/types.d.ts.map +1 -0
- package/dist/types/lib/index.d.ts +4 -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 +37 -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,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":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,EAEL,sBAAsB,IAAI,2BAA2B,EACrD,sBAAsB,IAAI,2BAA2B,EACrD,cAAc,IAAI,mBAAmB,GACtC,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,SAAS,MAAM,gBAAgB,CAAC;AAQvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,OAAO,OAAO,WAAW;IAsCT;IACA;IACA;IApCX,SAAS,CAAwB;IAE3C;;;;;;;;OAQG;IACI,UAAU,GAAG,GAAG,CAAC;IAExB;;;;;;OAMG;IACI,WAAW,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAEtD;;;;;;;;;;OAUG;IACH,YACqB,SAAqB,EACrB,cAA2B,EAAiB,EAC5C,UAA2B,EAAE;QAF7B,cAAS,GAAT,SAAS,CAAY;QACrB,gBAAW,GAAX,WAAW,CAAiC;QAC5C,YAAO,GAAP,OAAO,CAAsB;QAEhD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU;YACtC,CAAC,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC;YACxC,CAAC,CAAC,SAAS,CAAC;IAChB,CAAC;IAED;;;;;;OAMG;IACI,SAAS;QACd,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,IAAI,IAAI,CAAC;IAC5C,CAAC;IAED;;;;;;OAMG;IACO,iBAAiB;QACzB,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE;YACtB,cAAc,EAAE,IAAI,CAAC,SAAS,EAAE,iBAAiB,EAAE;YACnD,YAAY,EAAE,IAAI,CAAC,SAAS,EAAE,eAAe,EAAE;YAC/C,iBAAiB,EAAE,IAAI,CAAC,SAAS,EAAE,oBAAoB,EAAE;YACzD,WAAW,EAAE,IAAI,CAAC,WAAW;SACQ,CAAC;IAC1C,CAAC;IAED;;;;;;;;;;OAUG;IACO,sBAAsB,CAC9B,OAA6B,EAC7B,IAA0B;QAE1B,OAAO,OAAO;aACX,GAAG,CACF,CAAC,MAAM,EAAE,EAAE,CACT,MAGC,CACJ;aACA,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE;YACjB,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACtB,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC;YACxC,CAAC;YACD,OAAO,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC;QACxC,CAAC,CAAC;aACD,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;aACjC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC;IAC9C,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACO,qBAAqB,CAC7B,KAAyC;QAEzC,MAAM,eAAe,GAAG;YACtB,GAAG,IAAI,CAAC,OAAO,CAAC,6BAA6B;SAC9C,CAAC;QAEF,IAAI,CAAC,eAAe,CAAC,sBAAsB,EAAE,CAAC;YAC5C,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;gBACtB,eAAe,CAAC,sBAAsB,GAAG,UAAU,gBAAgB,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC;YACxI,CAAC;QACH,CAAC;QACD,IAAI,CAAC,eAAe,CAAC,gBAAgB,EAAE,CAAC;YACtC,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;gBACzB,eAAe,CAAC,gBAAgB,GAAG,IAAI,CAAC,sBAAsB,CAC5D,KAAK,CAAC,cAAc,EACpB,SAAS,CACV,CAAC;YACJ,CAAC;QACH,CAAC;QACD,IAAI,CAAC,eAAe,CAAC,gBAAgB,EAAE,CAAC;YACtC,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;gBACzB,eAAe,CAAC,gBAAgB,GAAG,IAAI,CAAC,sBAAsB,CAC5D,KAAK,CAAC,cAAc,EACpB,QAAQ,CACT,CAAC;YACJ,CAAC;QACH,CAAC;QAED,OAAO,eAAe,CAAC;IACzB,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACH,sBAAsB,GAAG,KAAK,EAC5B,MAAoB,EACK,EAAE;QAC3B,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACjD,MAAM,EACJ,OAAO,EAAE,eAAe;QACxB,oFAAoF;QACpF,YAAY,EAAE,aAAa,EAC3B,YAAY,EAAE,aAAa,EAC3B,UAAU,EAAE,WAAW,EACvB,GAAG,OAAO,EACX,GAAG,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC;QAEtC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,2BAA2B,CAAC,GAAG,EAAE;gBACpD,GAAG,OAAO;gBACV,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,6BAA6B,IAAI,EAAE,CAAC;gBACrD,MAAM;gBACN,OAAO,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE;oBAC5B,eAAe,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;oBACpC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBACvB,CAAC;aACF,CAAC,CAAC;YAEH,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;YACtB,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;YACrC,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACrB,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;YACtB,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;YACrC,OAAO,IAAI,cAAc,CAAC;gBACxB,KAAK,CAAC,UAAU;oBACd,UAAU,CAAC,OAAO,CAChB,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,+BAA+B,CAAC,CAC1D,CAAC;oBACF,UAAU,CAAC,KAAK,EAAE,CAAC;gBACrB,CAAC;aACF,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC;IAEF;;;;;;;;;;;;;;OAcG;IACH,sBAAsB,GAAG,GAAyB,EAAE;QAClD,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACjD,MAAM,EACJ,YAAY,EAAE,oBAAoB,EAClC,YAAY,EAAE,oBAAoB,EAClC,UAAU,EAAE,kBAAkB,EAC9B,OAAO,EAAE,eAAe,EACxB,GAAG,OAAO,EACX,GAAG,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC;QAEtC,MAAM,QAAQ,GAAmC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;QACxE,IAAI,aAAyB,CAAC;QAC9B,IAAI,CAAC,WAAW,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAC/C,aAAa,GAAG,OAAO,CAAC;QAC1B,CAAC,CAAC,CAAC;QAEH,MAAM,SAAS,GAAG,2BAA2B,CAAC,GAAG,EAAE;YACjD,GAAG,OAAO;YACV,OAAO,CAAC,KAAK,EAAE,SAAS;gBACtB,eAAe,EAAE,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;gBACpC,QAAQ,CAAC,OAAO,GAAG,KAAc,CAAC;gBAClC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACvB,CAAC;YACD,YAAY,EAAE,CAAC,KAAK,EAAE,EAAE;gBACtB,oBAAoB,EAAE,CAAC,KAAK,CAAC,CAAC;gBAC9B,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;gBACtB,aAAa,EAAE,CAAC;gBAChB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACvB,CAAC;YACD,YAAY,EAAE,GAAG,EAAE;gBACjB,oBAAoB,EAAE,EAAE,CAAC;gBACzB,wGAAwG;gBACxG,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;gBAC/C,aAAa,EAAE,CAAC;YAClB,CAAC;YACD,UAAU;gBACR,kBAAkB,EAAE,EAAE,CAAC;YACzB,CAAC;SACF,CAAC,CAAC;QAEH,OAAO,EAAE,IAAI,EAAE,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAC;IAC1D,CAAC,CAAC;IAEF;;;;;;;;;;;;OAYG;IACH,cAAc,GAAG,GAAW,EAAE;QAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACvC,MAAM,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACjD,MAAM,IAAI,GAAG,mBAAmB,CAAC,GAAG,CAAC,CAAC;QACtC,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,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, "&")
|
|
121
|
+
.replace(/</g, "<")
|
|
122
|
+
.replace(/>/g, ">")
|
|
123
|
+
.replace(/"/g, """)
|
|
124
|
+
.replace(/'/g, "'");
|
|
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"}
|