@quilted/quilt 0.8.6 → 0.8.8

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.
@@ -1,38 +0,0 @@
1
- import type {RenderableProps, VNode} from 'preact';
2
-
3
- import {
4
- BrowserDetailsContext,
5
- BrowserAssetsManifestContext,
6
- type BrowserDetails,
7
- } from '@quilted/preact-browser/server';
8
-
9
- import {type BrowserAssets} from '@quilted/assets';
10
-
11
- interface Props {
12
- browser?: BrowserDetails;
13
- assets?: BrowserAssets;
14
- }
15
-
16
- export function ServerContext({
17
- browser,
18
- assets,
19
- children,
20
- }: RenderableProps<Props>) {
21
- const withBrowser = browser ? (
22
- <BrowserDetailsContext.Provider value={browser}>
23
- {children}
24
- </BrowserDetailsContext.Provider>
25
- ) : (
26
- children
27
- );
28
-
29
- const withAssets = assets ? (
30
- <BrowserAssetsManifestContext.Provider value={assets}>
31
- {withBrowser}
32
- </BrowserAssetsManifestContext.Provider>
33
- ) : (
34
- withBrowser
35
- );
36
-
37
- return withAssets as VNode<{}>;
38
- }
@@ -1,385 +0,0 @@
1
- import {isValidElement, type VNode, type JSX} from 'preact';
2
- import {
3
- renderToStaticMarkup,
4
- renderToStringAsync,
5
- } from 'preact-render-to-string';
6
-
7
- import {
8
- styleAssetPreloadAttributes,
9
- scriptAssetPreloadAttributes,
10
- type BrowserAssets,
11
- type BrowserAssetsEntry,
12
- } from '@quilted/assets';
13
- import {
14
- BrowserResponse,
15
- ScriptAsset,
16
- ScriptAssetPreload,
17
- StyleAsset,
18
- StyleAssetPreload,
19
- } from '@quilted/preact-browser/server';
20
-
21
- import {HTMLResponse, EnhancedResponse} from '@quilted/request-router';
22
-
23
- import {ServerContext} from './ServerContext.tsx';
24
-
25
- export interface RenderHTMLFunction {
26
- (
27
- content: ReadableStream<string>,
28
- context: {
29
- readonly response: BrowserResponse;
30
- },
31
- ): ReadableStream<any> | string | Promise<ReadableStream<any> | string>;
32
- }
33
-
34
- export interface RenderToResponseOptions {
35
- readonly request: Request;
36
- readonly status?: number;
37
- readonly stream?: 'headers' | false;
38
- readonly headers?: HeadersInit;
39
- readonly assets?: BrowserAssets;
40
- readonly serializations?: Iterable<[string, unknown]>;
41
- readonly renderHTML?: boolean | 'fragment' | 'document' | RenderHTMLFunction;
42
- waitUntil?(promise: Promise<any>): void;
43
- }
44
-
45
- export async function renderToResponse(
46
- element: VNode<any>,
47
- options: RenderToResponseOptions,
48
- ): Promise<EnhancedResponse>;
49
- export async function renderToResponse(
50
- options: RenderToResponseOptions,
51
- ): Promise<EnhancedResponse>;
52
- export async function renderToResponse(
53
- optionsOrElement: VNode<any> | RenderToResponseOptions,
54
- definitelyOptions?: RenderToResponseOptions,
55
- ): Promise<EnhancedResponse> {
56
- let element: VNode<any> | undefined;
57
- let options: RenderToResponseOptions;
58
-
59
- if (isValidElement(optionsOrElement)) {
60
- element = optionsOrElement;
61
- options = definitelyOptions!;
62
- } else {
63
- options = optionsOrElement as any;
64
- }
65
-
66
- const {
67
- request,
68
- assets,
69
- status: explicitStatus,
70
- headers: explicitHeaders,
71
- serializations: explicitSerializations,
72
- waitUntil = noop,
73
- stream: shouldStream = false,
74
- renderHTML = true,
75
- } = options;
76
-
77
- const baseURL = (request as any).URL ?? new URL(request.url);
78
-
79
- const browserResponse = new BrowserResponse({
80
- request,
81
- status: explicitStatus,
82
- headers: new Headers(explicitHeaders),
83
- serializations: explicitSerializations,
84
- });
85
-
86
- let appStream: ReadableStream<any> | undefined;
87
-
88
- if (shouldStream === false && element != null) {
89
- let rendered: string;
90
-
91
- try {
92
- rendered = await renderToStringAsync(
93
- <ServerContext assets={assets} browser={browserResponse}>
94
- {element}
95
- </ServerContext>,
96
- );
97
- } catch (error) {
98
- if (error instanceof Response) {
99
- const mergedHeaders = new Headers(browserResponse.headers);
100
-
101
- // Copy headers from error response, potentially overwriting existing ones
102
- for (const [key, value] of error.headers) {
103
- if (key.toLowerCase() === 'set-cookie') continue;
104
- mergedHeaders.set(key, value);
105
- }
106
-
107
- for (const setCookie of error.headers.getSetCookie()) {
108
- mergedHeaders.append('Set-Cookie', setCookie);
109
- }
110
-
111
- const mergedResponse = new EnhancedResponse(error.body, {
112
- status: Math.max(browserResponse.status.value, error.status),
113
- headers: mergedHeaders,
114
- });
115
-
116
- return mergedResponse;
117
- }
118
-
119
- throw error;
120
- }
121
-
122
- const appTransformStream = new TransformStream();
123
- const appWriter = appTransformStream.writable.getWriter();
124
- appStream = appTransformStream.readable;
125
-
126
- appWriter.write(rendered);
127
- appWriter.close();
128
- }
129
-
130
- if (appStream == null) {
131
- const appTransformStream = new TransformStream();
132
- appStream = appTransformStream.readable;
133
-
134
- const renderAppStream = async function renderAppStream() {
135
- const appWriter = appTransformStream.writable.getWriter();
136
-
137
- if (element != null) {
138
- // TODO: how could we handle redirects automatically? For now, if people want
139
- // this, they will explicitly turn on streaming and will have to use some in-app
140
- // to manually handle redirects (e.g., by rendering a script tag that uses JavaScript
141
- // to redirect)
142
- const rendered = await renderToStringAsync(
143
- <ServerContext assets={assets} browser={browserResponse}>
144
- {element}
145
- </ServerContext>,
146
- );
147
-
148
- appWriter.write(rendered);
149
- }
150
-
151
- appWriter.close();
152
- };
153
-
154
- waitUntil(renderAppStream());
155
- }
156
-
157
- const body = await renderToHTMLBody(appStream);
158
-
159
- return new HTMLResponse(body, {
160
- status: browserResponse.status.value,
161
- headers: browserResponse.headers,
162
- });
163
-
164
- async function renderToHTMLBody(
165
- content: ReadableStream<any>,
166
- ): Promise<ReadableStream<any> | string> {
167
- if (typeof renderHTML === 'function') {
168
- const body = await renderHTML(content, {
169
- response: browserResponse,
170
- });
171
-
172
- return body;
173
- } else if (renderHTML === false || renderHTML === 'fragment') {
174
- return content;
175
- }
176
-
177
- const responseStream = new TextEncoderStream();
178
- const body = responseStream.readable;
179
-
180
- const renderFullHTML = async function renderFullHTML() {
181
- const writer = responseStream.writable.getWriter();
182
-
183
- writer.write(`<!DOCTYPE html>`);
184
-
185
- const synchronousAssets = assets?.entry({
186
- request,
187
- modules: browserResponse.assets.get({timing: 'load'}),
188
- });
189
- const preloadAssets = assets?.modules(
190
- browserResponse.assets.get({timing: 'preload'}),
191
- {
192
- request,
193
- },
194
- );
195
-
196
- if (synchronousAssets) {
197
- for (const style of synchronousAssets.styles) {
198
- browserResponse.headers.append(
199
- 'Link',
200
- preloadHeader(styleAssetPreloadAttributes(style)),
201
- );
202
- }
203
-
204
- for (const script of synchronousAssets.scripts) {
205
- browserResponse.headers.append(
206
- 'Link',
207
- preloadHeader(scriptAssetPreloadAttributes(script)),
208
- );
209
- }
210
- }
211
-
212
- const htmlContent = renderToStaticMarkup(
213
- <html {...browserResponse.htmlAttributes.value}>
214
- <head>
215
- {browserResponse.title.value && (
216
- <title>{browserResponse.title.value}</title>
217
- )}
218
- {browserResponse.links.value.map((link) => (
219
- <link {...(link as JSX.HTMLAttributes<HTMLLinkElement>)} />
220
- ))}
221
- {browserResponse.metas.value.map((meta) => (
222
- <meta {...(meta as JSX.HTMLAttributes<HTMLMetaElement>)} />
223
- ))}
224
- {browserResponse.serializations.value.map(({name, content}) => (
225
- // @ts-expect-error a custom element that I don’t want to define,
226
- // since it’s an optional part of the browser library.
227
- <browser-serialization
228
- name={name}
229
- content={JSON.stringify(content)}
230
- />
231
- ))}
232
- {synchronousAssets?.scripts.map((script) => (
233
- <ScriptAsset
234
- key={script.source}
235
- asset={script}
236
- baseURL={baseURL}
237
- />
238
- ))}
239
- {synchronousAssets?.styles.map((style) => (
240
- <StyleAsset key={style.source} asset={style} baseURL={baseURL} />
241
- ))}
242
- {preloadAssets?.styles.map((style) => (
243
- <StyleAssetPreload
244
- key={style.source}
245
- asset={style}
246
- baseURL={baseURL}
247
- />
248
- ))}
249
- {preloadAssets?.scripts.map((script) => (
250
- <ScriptAssetPreload
251
- key={script.source}
252
- asset={script}
253
- baseURL={baseURL}
254
- />
255
- ))}
256
- </head>
257
- <body
258
- {...browserResponse.bodyAttributes.value}
259
- dangerouslySetInnerHTML={{__html: '%%CONTENT%%'}}
260
- ></body>
261
- </html>,
262
- );
263
-
264
- const [firstChunk, secondChunk] = htmlContent.split('%%CONTENT%%');
265
- writer.write(firstChunk);
266
- if (element != null) writer.write(`<div id="app">`);
267
-
268
- const reader = content.getReader();
269
-
270
- while (true) {
271
- const {done, value} = await reader.read();
272
-
273
- if (done) {
274
- break;
275
- }
276
-
277
- writer.write(value);
278
- }
279
-
280
- if (element != null) writer.write(`</div>`);
281
-
282
- const newSynchronousAssets = assets?.entry({
283
- request,
284
- modules: browserResponse.assets.get({timing: 'load'}),
285
- });
286
-
287
- const newPreloadAssets = assets?.modules(
288
- browserResponse.assets.get({timing: 'preload'}),
289
- {
290
- request,
291
- },
292
- );
293
-
294
- if (newSynchronousAssets) {
295
- const diffedSynchronousAssets = diffBrowserAssetsEntries(
296
- newSynchronousAssets,
297
- synchronousAssets!,
298
- );
299
-
300
- const diffedPreloadAssets = diffBrowserAssetsEntries(
301
- newPreloadAssets!,
302
- preloadAssets!,
303
- );
304
-
305
- const additionalAssetsContent = renderToStaticMarkup(
306
- <>
307
- {diffedSynchronousAssets.scripts.map((script) => (
308
- <ScriptAsset
309
- key={script.source}
310
- asset={script}
311
- baseURL={baseURL}
312
- />
313
- ))}
314
- {diffedSynchronousAssets.styles.map((style) => (
315
- <StyleAsset key={style.source} asset={style} baseURL={baseURL} />
316
- ))}
317
- {diffedPreloadAssets.styles.map((style) => (
318
- <StyleAssetPreload
319
- key={style.source}
320
- asset={style}
321
- baseURL={baseURL}
322
- />
323
- ))}
324
- {diffedPreloadAssets.scripts.map((script) => (
325
- <ScriptAssetPreload
326
- key={script.source}
327
- asset={script}
328
- baseURL={baseURL}
329
- />
330
- ))}
331
- </>,
332
- );
333
-
334
- writer.write(additionalAssetsContent);
335
- }
336
-
337
- writer.write(secondChunk);
338
- writer.close();
339
- };
340
-
341
- waitUntil(renderFullHTML());
342
-
343
- return body;
344
- }
345
- }
346
-
347
- function preloadHeader(attributes: Partial<HTMLLinkElement>) {
348
- const {
349
- as,
350
- rel = 'preload',
351
- href,
352
- crossOrigin,
353
- crossorigin,
354
- } = attributes as any;
355
-
356
- // Support both property and attribute versions of the casing
357
- const finalCrossOrigin = crossOrigin ?? crossorigin;
358
-
359
- let header = `<${href}>; rel="${rel}"; as="${as}"`;
360
-
361
- if (finalCrossOrigin === '' || finalCrossOrigin === true) {
362
- header += `; crossorigin`;
363
- } else if (typeof finalCrossOrigin === 'string') {
364
- header += `; crossorigin="${finalCrossOrigin}"`;
365
- }
366
-
367
- return header;
368
- }
369
-
370
- function diffBrowserAssetsEntries(
371
- newList: BrowserAssetsEntry,
372
- oldList: BrowserAssetsEntry,
373
- ): BrowserAssetsEntry {
374
- const oldStyles = new Set(oldList.styles.map((style) => style.source));
375
- const oldScripts = new Set(oldList.scripts.map((script) => script.source));
376
-
377
- return {
378
- styles: newList.styles.filter((style) => !oldStyles.has(style.source)),
379
- scripts: newList.scripts.filter((script) => !oldScripts.has(script.source)),
380
- };
381
- }
382
-
383
- function noop(..._args: any) {
384
- // noop
385
- }