@quilted/quilt 0.5.151 → 0.5.153

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 (186) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/build/cjs/assets.cjs +39 -0
  3. package/build/cjs/async.cjs +32 -0
  4. package/build/cjs/events.cjs +46 -0
  5. package/build/cjs/graphql/testing.cjs +2 -6
  6. package/build/cjs/html/HTML.cjs +21 -0
  7. package/build/cjs/html.cjs +33 -6
  8. package/build/cjs/http.cjs +16 -0
  9. package/build/cjs/index.cjs +0 -319
  10. package/build/cjs/localize.cjs +46 -0
  11. package/build/cjs/navigate/testing.cjs +12 -0
  12. package/build/cjs/navigate.cjs +70 -0
  13. package/build/cjs/performance.cjs +26 -0
  14. package/build/cjs/polyfills/fetch-get-set-cookie.cjs +4 -0
  15. package/build/cjs/react/testing.cjs +30 -0
  16. package/build/cjs/react/tools.cjs +19 -0
  17. package/build/cjs/react.cjs +15 -0
  18. package/build/cjs/server/ServerContext.cjs +1 -1
  19. package/build/cjs/server/preload.cjs +1 -1
  20. package/build/cjs/server/request-router.cjs +215 -231
  21. package/build/cjs/{server/index.cjs → server.cjs} +26 -52
  22. package/build/cjs/signals.cjs +51 -0
  23. package/build/cjs/static/StaticContext.cjs +1 -1
  24. package/build/cjs/static/index.cjs +43 -51
  25. package/build/cjs/static/render.cjs +1 -1
  26. package/build/cjs/testing.cjs +6 -34
  27. package/build/esm/assets.mjs +2 -0
  28. package/build/esm/async.mjs +3 -0
  29. package/build/esm/events.mjs +1 -0
  30. package/build/esm/graphql/testing.mjs +1 -1
  31. package/build/esm/html/HTML.mjs +19 -0
  32. package/build/esm/html.mjs +3 -1
  33. package/build/esm/http.mjs +1 -1
  34. package/build/esm/index.mjs +1 -15
  35. package/build/esm/localize.mjs +1 -0
  36. package/build/esm/navigate/testing.mjs +1 -0
  37. package/build/esm/navigate.mjs +1 -0
  38. package/build/esm/performance.mjs +1 -0
  39. package/build/esm/polyfills/fetch-get-set-cookie.mjs +1 -0
  40. package/build/esm/react/testing.mjs +3 -0
  41. package/build/esm/react/tools.mjs +2 -0
  42. package/build/esm/react.mjs +2 -0
  43. package/build/esm/server/ServerContext.mjs +2 -2
  44. package/build/esm/server/preload.mjs +2 -2
  45. package/build/esm/server/request-router.mjs +218 -232
  46. package/build/esm/server.mjs +10 -0
  47. package/build/esm/signals.mjs +2 -0
  48. package/build/esm/static/StaticContext.mjs +2 -2
  49. package/build/esm/static/index.mjs +40 -48
  50. package/build/esm/static/render.mjs +2 -2
  51. package/build/esm/testing.mjs +0 -3
  52. package/build/esnext/assets.esnext +2 -0
  53. package/build/esnext/async.esnext +3 -0
  54. package/build/esnext/events.esnext +1 -0
  55. package/build/esnext/graphql/testing.esnext +1 -1
  56. package/build/esnext/html/HTML.esnext +19 -0
  57. package/build/esnext/html.esnext +3 -1
  58. package/build/esnext/http.esnext +1 -1
  59. package/build/esnext/index.esnext +1 -15
  60. package/build/esnext/localize.esnext +1 -0
  61. package/build/esnext/navigate/testing.esnext +1 -0
  62. package/build/esnext/navigate.esnext +1 -0
  63. package/build/esnext/performance.esnext +1 -0
  64. package/build/esnext/polyfills/fetch-get-set-cookie.esnext +1 -0
  65. package/build/esnext/react/testing.esnext +3 -0
  66. package/build/esnext/react/tools.esnext +2 -0
  67. package/build/esnext/react.esnext +2 -0
  68. package/build/esnext/server/ServerContext.esnext +2 -2
  69. package/build/esnext/server/preload.esnext +2 -2
  70. package/build/esnext/server/request-router.esnext +218 -232
  71. package/build/esnext/server.esnext +10 -0
  72. package/build/esnext/signals.esnext +2 -0
  73. package/build/esnext/static/StaticContext.esnext +2 -2
  74. package/build/esnext/static/index.esnext +40 -48
  75. package/build/esnext/static/render.esnext +2 -2
  76. package/build/esnext/testing.esnext +0 -3
  77. package/build/tsconfig.tsbuildinfo +1 -1
  78. package/build/typescript/assets.d.ts +3 -1
  79. package/build/typescript/assets.d.ts.map +1 -1
  80. package/build/typescript/async.d.ts +6 -0
  81. package/build/typescript/async.d.ts.map +1 -0
  82. package/build/typescript/events.d.ts +3 -0
  83. package/build/typescript/events.d.ts.map +1 -0
  84. package/build/typescript/globals.d.ts +8 -0
  85. package/build/typescript/globals.d.ts.map +1 -0
  86. package/build/typescript/graphql/testing/matchers/operations.d.ts +7 -0
  87. package/build/typescript/graphql/testing/matchers/operations.d.ts.map +1 -0
  88. package/build/typescript/graphql/testing/matchers/utilities.d.ts +8 -0
  89. package/build/typescript/graphql/testing/matchers/utilities.d.ts.map +1 -0
  90. package/build/typescript/graphql/testing/matchers.d.ts +11 -0
  91. package/build/typescript/graphql/testing/matchers.d.ts.map +1 -0
  92. package/build/typescript/graphql/testing.d.ts +2 -1
  93. package/build/typescript/graphql/testing.d.ts.map +1 -1
  94. package/build/typescript/html/HTML.d.ts +5 -0
  95. package/build/typescript/html/HTML.d.ts.map +1 -0
  96. package/build/typescript/html.d.ts +3 -1
  97. package/build/typescript/html.d.ts.map +1 -1
  98. package/build/typescript/http.d.ts +1 -1
  99. package/build/typescript/http.d.ts.map +1 -1
  100. package/build/typescript/index.d.ts +1 -24
  101. package/build/typescript/index.d.ts.map +1 -1
  102. package/build/typescript/localize.d.ts +3 -0
  103. package/build/typescript/localize.d.ts.map +1 -0
  104. package/build/typescript/magic/assets.d.ts +4 -2
  105. package/build/typescript/magic/assets.d.ts.map +1 -1
  106. package/build/typescript/navigate/testing.d.ts +2 -0
  107. package/build/typescript/navigate/testing.d.ts.map +1 -0
  108. package/build/typescript/navigate.d.ts +3 -0
  109. package/build/typescript/navigate.d.ts.map +1 -0
  110. package/build/typescript/performance.d.ts +3 -0
  111. package/build/typescript/performance.d.ts.map +1 -0
  112. package/build/typescript/polyfills/fetch-get-set-cookie.d.ts +2 -0
  113. package/build/typescript/polyfills/fetch-get-set-cookie.d.ts.map +1 -0
  114. package/build/typescript/react/testing.d.ts +5 -0
  115. package/build/typescript/react/testing.d.ts.map +1 -0
  116. package/build/typescript/react/tools.d.ts +3 -0
  117. package/build/typescript/react/tools.d.ts.map +1 -0
  118. package/build/typescript/react-dom.d.ts +3 -0
  119. package/build/typescript/react-dom.d.ts.map +1 -0
  120. package/build/typescript/react.d.ts +3 -0
  121. package/build/typescript/react.d.ts.map +1 -0
  122. package/build/typescript/routing.d.ts +3 -0
  123. package/build/typescript/routing.d.ts.map +1 -0
  124. package/build/typescript/server/ServerContext.d.ts +2 -2
  125. package/build/typescript/server/request-router.d.ts +13 -37
  126. package/build/typescript/server/request-router.d.ts.map +1 -1
  127. package/build/typescript/server.d.ts +14 -0
  128. package/build/typescript/server.d.ts.map +1 -0
  129. package/build/typescript/signals.d.ts +3 -0
  130. package/build/typescript/signals.d.ts.map +1 -0
  131. package/build/typescript/static/StaticContext.d.ts +2 -2
  132. package/build/typescript/static/index.d.ts.map +1 -1
  133. package/build/typescript/static/render.d.ts +2 -2
  134. package/build/typescript/testing.d.ts +0 -4
  135. package/build/typescript/testing.d.ts.map +1 -1
  136. package/package.json +158 -54
  137. package/source/assets.ts +20 -2
  138. package/source/async.ts +17 -0
  139. package/source/events.ts +20 -0
  140. package/source/graphql/testing/matchers.ts +37 -0
  141. package/source/graphql/testing.ts +3 -2
  142. package/source/html/HTML.tsx +22 -0
  143. package/source/html.ts +15 -3
  144. package/source/http.ts +4 -0
  145. package/source/index.ts +1 -149
  146. package/source/localize.ts +21 -0
  147. package/source/magic/assets.ts +7 -4
  148. package/source/navigate/testing.ts +1 -0
  149. package/source/navigate.ts +24 -0
  150. package/source/performance.ts +12 -0
  151. package/source/polyfills/fetch-get-set-cookie.ts +1 -0
  152. package/source/react/testing.ts +30 -0
  153. package/source/react/tools.ts +2 -0
  154. package/source/routing.ts +24 -0
  155. package/source/server/ServerContext.tsx +3 -3
  156. package/source/server/preload.ts +2 -2
  157. package/source/server/request-router.tsx +281 -403
  158. package/source/{server/index.ts → server.ts} +20 -46
  159. package/source/signals.ts +12 -0
  160. package/source/static/StaticContext.tsx +3 -3
  161. package/source/static/index.tsx +40 -56
  162. package/source/static/render.tsx +2 -2
  163. package/source/testing.ts +0 -29
  164. package/build/cjs/App.cjs +0 -72
  165. package/build/cjs/TestApp.cjs +0 -20
  166. package/build/cjs/matchers.cjs +0 -4
  167. package/build/esm/App.mjs +0 -70
  168. package/build/esm/TestApp.mjs +0 -18
  169. package/build/esm/matchers.mjs +0 -1
  170. package/build/esm/server/index.mjs +0 -10
  171. package/build/esnext/App.esnext +0 -70
  172. package/build/esnext/TestApp.esnext +0 -18
  173. package/build/esnext/matchers.esnext +0 -1
  174. package/build/esnext/server/index.esnext +0 -10
  175. package/source/App.tsx +0 -162
  176. package/source/TestApp.tsx +0 -33
  177. package/source/matchers/graphql.ts +0 -24
  178. package/source/matchers.ts +0 -3
  179. /package/build/cjs/{global.cjs → globals.cjs} +0 -0
  180. /package/build/esm/{global.mjs → globals.mjs} +0 -0
  181. /package/build/esnext/{global.esnext → globals.esnext} +0 -0
  182. /package/source/{global.ts → globals.ts} +0 -0
  183. /package/source/{matchers/graphql → graphql/testing/matchers}/operations.ts +0 -0
  184. /package/source/{matchers/graphql → graphql/testing/matchers}/utilities.ts +0 -0
  185. /package/source/{react-dom/index.ts → react-dom.ts} +0 -0
  186. /package/source/{react/index.ts → react.ts} +0 -0
@@ -1,9 +1,8 @@
1
- import {Fragment, type ReactElement} from 'react';
1
+ import {isValidElement, type ReactElement} from 'react';
2
+ import {renderToStaticMarkup} from 'react-dom/server';
2
3
 
3
4
  import {
4
- styleAssetAttributes,
5
5
  styleAssetPreloadAttributes,
6
- scriptAssetAttributes,
7
6
  scriptAssetPreloadAttributes,
8
7
  type AssetsCacheKey,
9
8
  type BrowserAssets,
@@ -12,451 +11,313 @@ import {
12
11
  import {AssetsManager} from '@quilted/react-assets/server';
13
12
  import {HttpManager} from '@quilted/react-http/server';
14
13
  import {
15
- renderHtmlToString,
16
- HtmlManager,
17
- Html,
18
- type HtmlProps,
14
+ Head,
15
+ Script,
16
+ ScriptPreload,
17
+ Style,
18
+ StylePreload,
19
+ HTMLManager,
19
20
  } from '@quilted/react-html/server';
20
- import type {
21
- Options as ExtractOptions,
22
- ServerRenderRequestContext,
23
- } from '@quilted/react-server-render/server';
24
21
  import {extract} from '@quilted/react-server-render/server';
25
22
 
26
- import {html, redirect} from '@quilted/request-router';
27
- import type {
28
- EnhancedRequest,
29
- RequestHandler,
30
- RequestContext,
31
- } from '@quilted/request-router';
23
+ import {HTMLResponse, RedirectResponse} from '@quilted/request-router';
32
24
 
33
25
  import {ServerContext} from './ServerContext.tsx';
34
26
 
35
- export interface ServerRenderOptions<
36
- Context = RequestContext,
37
- CacheKey = AssetsCacheKey,
38
- > {
39
- stream?: 'headers' | false;
40
- assets?: BrowserAssets<CacheKey>;
41
- extract?: Omit<ExtractOptions, 'context'> & {
42
- readonly context?:
43
- | ServerRenderRequestContext
44
- | ((
45
- request: EnhancedRequest,
46
- context: Context,
47
- ) => ServerRenderRequestContext);
48
- };
49
- html?:
50
- | ServerRenderHtmlRender<Context>
51
- | {
52
- readonly rootElement?: HtmlProps['rootElement'];
53
- };
54
- }
55
-
56
- export interface ServerRenderHtmlRender<Context> {
57
- (
58
- content: string | undefined,
59
- details: Pick<ServerRenderAppDetails, 'http' | 'html'> & {
60
- readonly request: EnhancedRequest;
61
- readonly context: Context;
27
+ export interface RenderOptions<CacheKey = AssetsCacheKey> {
28
+ readonly request: Request;
29
+ readonly stream?: 'headers' | false;
30
+ readonly assets?: BrowserAssets<CacheKey>;
31
+ readonly cacheKey?: CacheKey;
32
+ waitUntil?(promise: Promise<any>): void;
33
+ renderHTML?(
34
+ content: ReadableStream<string>,
35
+ context: {
36
+ readonly manager: HTMLManager;
62
37
  readonly assets?: BrowserAssetsEntry;
63
38
  readonly preloadAssets?: BrowserAssetsEntry;
64
- readonly rootElement?: HtmlProps['rootElement'];
65
39
  },
66
- ): ReactElement<any> | Promise<ReactElement<any>>;
40
+ ): ReadableStream<any> | Promise<ReadableStream<any>>;
67
41
  }
68
42
 
69
- export interface ServerRenderAppDetails<
70
- _Context = RequestContext,
71
- CacheKey = AssetsCacheKey,
72
- > {
73
- readonly http: HttpManager;
74
- readonly html: HtmlManager;
75
- readonly assets: AssetsManager<CacheKey>;
76
- readonly rendered?: string;
77
- }
43
+ export async function renderToResponse<CacheKey = AssetsCacheKey>(
44
+ element: ReactElement<any>,
45
+ options: RenderOptions<CacheKey>,
46
+ ): Promise<HTMLResponse | RedirectResponse>;
47
+ export async function renderToResponse<CacheKey = AssetsCacheKey>(
48
+ options: RenderOptions<CacheKey>,
49
+ ): Promise<HTMLResponse | RedirectResponse>;
50
+ export async function renderToResponse<CacheKey = AssetsCacheKey>(
51
+ optionsOrElement: ReactElement<any> | RenderOptions<CacheKey>,
52
+ definitelyOptions?: RenderOptions<CacheKey>,
53
+ ) {
54
+ let element: ReactElement<any> | undefined;
55
+ let options: RenderOptions<CacheKey>;
78
56
 
79
- export function createServerRender<
80
- Context = RequestContext,
81
- CacheKey = AssetsCacheKey,
82
- >(
83
- getApp:
84
- | ReactElement<any>
85
- | ((
86
- request: EnhancedRequest,
87
- context: Context,
88
- ) =>
89
- | ReactElement<any>
90
- | undefined
91
- | Promise<ReactElement<any> | undefined>),
92
- options?: ServerRenderOptions<Context, CacheKey>,
93
- ): RequestHandler<Context>;
94
- export function createServerRender<
95
- Context = RequestContext,
96
- CacheKey = AssetsCacheKey,
97
- >(options?: ServerRenderOptions<Context, CacheKey>): RequestHandler<Context>;
98
- export function createServerRender<
99
- Context = RequestContext,
100
- CacheKey = AssetsCacheKey,
101
- >(
102
- ...args: [
103
- (
104
- | ReactElement<any>
105
- | ((
106
- request: EnhancedRequest,
107
- context: Context,
108
- ) =>
109
- | ReactElement<any>
110
- | undefined
111
- | Promise<ReactElement<any> | undefined>)
112
- | ServerRenderOptions<Context, CacheKey>
113
- | undefined
114
- ),
115
- ServerRenderOptions<Context, CacheKey>?,
116
- ]
117
- ): RequestHandler<Context> {
118
- let getApp:
119
- | ReactElement<any>
120
- | ((
121
- request: EnhancedRequest,
122
- context: Context,
123
- ) =>
124
- | ReactElement<any>
125
- | undefined
126
- | Promise<ReactElement<any> | undefined>)
127
- | undefined;
128
- let options: ServerRenderOptions<Context, CacheKey>;
129
-
130
- if (args.length > 1) {
131
- getApp = args[0] as any;
132
- options = (args[1] ?? {}) as any;
57
+ if (isValidElement(optionsOrElement)) {
58
+ element = optionsOrElement;
59
+ options = definitelyOptions!;
133
60
  } else {
134
- options = (args[0] ?? {}) as any;
61
+ options = optionsOrElement as any;
135
62
  }
136
63
 
137
- const stream = options.stream;
138
-
139
- return async (request, requestContext) => {
140
- const accepts = request.headers.get('Accept');
141
-
142
- if (accepts != null && !accepts.includes('text/html')) return;
64
+ const {
65
+ request,
66
+ stream: shouldStream = false,
67
+ assets,
68
+ cacheKey: explicitCacheKey,
69
+ waitUntil = noop,
70
+ renderHTML,
71
+ } = options;
72
+
73
+ const baseUrl = (request as any).URL ?? new URL(request.url);
74
+
75
+ const cacheKey =
76
+ explicitCacheKey ??
77
+ (((await assets?.cacheKey?.(request)) ?? {}) as CacheKey);
78
+
79
+ const html = new HTMLManager();
80
+ const http = new HttpManager({headers: request.headers});
81
+ const assetsManager = new AssetsManager<CacheKey>({cacheKey});
82
+
83
+ let responseStatus = 200;
84
+ let appHeaders: Headers | undefined;
85
+ let appStream: ReadableStream<any> | undefined;
86
+
87
+ if (shouldStream === false && element != null) {
88
+ const rendered = await extract(element, {
89
+ decorate(element) {
90
+ return (
91
+ <ServerContext
92
+ http={http}
93
+ html={html}
94
+ url={baseUrl}
95
+ assets={assetsManager}
96
+ >
97
+ {element}
98
+ </ServerContext>
99
+ );
100
+ },
101
+ });
143
102
 
144
- const renderResponse = stream
145
- ? renderAppToStreamedResponse
146
- : renderAppToResponse;
103
+ const {headers, statusCode = 200, redirectUrl} = http.state;
147
104
 
148
- return renderResponse(
149
- typeof getApp === 'function'
150
- ? () => (getApp as any)(request, requestContext)
151
- : getApp,
152
- {
153
- ...options,
105
+ if (redirectUrl) {
106
+ return new RedirectResponse(redirectUrl, {
107
+ status: statusCode as 301,
108
+ headers,
154
109
  request,
155
- context: requestContext,
156
- extract: {
157
- ...options.extract,
158
- context:
159
- typeof options.extract?.context === 'function'
160
- ? options.extract.context(request, requestContext)
161
- : options.extract?.context,
162
- },
163
- },
164
- );
165
- };
166
- }
110
+ });
111
+ }
167
112
 
168
- export async function renderAppToResponse<
169
- Context = RequestContext,
170
- CacheKey = AssetsCacheKey,
171
- >(
172
- getApp:
173
- | ReactElement<any>
174
- | (() =>
175
- | ReactElement<any>
176
- | undefined
177
- | Promise<ReactElement<any> | undefined>)
178
- | undefined,
179
- {
180
- request,
181
- context,
182
- assets,
183
- extract,
184
- html: htmlOptions,
185
- }: Pick<
186
- ServerRenderOptions<Context, CacheKey>,
187
- 'assets' | 'html' | 'extract'
188
- > & {readonly request: EnhancedRequest; readonly context: Context},
189
- ) {
190
- const app = typeof getApp === 'function' ? await getApp() : getApp;
191
- const cacheKey = (await assets?.cacheKey?.(request)) as CacheKey;
192
-
193
- const renderDetails = await serverRenderDetailsForApp(app, {
194
- extract,
195
- cacheKey,
196
- url: request.url,
197
- headers: request.headers,
198
- });
113
+ appHeaders = headers;
114
+ responseStatus = statusCode;
199
115
 
200
- const {headers, statusCode = 200, redirectUrl} = renderDetails.http.state;
116
+ const appTransformStream = new TransformStream();
117
+ const appWriter = appTransformStream.writable.getWriter();
118
+ appStream = appTransformStream.readable;
201
119
 
202
- if (redirectUrl) {
203
- return redirect(redirectUrl, {
204
- status: statusCode as 301,
205
- headers,
206
- });
120
+ appWriter.write(rendered);
121
+ appWriter.close();
207
122
  }
208
123
 
209
- const content = await renderAppDetailsToHtmlString<Context, CacheKey>(
210
- renderDetails,
211
- {
212
- request,
213
- context,
214
- assets,
215
- html: htmlOptions,
216
- },
217
- );
124
+ if (appStream == null) {
125
+ const appTransformStream = new TransformStream();
126
+ appStream = appTransformStream.readable;
127
+
128
+ const renderAppStream = async function renderAppStream() {
129
+ const appWriter = appTransformStream.writable.getWriter();
130
+
131
+ if (element != null) {
132
+ const rendered = await extract(element, {
133
+ decorate(element) {
134
+ return (
135
+ <ServerContext
136
+ http={http}
137
+ html={html}
138
+ url={baseUrl}
139
+ assets={assetsManager}
140
+ >
141
+ {element}
142
+ </ServerContext>
143
+ );
144
+ },
145
+ });
146
+
147
+ appWriter.write(rendered);
148
+ }
149
+
150
+ appWriter.close();
151
+ };
152
+
153
+ waitUntil(renderAppStream());
154
+ }
218
155
 
219
- return html(content, {
156
+ const {headers, body} = await renderToHTMLStream(appStream);
157
+
158
+ return new HTMLResponse(body, {
159
+ status: responseStatus,
220
160
  headers,
221
- status: statusCode,
222
161
  });
223
- }
224
162
 
225
- export async function renderAppToStreamedResponse<
226
- Context = RequestContext,
227
- CacheKey = AssetsCacheKey,
228
- >(
229
- getApp:
230
- | ReactElement<any>
231
- | (() =>
232
- | ReactElement<any>
233
- | undefined
234
- | Promise<ReactElement<any> | undefined>)
235
- | undefined,
236
- {
237
- request,
238
- context,
239
- assets,
240
- extract,
241
- html: htmlOptions,
242
- }: Pick<
243
- ServerRenderOptions<Context, CacheKey>,
244
- 'assets' | 'html' | 'extract'
245
- > & {readonly request: EnhancedRequest; readonly context: Context},
246
- ) {
247
- const headers = new Headers();
248
- const stream = new TransformStream();
163
+ async function renderToHTMLStream(content: ReadableStream<any>) {
164
+ const headers = new Headers(appHeaders);
165
+
166
+ const [synchronousAssets, preloadAssets] = await Promise.all([
167
+ assets?.entry({
168
+ cacheKey,
169
+ modules: assetsManager.usedModules({timing: 'load'}),
170
+ }),
171
+ assets?.modules(assetsManager.usedModules({timing: 'preload'}), {
172
+ cacheKey,
173
+ }),
174
+ ]);
175
+
176
+ if (synchronousAssets) {
177
+ for (const style of synchronousAssets.styles) {
178
+ headers.append(
179
+ 'Link',
180
+ preloadHeader(styleAssetPreloadAttributes(style)),
181
+ );
182
+ }
183
+
184
+ for (const script of synchronousAssets.scripts) {
185
+ headers.append(
186
+ 'Link',
187
+ preloadHeader(scriptAssetPreloadAttributes(script)),
188
+ );
189
+ }
190
+ }
249
191
 
250
- const cacheKey = (await assets?.cacheKey?.(request)) as CacheKey;
251
- const guaranteedAssets = await assets?.entry({cacheKey});
192
+ if (renderHTML) {
193
+ const body = await renderHTML(content, {
194
+ manager: html,
195
+ assets: synchronousAssets,
196
+ preloadAssets,
197
+ });
252
198
 
253
- if (guaranteedAssets) {
254
- for (const style of guaranteedAssets.styles) {
255
- headers.append('Link', preloadHeader(styleAssetPreloadAttributes(style)));
199
+ return {headers, body};
256
200
  }
257
201
 
258
- for (const script of guaranteedAssets.scripts) {
259
- headers.append(
260
- 'Link',
261
- preloadHeader(scriptAssetPreloadAttributes(script)),
202
+ const responseStream = new TextEncoderStream();
203
+ const body = responseStream.readable;
204
+
205
+ const renderFullHTML = async function renderFullHTML() {
206
+ const writer = responseStream.writable.getWriter();
207
+
208
+ writer.write(`<!DOCTYPE html>`);
209
+
210
+ const {htmlAttributes, bodyAttributes, ...headProps} = html.state;
211
+ const htmlContent = renderToStaticMarkup(
212
+ // eslint-disable-next-line jsx-a11y/html-has-lang
213
+ <html {...htmlAttributes}>
214
+ <head>
215
+ <Head {...headProps} />
216
+ {synchronousAssets?.scripts.map((script) => (
217
+ <Script key={script.source} asset={script} baseUrl={baseUrl} />
218
+ ))}
219
+ {synchronousAssets?.styles.map((style) => (
220
+ <Style key={style.source} asset={style} baseUrl={baseUrl} />
221
+ ))}
222
+ {preloadAssets?.styles.map((style) => (
223
+ <StylePreload
224
+ key={style.source}
225
+ asset={style}
226
+ baseUrl={baseUrl}
227
+ />
228
+ ))}
229
+ {preloadAssets?.scripts.map((script) => (
230
+ <ScriptPreload
231
+ key={script.source}
232
+ asset={script}
233
+ baseUrl={baseUrl}
234
+ />
235
+ ))}
236
+ </head>
237
+ <body
238
+ {...bodyAttributes}
239
+ dangerouslySetInnerHTML={{__html: '%%CONTENT%%'}}
240
+ ></body>
241
+ </html>,
262
242
  );
263
- }
264
- }
265
-
266
- renderResponseToStream();
267
-
268
- return html(stream.readable, {
269
- headers,
270
- status: 200,
271
- });
272
243
 
273
- async function renderResponseToStream() {
274
- const app = typeof getApp === 'function' ? await getApp() : getApp;
244
+ const [firstChunk, secondChunk] = htmlContent.split('%%CONTENT%%');
245
+ writer.write(firstChunk);
246
+ if (element != null) writer.write(`<div id="app">`);
275
247
 
276
- const renderDetails = await serverRenderDetailsForApp(app, {
277
- extract,
278
- cacheKey,
279
- url: request.url,
280
- headers: request.headers,
281
- });
248
+ const reader = content.getReader();
282
249
 
283
- const content = await renderAppDetailsToHtmlString<Context, CacheKey>(
284
- renderDetails,
285
- {
286
- request,
287
- context,
288
- assets,
289
- html: htmlOptions,
290
- },
291
- );
250
+ // eslint-disable-next-line no-constant-condition
251
+ while (true) {
252
+ const {done, value} = await reader.read();
292
253
 
293
- const encoder = new TextEncoder();
294
- const writer = stream.writable.getWriter();
295
- await writer.write(encoder.encode(content));
296
- await writer.close();
297
- }
298
- }
299
-
300
- async function serverRenderDetailsForApp<
301
- Context = RequestContext,
302
- CacheKey = AssetsCacheKey,
303
- >(
304
- app: ReactElement<any> | undefined,
305
- {
306
- url,
307
- headers,
308
- cacheKey,
309
- extract: extractOptions,
310
- }: Pick<ServerRenderOptions, 'extract'> & {
311
- url?: string | URL;
312
- cacheKey?: CacheKey;
313
- headers?: NonNullable<
314
- ConstructorParameters<typeof HttpManager>[0]
315
- >['headers'];
316
- } = {},
317
- ): Promise<ServerRenderAppDetails<Context, CacheKey>> {
318
- const html = new HtmlManager();
319
- const http = new HttpManager({headers});
320
- const assets = new AssetsManager<CacheKey>({cacheKey});
321
-
322
- const {decorate, ...rest} = extractOptions ?? {};
323
-
324
- const rendered = app
325
- ? await extract(app, {
326
- decorate(app) {
327
- return (
328
- <ServerContext http={http} html={html} url={url} assets={assets}>
329
- {decorate?.(app) ?? app}
330
- </ServerContext>
331
- );
332
- },
333
- ...rest,
334
- })
335
- : undefined;
336
-
337
- return {rendered, http, html, assets};
338
- }
254
+ if (done) {
255
+ break;
256
+ }
339
257
 
340
- async function renderAppDetailsToHtmlString<
341
- Context = RequestContext,
342
- CacheKey = AssetsCacheKey,
343
- >(
344
- details: ServerRenderAppDetails<Context, CacheKey>,
345
- {
346
- request,
347
- context,
348
- assets,
349
- html: htmlOptions,
350
- }: Pick<ServerRenderOptions<Context, CacheKey>, 'assets' | 'html'> & {
351
- readonly request: EnhancedRequest;
352
- readonly context: Context;
353
- readonly cacheKey?: Partial<CacheKey>;
354
- },
355
- ) {
356
- const {http, rendered, html: htmlManager, assets: assetsManager} = details;
258
+ writer.write(value);
259
+ }
357
260
 
358
- const cacheKey = assetsManager.cacheKey as CacheKey;
359
- const usedModules = assetsManager.usedModules({timing: 'load'});
261
+ if (element != null) writer.write(`</div>`);
360
262
 
361
- const [entryAssets, preloadAssets] = assets
362
- ? await Promise.all([
363
- assets.entry({modules: usedModules, cacheKey}),
364
- assets.modules(assetsManager.usedModules({timing: 'preload'}), {
263
+ const [newSynchronousAssets, newPreloadAssets] = await Promise.all([
264
+ assets?.entry({
365
265
  cacheKey,
266
+ modules: assetsManager.usedModules({timing: 'load'}),
366
267
  }),
367
- ])
368
- : [];
369
-
370
- let renderHtml: ServerRenderHtmlRender<Context>;
371
- let rootElement: HtmlProps['rootElement'];
372
-
373
- if (typeof htmlOptions === 'function') {
374
- renderHtml = htmlOptions;
375
- } else {
376
- rootElement = htmlOptions?.rootElement;
377
- renderHtml = defaultRenderHtml;
378
- }
268
+ assets?.modules(assetsManager.usedModules({timing: 'preload'}), {
269
+ cacheKey,
270
+ }),
271
+ ]);
379
272
 
380
- const htmlElement = await renderHtml(rendered, {
381
- request,
382
- context,
383
- html: htmlManager,
384
- http,
385
- assets: entryAssets,
386
- preloadAssets,
387
- rootElement,
388
- });
273
+ if (newSynchronousAssets) {
274
+ const diffedSynchronousAssets = diffBrowserAssetsEntries(
275
+ newSynchronousAssets,
276
+ synchronousAssets!,
277
+ );
389
278
 
390
- return renderHtmlToString(htmlElement);
391
- }
279
+ const diffedPreloadAssets = diffBrowserAssetsEntries(
280
+ newPreloadAssets!,
281
+ preloadAssets!,
282
+ );
392
283
 
393
- const defaultRenderHtml: ServerRenderHtmlRender<any> =
394
- function defaultRenderHtml(
395
- content,
396
- {request, html, assets, preloadAssets, rootElement},
397
- ) {
398
- const baseUrl = new URL(request.url);
399
-
400
- return (
401
- <Html
402
- manager={html}
403
- rootElement={rootElement}
404
- headEndContent={
284
+ const additionalAssetsContent = renderToStaticMarkup(
405
285
  <>
406
- {assets &&
407
- assets.styles.map((style) => {
408
- const attributes = styleAssetAttributes(style, {baseUrl});
409
- return <link key={style.source} {...(attributes as any)} />;
410
- })}
411
-
412
- {assets &&
413
- assets.scripts.map((script) => {
414
- const isModule = script.attributes?.type === 'module';
415
-
416
- const attributes = scriptAssetAttributes(script, {
417
- baseUrl,
418
- });
419
-
420
- if (isModule) {
421
- return (
422
- <Fragment key={script.source}>
423
- <link
424
- {...(scriptAssetPreloadAttributes(script) as any)}
425
- />
426
- <script {...(attributes as any)} async />
427
- </Fragment>
428
- );
429
- }
430
-
431
- return (
432
- <script key={script.source} {...(attributes as any)} defer />
433
- );
434
- })}
435
-
436
- {preloadAssets &&
437
- preloadAssets.styles.map((style) => {
438
- const attributes = styleAssetPreloadAttributes(style, {
439
- baseUrl,
440
- });
441
-
442
- return <link key={style.source} {...(attributes as any)} />;
443
- })}
444
-
445
- {preloadAssets &&
446
- preloadAssets.scripts.map((script) => {
447
- const attributes = scriptAssetPreloadAttributes(script, {
448
- baseUrl,
449
- });
450
-
451
- return <link key={script.source} {...(attributes as any)} />;
452
- })}
453
- </>
454
- }
455
- >
456
- {content}
457
- </Html>
458
- );
459
- };
286
+ {diffedSynchronousAssets.scripts.map((script) => (
287
+ <Script key={script.source} asset={script} baseUrl={baseUrl} />
288
+ ))}
289
+ {diffedSynchronousAssets.styles.map((style) => (
290
+ <Style key={style.source} asset={style} baseUrl={baseUrl} />
291
+ ))}
292
+ {diffedPreloadAssets.styles.map((style) => (
293
+ <StylePreload
294
+ key={style.source}
295
+ asset={style}
296
+ baseUrl={baseUrl}
297
+ />
298
+ ))}
299
+ {diffedPreloadAssets.scripts.map((script) => (
300
+ <ScriptPreload
301
+ key={script.source}
302
+ asset={script}
303
+ baseUrl={baseUrl}
304
+ />
305
+ ))}
306
+ </>,
307
+ );
308
+
309
+ writer.write(additionalAssetsContent);
310
+ }
311
+
312
+ writer.write(secondChunk);
313
+ writer.close();
314
+ };
315
+
316
+ waitUntil(renderFullHTML());
317
+
318
+ return {headers, body};
319
+ }
320
+ }
460
321
 
461
322
  function preloadHeader(attributes: Partial<HTMLLinkElement>) {
462
323
  const {
@@ -480,3 +341,20 @@ function preloadHeader(attributes: Partial<HTMLLinkElement>) {
480
341
 
481
342
  return header;
482
343
  }
344
+
345
+ function diffBrowserAssetsEntries(
346
+ newList: BrowserAssetsEntry,
347
+ oldList: BrowserAssetsEntry,
348
+ ): BrowserAssetsEntry {
349
+ const oldStyles = new Set(oldList.styles.map((style) => style.source));
350
+ const oldScripts = new Set(oldList.scripts.map((script) => script.source));
351
+
352
+ return {
353
+ styles: newList.styles.filter((style) => !oldStyles.has(style.source)),
354
+ scripts: newList.scripts.filter((script) => !oldScripts.has(script.source)),
355
+ };
356
+ }
357
+
358
+ function noop(..._args: any) {
359
+ // noop
360
+ }