@bleedingdev/modern-js-runtime 3.9.0-ultramodern.9 → 3.9.3-ultramodern.2

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 (130) hide show
  1. package/dist/cjs/cli/code.js +2 -2
  2. package/dist/cjs/cli/index.js +0 -7
  3. package/dist/cjs/cli/ssr/loadable-bundler-plugin.js +2 -0
  4. package/dist/cjs/cli/template.server.js +1 -2
  5. package/dist/cjs/core/browser/hydrate.js +3 -9
  6. package/dist/cjs/core/browser/index.js +4 -5
  7. package/dist/cjs/core/context/index.js +8 -2
  8. package/dist/cjs/core/context/public.js +4 -4
  9. package/dist/cjs/core/context/runtime.js +3 -9
  10. package/dist/cjs/core/index.js +3 -3
  11. package/dist/cjs/core/plugin/index.js +2 -2
  12. package/dist/cjs/core/react/index.js +3 -3
  13. package/dist/cjs/core/react/wrapper.js +7 -8
  14. package/dist/cjs/core/server/requestHandler.js +253 -25
  15. package/dist/cjs/core/server/stream/beforeTemplate.js +3 -3
  16. package/dist/cjs/core/server/stream/beforeTemplate.worker.js +2 -2
  17. package/dist/cjs/core/server/stream/createReadableStream.js +51 -19
  18. package/dist/cjs/core/server/stream/createReadableStream.worker.js +54 -27
  19. package/dist/cjs/core/server/stream/deferredScript.js +12 -6
  20. package/dist/cjs/core/server/stream/deferredScriptOutputCoordinator.js +370 -0
  21. package/dist/cjs/core/server/stream/shared.js +4 -0
  22. package/dist/cjs/core/server/string/index.js +6 -4
  23. package/dist/cjs/core/server/string/loadable.js +4 -4
  24. package/dist/cjs/document/cli/index.js +14 -5
  25. package/dist/cjs/exports/head.js +2 -2
  26. package/dist/cjs/internal.js +2 -2
  27. package/dist/cjs/router/cli/code/nestedRoutes.js +1 -2
  28. package/dist/cjs/router/cli/code/templates.js +2 -2
  29. package/dist/cjs/router/cli/code/utils.js +5 -4
  30. package/dist/cjs/router/runtime/DeferredDataScripts.node.js +13 -10
  31. package/dist/cjs/router/runtime/PrefetchLink.js +4 -4
  32. package/dist/cjs/router/runtime/hooks.js +6 -7
  33. package/dist/cjs/router/runtime/plugin.js +22 -27
  34. package/dist/cjs/router/runtime/plugin.node.js +8 -9
  35. package/dist/esm/cli/code.mjs +2 -2
  36. package/dist/esm/cli/index.mjs +0 -7
  37. package/dist/esm/cli/ssr/loadable-bundler-plugin.mjs +2 -0
  38. package/dist/esm/cli/template.server.mjs +1 -2
  39. package/dist/esm/core/browser/hydrate.mjs +3 -9
  40. package/dist/esm/core/browser/index.mjs +1 -2
  41. package/dist/esm/core/context/index.mjs +1 -1
  42. package/dist/esm/core/context/public.mjs +1 -1
  43. package/dist/esm/core/context/runtime.mjs +3 -9
  44. package/dist/esm/core/index.mjs +1 -1
  45. package/dist/esm/core/plugin/index.mjs +1 -1
  46. package/dist/esm/core/react/index.mjs +1 -1
  47. package/dist/esm/core/react/wrapper.mjs +1 -2
  48. package/dist/esm/core/server/requestHandler.mjs +223 -19
  49. package/dist/esm/core/server/stream/beforeTemplate.mjs +3 -3
  50. package/dist/esm/core/server/stream/beforeTemplate.worker.mjs +2 -2
  51. package/dist/esm/core/server/stream/createReadableStream.mjs +48 -16
  52. package/dist/esm/core/server/stream/createReadableStream.worker.mjs +53 -26
  53. package/dist/esm/core/server/stream/deferredScript.mjs +12 -6
  54. package/dist/esm/core/server/stream/deferredScriptOutputCoordinator.mjs +332 -0
  55. package/dist/esm/core/server/stream/shared.mjs +2 -1
  56. package/dist/esm/core/server/string/index.mjs +3 -1
  57. package/dist/esm/core/server/string/loadable.mjs +4 -4
  58. package/dist/esm/document/cli/index.mjs +12 -6
  59. package/dist/esm/exports/head.mjs +1 -1
  60. package/dist/esm/internal.mjs +1 -1
  61. package/dist/esm/router/cli/code/nestedRoutes.mjs +1 -2
  62. package/dist/esm/router/cli/code/templates.mjs +2 -2
  63. package/dist/esm/router/cli/code/utils.mjs +6 -5
  64. package/dist/esm/router/runtime/DeferredDataScripts.node.mjs +13 -10
  65. package/dist/esm/router/runtime/PrefetchLink.mjs +1 -1
  66. package/dist/esm/router/runtime/hooks.mjs +6 -7
  67. package/dist/esm/router/runtime/plugin.mjs +14 -19
  68. package/dist/esm/router/runtime/plugin.node.mjs +3 -4
  69. package/dist/esm-node/cli/code.mjs +2 -2
  70. package/dist/esm-node/cli/index.mjs +1 -9
  71. package/dist/esm-node/cli/ssr/loadable-bundler-plugin.mjs +2 -0
  72. package/dist/esm-node/cli/template.server.mjs +1 -2
  73. package/dist/esm-node/core/browser/hydrate.mjs +3 -9
  74. package/dist/esm-node/core/browser/index.mjs +1 -2
  75. package/dist/esm-node/core/context/index.mjs +1 -1
  76. package/dist/esm-node/core/context/public.mjs +1 -1
  77. package/dist/esm-node/core/context/runtime.mjs +3 -9
  78. package/dist/esm-node/core/index.mjs +1 -1
  79. package/dist/esm-node/core/plugin/index.mjs +1 -1
  80. package/dist/esm-node/core/react/index.mjs +1 -1
  81. package/dist/esm-node/core/react/wrapper.mjs +1 -2
  82. package/dist/esm-node/core/server/requestHandler.mjs +223 -19
  83. package/dist/esm-node/core/server/stream/beforeTemplate.mjs +3 -3
  84. package/dist/esm-node/core/server/stream/beforeTemplate.worker.mjs +2 -2
  85. package/dist/esm-node/core/server/stream/createReadableStream.mjs +48 -16
  86. package/dist/esm-node/core/server/stream/createReadableStream.worker.mjs +53 -26
  87. package/dist/esm-node/core/server/stream/deferredScript.mjs +12 -6
  88. package/dist/esm-node/core/server/stream/deferredScriptOutputCoordinator.mjs +333 -0
  89. package/dist/esm-node/core/server/stream/shared.mjs +2 -1
  90. package/dist/esm-node/core/server/string/index.mjs +3 -1
  91. package/dist/esm-node/core/server/string/loadable.mjs +4 -4
  92. package/dist/esm-node/document/cli/index.mjs +12 -6
  93. package/dist/esm-node/exports/head.mjs +1 -1
  94. package/dist/esm-node/internal.mjs +1 -1
  95. package/dist/esm-node/router/cli/code/nestedRoutes.mjs +1 -2
  96. package/dist/esm-node/router/cli/code/templates.mjs +2 -2
  97. package/dist/esm-node/router/cli/code/utils.mjs +6 -5
  98. package/dist/esm-node/router/runtime/DeferredDataScripts.node.mjs +13 -10
  99. package/dist/esm-node/router/runtime/PrefetchLink.mjs +1 -1
  100. package/dist/esm-node/router/runtime/hooks.mjs +6 -7
  101. package/dist/esm-node/router/runtime/plugin.mjs +14 -19
  102. package/dist/esm-node/router/runtime/plugin.node.mjs +3 -4
  103. package/dist/types/common.d.ts +2 -8
  104. package/dist/types/core/context/index.d.ts +1 -1
  105. package/dist/types/core/context/public.d.ts +1 -1
  106. package/dist/types/core/index.d.ts +1 -1
  107. package/dist/types/core/react/wrapper.d.ts +1 -1
  108. package/dist/types/core/server/requestHandler.d.ts +30 -1
  109. package/dist/types/core/server/stream/deferredScript.d.ts +1 -1
  110. package/dist/types/core/server/stream/deferredScriptOutputCoordinator.d.ts +72 -0
  111. package/dist/types/core/server/stream/shared.d.ts +14 -0
  112. package/dist/types/document/cli/index.d.ts +1 -0
  113. package/dist/types/internal.d.ts +1 -1
  114. package/dist/types/router/cli/code/utils.d.ts +1 -1
  115. package/dist/types/router/runtime/hooks.d.ts +12 -16
  116. package/package.json +32 -26
  117. package/rslib.config.mts +3 -0
  118. package/rstest.config.mts +0 -4
  119. package/dist/cjs/core/server/requestResponse.js +0 -107
  120. package/dist/cjs/core/server/routerCleanup.js +0 -129
  121. package/dist/cjs/rsc/server.worker.js +0 -62
  122. package/dist/esm/core/server/requestResponse.mjs +0 -63
  123. package/dist/esm/core/server/routerCleanup.mjs +0 -82
  124. package/dist/esm/rsc/server.worker.mjs +0 -1
  125. package/dist/esm-node/core/server/requestResponse.mjs +0 -64
  126. package/dist/esm-node/core/server/routerCleanup.mjs +0 -83
  127. package/dist/esm-node/rsc/server.worker.mjs +0 -2
  128. package/dist/types/core/server/requestResponse.d.ts +0 -15
  129. package/dist/types/core/server/routerCleanup.d.ts +0 -31
  130. package/dist/types/rsc/server.worker.d.ts +0 -1
@@ -1,16 +1,196 @@
1
1
  import "node:module";
2
2
  import { jsx } from "react/jsx-runtime";
3
+ import { getGlobalInternalRuntimeContext, getGlobalRSCRoot, getInitialContext } from "@modern-js/runtime/context";
3
4
  import { storage } from "@modern-js/runtime-utils/node";
4
5
  import { getPathname, parseCookie, parseHeaders, parseQuery } from "@modern-js/runtime-utils/universal/request";
5
6
  import react, { Fragment } from "react";
6
- import { getGlobalInternalRuntimeContext, getGlobalRSCRoot } from "../context/index.mjs";
7
- import { getInitialContext } from "../context/runtime.mjs";
7
+ import { handleRSCRedirect } from "../../router/runtime/redirect.mjs";
8
8
  import { getServerPayload } from "../context/serverPayload/index.mjs";
9
9
  import { createRoot } from "../react/index.mjs";
10
10
  import { CHUNK_CSS_PLACEHOLDER } from "./constants.mjs";
11
- import { applyRouterSnapshotResult, createLoaderRedirectResponse, finalizeRenderResponse } from "./requestResponse.mjs";
12
- import { createRouterCleanup, finishWithRouterCleanup, runWithRouterCleanupOnError } from "./routerCleanup.mjs";
11
+ import { SSRErrors } from "./tracer.mjs";
13
12
  import { getSSRConfigByEntry, getSSRMode } from "./utils.mjs";
13
+ const REQUEST_END_ERROR = 'An error occurs during request lifecycle completion';
14
+ const RESPONSE_BODY_CANCEL_ERROR = 'An error occurs while cancelling a discarded response body';
15
+ async function runWithRequestLifecycleOnError(lifecycle, callback) {
16
+ try {
17
+ return await callback();
18
+ } catch (error) {
19
+ await lifecycle.run({
20
+ status: 'error',
21
+ error
22
+ });
23
+ throw error;
24
+ }
25
+ }
26
+ async function finishWithRequestLifecycle(lifecycle, callback) {
27
+ try {
28
+ return await callback();
29
+ } catch (error) {
30
+ if (!lifecycle.deferred) await lifecycle.run({
31
+ status: 'error',
32
+ error
33
+ });
34
+ throw error;
35
+ } finally{
36
+ if (!lifecycle.deferred) await lifecycle.run();
37
+ }
38
+ }
39
+ function createRequestLifecycle(onEnd, onError) {
40
+ let deferred = false;
41
+ let finished;
42
+ const run = (terminal = {
43
+ status: 'complete'
44
+ })=>{
45
+ finished ??= Promise.resolve().then(()=>onEnd(terminal)).catch((error)=>{
46
+ onError(error, REQUEST_END_ERROR);
47
+ });
48
+ return finished;
49
+ };
50
+ const deferUntilBodyDone = (response)=>{
51
+ const { body } = response;
52
+ if (!body) return response;
53
+ deferred = true;
54
+ if (body.locked) throw new TypeError('Cannot observe a locked response body before request completion');
55
+ let cancelled = false;
56
+ const reader = body.getReader();
57
+ const wrappedBody = new ReadableStream({
58
+ async pull (controller) {
59
+ let result;
60
+ try {
61
+ result = await reader.read();
62
+ } catch (error) {
63
+ if (!cancelled) {
64
+ await run({
65
+ status: 'error',
66
+ error
67
+ });
68
+ controller.error(error);
69
+ }
70
+ return;
71
+ }
72
+ if (cancelled) return;
73
+ if (result.done) {
74
+ await run();
75
+ if (!cancelled) controller.close();
76
+ return;
77
+ }
78
+ controller.enqueue(result.value);
79
+ },
80
+ async cancel (reason) {
81
+ cancelled = true;
82
+ try {
83
+ await reader.cancel(reason);
84
+ } catch {}
85
+ await run({
86
+ status: 'cancelled',
87
+ reason
88
+ });
89
+ }
90
+ });
91
+ return new Response(wrappedBody, {
92
+ status: response.status,
93
+ statusText: response.statusText,
94
+ headers: response.headers
95
+ });
96
+ };
97
+ const discardBody = async (response, terminal = {
98
+ status: 'discarded'
99
+ })=>{
100
+ const { body } = response;
101
+ if (!body) return void await run(terminal);
102
+ deferred = true;
103
+ if (body.locked) throw new TypeError('Cannot discard a locked response body before request completion');
104
+ try {
105
+ await body.cancel('Response body discarded during finalization');
106
+ } catch (error) {
107
+ await run('error' === terminal.status ? terminal : {
108
+ status: 'error',
109
+ error
110
+ });
111
+ if ('error' === terminal.status) onError(error, RESPONSE_BODY_CANCEL_ERROR);
112
+ throw error;
113
+ }
114
+ await run(terminal);
115
+ };
116
+ return {
117
+ get deferred () {
118
+ return deferred;
119
+ },
120
+ run,
121
+ deferUntilBodyDone,
122
+ discardBody
123
+ };
124
+ }
125
+ const isRedirectStatus = (status)=>301 === status || 302 === status || 303 === status || 307 === status || 308 === status;
126
+ const isNullBodyStatus = (status)=>204 === status || 205 === status || 304 === status;
127
+ const getRedirectLocation = (headers)=>{
128
+ const location = headers.get('Location');
129
+ return null !== location && '' !== location && URL.canParse(location, 'http://localhost') ? location : void 0;
130
+ };
131
+ const processRedirect = (headers, status, ctx)=>{
132
+ headers.delete('content-length');
133
+ headers.delete('transfer-encoding');
134
+ if (ctx.enableRsc && ctx.isRSCNavigation) return handleRSCRedirect(headers, ctx.basename, status);
135
+ return new Response(null, {
136
+ status,
137
+ headers
138
+ });
139
+ };
140
+ const applyRouterResult = (context, routerResult, onError)=>{
141
+ const routerStatusCode = routerResult?.statusCode;
142
+ if (void 0 !== routerStatusCode && 0 !== routerStatusCode && !Number.isNaN(routerStatusCode) && 200 !== routerStatusCode) context.ssrContext?.response.status(routerStatusCode);
143
+ const errors = Object.values(routerResult?.errors || {});
144
+ if (errors.length > 0) onError(errors[0], SSRErrors.LOADER_ERROR);
145
+ };
146
+ const createLoaderRedirectResponse = (beforeRenderResult, redirectCtx)=>{
147
+ if (void 0 === beforeRenderResult || !isRedirectStatus(beforeRenderResult.status)) return;
148
+ if (beforeRenderResult.headers.has('X-Modernjs-Redirect')) return beforeRenderResult;
149
+ const redirectUrl = getRedirectLocation(beforeRenderResult.headers);
150
+ if (void 0 === redirectUrl) return;
151
+ return processRedirect(new Headers(beforeRenderResult.headers), beforeRenderResult.status, redirectCtx);
152
+ };
153
+ const finalizeRenderResponse = async (response, responseProxy, redirectCtx, lifecycle)=>{
154
+ try {
155
+ const proxyHeaders = new Headers(responseProxy.headers);
156
+ if (-1 !== responseProxy.status && isRedirectStatus(responseProxy.status) && void 0 !== getRedirectLocation(proxyHeaders)) {
157
+ await lifecycle.discardBody(response);
158
+ return processRedirect(proxyHeaders, responseProxy.status, redirectCtx);
159
+ }
160
+ const headers = new Headers(response.headers);
161
+ Object.entries(responseProxy.headers).forEach(([key, value])=>{
162
+ headers.set(key, value);
163
+ });
164
+ if (-1 !== responseProxy.status) {
165
+ if (isNullBodyStatus(responseProxy.status)) {
166
+ await lifecycle.discardBody(response);
167
+ headers.delete('content-length');
168
+ headers.delete('transfer-encoding');
169
+ return new Response(null, {
170
+ status: responseProxy.status,
171
+ headers
172
+ });
173
+ }
174
+ if (response.body?.locked) lifecycle.deferUntilBodyDone(response);
175
+ return lifecycle.deferUntilBodyDone(new Response(response.body, {
176
+ status: responseProxy.status,
177
+ headers
178
+ }));
179
+ }
180
+ Object.entries(responseProxy.headers).forEach(([key, value])=>{
181
+ response.headers.set(key, value);
182
+ });
183
+ return lifecycle.deferUntilBodyDone(response);
184
+ } catch (error) {
185
+ if (!lifecycle.deferred) try {
186
+ await lifecycle.discardBody(response, {
187
+ status: 'error',
188
+ error
189
+ });
190
+ } catch {}
191
+ throw error;
192
+ }
193
+ };
14
194
  async function handleRSCRequest(request, Root, context, options, handleRequest) {
15
195
  const serverPayload = getServerPayload();
16
196
  if (void 0 !== serverPayload) return await handleRequest(request, Root, {
@@ -101,12 +281,14 @@ const createRequestHandler = async (handleRequest, createRequestOptions)=>{
101
281
  status: -1
102
282
  };
103
283
  const activeDeferreds = new Map();
284
+ const deferredScriptKeys = new Map();
104
285
  return storage.run({
105
286
  headers: headersData,
106
287
  request,
107
288
  monitors: options.monitors,
108
289
  responseProxy,
109
290
  activeDeferreds,
291
+ deferredScriptKeys,
110
292
  serverPayload: void 0
111
293
  }, async ()=>{
112
294
  const Root = createRoot();
@@ -131,28 +313,50 @@ const createRequestHandler = async (handleRequest, createRequestOptions)=>{
131
313
  isRSCNavigation: 'true' === request.headers.get('x-rsc-tree'),
132
314
  basename: ssrContext.baseUrl || '/'
133
315
  };
134
- const routerCleanup = createRouterCleanup(context, options.onError);
135
- const beforeRenderResult = await runWithRouterCleanupOnError(routerCleanup, ()=>runBeforeRender(context));
136
- await runWithRouterCleanupOnError(routerCleanup, ()=>{
137
- applyRouterSnapshotResult(context, options.onError);
138
- });
139
- if ("u" > typeof Response) {
140
- const redirectResponse = await runWithRouterCleanupOnError(routerCleanup, ()=>createLoaderRedirectResponse(beforeRenderResult, redirectCtx));
141
- if (redirectResponse) {
142
- await routerCleanup.run();
143
- return redirectResponse;
144
- }
316
+ const lifecycle = createRequestLifecycle(async (terminal)=>{
317
+ await hooks.onRequestEnd?.call({
318
+ runtimeContext: context,
319
+ terminal
320
+ });
321
+ }, options.onError);
322
+ const beforeRenderResult = await runWithRequestLifecycleOnError(lifecycle, ()=>runBeforeRender(context));
323
+ let redirectResponse;
324
+ try {
325
+ const prepared = {
326
+ runtimeContext: context,
327
+ routerResult: context.routerContext
328
+ };
329
+ const result = await hooks.onRenderPrepared?.call(prepared) ?? prepared;
330
+ applyRouterResult(context, result.routerResult, options.onError);
331
+ if ("u" > typeof Response) redirectResponse = createLoaderRedirectResponse(beforeRenderResult, redirectCtx);
332
+ } catch (error) {
333
+ if (beforeRenderResult) try {
334
+ await lifecycle.discardBody(beforeRenderResult, {
335
+ status: 'error',
336
+ error
337
+ });
338
+ } catch {}
339
+ else await lifecycle.run({
340
+ status: 'error',
341
+ error
342
+ });
343
+ throw error;
344
+ }
345
+ if (redirectResponse) {
346
+ const response = redirectResponse;
347
+ if (beforeRenderResult?.body && response !== beforeRenderResult) await lifecycle.discardBody(beforeRenderResult);
348
+ return finishWithRequestLifecycle(lifecycle, ()=>lifecycle.deferUntilBodyDone(response));
145
349
  }
146
- await runWithRouterCleanupOnError(routerCleanup, ()=>{
350
+ await runWithRequestLifecycleOnError(lifecycle, ()=>{
147
351
  if (!createRequestOptions?.enableRsc) {
148
352
  const { htmlTemplate } = options.resource;
149
353
  options.resource.htmlTemplate = htmlTemplate.replace('</head>', `${CHUNK_CSS_PLACEHOLDER}</head>`);
150
354
  }
151
355
  });
152
- const response = await runWithRouterCleanupOnError(routerCleanup, ()=>renderRequest(request, Root, context, options, handleRequest, !!createRequestOptions?.enableRsc));
153
- return finishWithRouterCleanup(routerCleanup, ()=>finalizeRenderResponse(response, responseProxy, redirectCtx, routerCleanup));
356
+ const response = await runWithRequestLifecycleOnError(lifecycle, ()=>renderRequest(request, Root, context, options, handleRequest, !!createRequestOptions?.enableRsc));
357
+ return finishWithRequestLifecycle(lifecycle, ()=>finalizeRenderResponse(response, responseProxy, redirectCtx, lifecycle));
154
358
  });
155
359
  };
156
360
  return requestHandler;
157
361
  };
158
- export { createRequestHandler };
362
+ export { REQUEST_END_ERROR, RESPONSE_BODY_CANCEL_ERROR, applyRouterResult, createLoaderRedirectResponse, createRequestHandler, createRequestLifecycle, finalizeRenderResponse, finishWithRequestLifecycle, runWithRequestLifecycleOnError };
@@ -15,8 +15,8 @@ const readAsset = async (chunk)=>{
15
15
  };
16
16
  const checkIsInline = (chunk, enableInline)=>{
17
17
  if ('production' !== process.env.NODE_ENV) return false;
18
- if (enableInline instanceof RegExp) return enableInline.test(chunk);
19
- return Boolean(enableInline);
18
+ else if (enableInline instanceof RegExp) return enableInline.test(chunk);
19
+ else return Boolean(enableInline);
20
20
  };
21
21
  async function buildShellBeforeTemplate(beforeAppTemplate, options) {
22
22
  const { config, runtimeContext, styledComponentsStyleTags, entryName, lifecycle, helmetData } = options;
@@ -74,7 +74,7 @@ async function buildShellBeforeTemplate(beforeAppTemplate, options) {
74
74
  const styles = await Promise.all(cssChunks.map(async (chunk)=>{
75
75
  const link = `<link href="${chunk}" rel="stylesheet" />`;
76
76
  if (checkIsNode() && checkIsInline(chunk, inlineStyles)) return readAsset(chunk).then((content)=>`<style>${content}</style>`).catch((_)=>link);
77
- return link;
77
+ else return link;
78
78
  }));
79
79
  return `${styles.join('')}`;
80
80
  }
@@ -5,8 +5,8 @@ import { buildHtml, replaceSSRTemplateChunk } from "../shared.mjs";
5
5
  import { hasStylesheetLink } from "../utils.mjs";
6
6
  const checkIsInline = (chunk, enableInline)=>{
7
7
  if ('production' !== process.env.NODE_ENV) return false;
8
- if (enableInline instanceof RegExp) return enableInline.test(chunk);
9
- return Boolean(enableInline);
8
+ else if (enableInline instanceof RegExp) return enableInline.test(chunk);
9
+ else return Boolean(enableInline);
10
10
  };
11
11
  async function buildShellBeforeTemplate(beforeAppTemplate, options) {
12
12
  const { config, runtimeContext, styledComponentsStyleTags, entryName, lifecycle, helmetData } = options;
@@ -1,16 +1,18 @@
1
1
  import "node:module";
2
+ import { getGlobalEnableRsc, getGlobalInternalRuntimeContext } from "@modern-js/runtime/context";
2
3
  import { storage } from "@modern-js/runtime-utils/node";
3
4
  import { SSR_HYDRATION_ID_PREFIX } from "@modern-js/utils/universal/constants";
4
5
  import { PassThrough, Readable, Transform, finished, pipeline } from "stream";
6
+ import { StringDecoder } from "string_decoder";
5
7
  import { ESCAPED_SHELL_STREAM_END_MARK } from "../../../common.mjs";
6
8
  import { RenderLevel } from "../../constants.mjs";
7
- import { getGlobalEnableRsc, getGlobalInternalRuntimeContext } from "../../context/index.mjs";
8
9
  import { getMonitors } from "../../context/monitors/index.mjs";
9
10
  import { wrapRuntimeComponentResolver } from "../../react/wrapper.mjs";
10
11
  import { createReplaceHelemt, getHelmetData } from "../helmet.mjs";
11
12
  import { createSSRRenderLifecycle, createSSRStreamErrorReporter, observeSSRStream, orderSSRStreamTransforms } from "../shared.mjs";
12
13
  import { enqueueFromEntries } from "./deferredScript.mjs";
13
- import { ShellChunkStatus, getReadableStreamFromString, resolveStreamingMode } from "./shared.mjs";
14
+ import { DeferredScriptOutputCoordinator } from "./deferredScriptOutputCoordinator.mjs";
15
+ import { SHELL_PROGRESSIVE_CHUNK_SIZE, ShellChunkStatus, getReadableStreamFromString, resolveStreamingMode } from "./shared.mjs";
14
16
  import { getTemplates } from "./template.mjs";
15
17
  const createReadableStreamFromElement = async (request, rootElement, options)=>{
16
18
  const { renderToPipeableStream } = await import("react-dom/server");
@@ -24,6 +26,7 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
24
26
  config,
25
27
  platform: 'node',
26
28
  mode: 'stream',
29
+ monitors: getMonitors(),
27
30
  isRsc,
28
31
  terminalMarker: ESCAPED_SHELL_STREAM_END_MARK
29
32
  }) || [];
@@ -122,29 +125,42 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
122
125
  });
123
126
  if (failed) return;
124
127
  const chunks = [];
128
+ let bufferedLength = 0;
125
129
  const marker = Buffer.from(ESCAPED_SHELL_STREAM_END_MARK);
130
+ let markerTail = Buffer.alloc(0);
131
+ const decoder = new StringDecoder('utf8');
126
132
  const pendingScripts = [];
133
+ let deferredResolversComplete = Promise.resolve();
127
134
  let shellChunkStatus = ShellChunkStatus.START;
128
- const emitShell = (destination, buffered, markerIndex)=>{
135
+ const emitShell = (buffered, markerIndex)=>{
129
136
  const beforeMark = lifecycle.completedBody(buffered.subarray(0, markerIndex).toString('utf8'), 'shell');
130
137
  const completedShellBefore = createReplaceHelemt(getHelmetData(extenders))(shellBefore);
131
138
  shellChunkStatus = ShellChunkStatus.FINISH;
132
139
  chunks.length = 0;
133
- destination.push(`${completedShellBefore}${beforeMark}${shellAfter}`);
140
+ bufferedLength = 0;
141
+ coordinator.writeReact(`${completedShellBefore}${beforeMark}${shellAfter}`);
134
142
  const afterMark = buffered.subarray(markerIndex + marker.length);
135
- if (afterMark.length > 0) destination.push(afterMark);
136
- for (const script of pendingScripts)destination.push(script);
143
+ if (afterMark.length > 0) coordinator.writeReact(decoder.write(afterMark));
144
+ coordinator.markShellFinished();
145
+ for (const script of pendingScripts)coordinator.enqueueResolver(script);
137
146
  pendingScripts.length = 0;
138
147
  };
139
148
  const body = new Transform({
140
149
  transform (chunk, _encoding, callback) {
141
150
  try {
142
- if (shellChunkStatus === ShellChunkStatus.FINISH) this.push(chunk);
151
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
152
+ if (shellChunkStatus === ShellChunkStatus.FINISH) coordinator.writeReact(decoder.write(bytes));
143
153
  else {
144
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
145
- const buffered = Buffer.concat(chunks);
146
- const markerIndex = buffered.indexOf(marker);
147
- if (-1 !== markerIndex) emitShell(this, buffered, markerIndex);
154
+ const window = Buffer.concat([
155
+ markerTail,
156
+ bytes
157
+ ]);
158
+ const windowIndex = window.indexOf(marker);
159
+ const windowStart = bufferedLength - markerTail.length;
160
+ chunks.push(bytes);
161
+ bufferedLength += bytes.length;
162
+ if (-1 === windowIndex) markerTail = window.subarray(Math.max(0, window.length - marker.length + 1));
163
+ else emitShell(Buffer.concat(chunks, bufferedLength), windowStart + windowIndex);
148
164
  }
149
165
  callback();
150
166
  } catch (error) {
@@ -155,14 +171,27 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
155
171
  try {
156
172
  if (shellChunkStatus !== ShellChunkStatus.FINISH) {
157
173
  const buffered = Buffer.concat(chunks);
158
- emitShell(this, buffered, buffered.length);
174
+ emitShell(buffered, buffered.length);
159
175
  }
160
- callback();
176
+ const trailing = decoder.end();
177
+ if (trailing) coordinator.writeReact(trailing);
178
+ deferredResolversComplete.then(()=>{
179
+ try {
180
+ coordinator.finish();
181
+ callback();
182
+ } catch (error) {
183
+ callback(error instanceof Error ? error : new Error(String(error)));
184
+ }
185
+ }, (error)=>callback(error instanceof Error ? error : new Error(String(error))));
161
186
  } catch (error) {
162
187
  callback(error instanceof Error ? error : new Error(String(error)));
163
188
  }
164
189
  }
165
190
  });
191
+ const coordinator = new DeferredScriptOutputCoordinator((content)=>{
192
+ body.push(content);
193
+ });
194
+ body.once('close', ()=>coordinator.abort());
166
195
  own(body);
167
196
  const passThrough = new PassThrough();
168
197
  let processedStream = own(passThrough);
@@ -173,11 +202,13 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
173
202
  deliver(Readable.toWeb(body));
174
203
  reactStream.pipe(passThrough);
175
204
  try {
176
- const activeDeferreds = storage.useContext?.()?.activeDeferreds;
205
+ const storageContext = storage.useContext?.();
206
+ const activeDeferreds = storageContext?.activeDeferreds;
207
+ const deferredScriptKeys = storageContext?.deferredScriptKeys;
177
208
  const entries = activeDeferreds instanceof Map ? Array.from(activeDeferreds.entries()) : [];
178
- enqueueFromEntries(entries, config.nonce, (script)=>{
209
+ if (entries.length > 0) deferredResolversComplete = enqueueFromEntries(entries, deferredScriptKeys, config.nonce, (script)=>{
179
210
  if (failed || body.destroyed || body.writableEnded) return;
180
- if (shellChunkStatus === ShellChunkStatus.FINISH) body.write(script);
211
+ if (shellChunkStatus === ShellChunkStatus.FINISH) coordinator.enqueueResolver(script);
181
212
  else pendingScripts.push(script);
182
213
  });
183
214
  } catch (error) {
@@ -214,6 +245,7 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
214
245
  reactStream = renderToPipeableStream(processedRootElement, {
215
246
  nonce: config.nonce,
216
247
  identifierPrefix: SSR_HYDRATION_ID_PREFIX,
248
+ progressiveChunkSize: SHELL_PROGRESSIVE_CHUNK_SIZE,
217
249
  [onReady] () {
218
250
  startOutput().catch(fail);
219
251
  },
@@ -1,15 +1,17 @@
1
1
  import "node:module";
2
2
  import { renderSSRStream } from "@modern-js/render/ssr";
3
+ import { getGlobalEnableRsc, getGlobalInternalRuntimeContext } from "@modern-js/runtime/context";
3
4
  import { storage } from "@modern-js/runtime-utils/node";
4
5
  import react from "react";
5
6
  import { ESCAPED_SHELL_STREAM_END_MARK } from "../../../common.mjs";
6
7
  import { RenderLevel } from "../../constants.mjs";
7
- import { getGlobalEnableRsc, getGlobalInternalRuntimeContext } from "../../context/index.mjs";
8
+ import { getMonitors } from "../../context/monitors/index.mjs";
8
9
  import { wrapRuntimeComponentResolver } from "../../react/wrapper.mjs";
9
10
  import { createReplaceHelemt, getHelmetData } from "../helmet.mjs";
10
11
  import { createSSRRenderLifecycle, createSSRStreamErrorReporter, observeSSRStream, orderSSRStreamTransforms } from "../shared.mjs";
11
12
  import { enqueueFromEntries } from "./deferredScript.mjs";
12
- import { ShellChunkStatus, getReadableStreamFromString, resolveStreamingMode } from "./shared.mjs";
13
+ import { DeferredScriptOutputCoordinator } from "./deferredScriptOutputCoordinator.mjs";
14
+ import { SHELL_PROGRESSIVE_CHUNK_SIZE, ShellChunkStatus, getReadableStreamFromString, resolveStreamingMode } from "./shared.mjs";
13
15
  import { getTemplates } from "./template.mjs";
14
16
  const createReadableStreamFromElement = async (request, rootElement, options)=>{
15
17
  const { htmlTemplate, runtimeContext, config, ssrConfig, entryName, resource, rscManifest, rscRoot } = options;
@@ -22,6 +24,7 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
22
24
  config,
23
25
  platform: 'web',
24
26
  mode: 'stream',
27
+ monitors: getMonitors(),
25
28
  isRsc,
26
29
  terminalMarker: ESCAPED_SHELL_STREAM_END_MARK
27
30
  }) || [];
@@ -34,6 +37,7 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
34
37
  let pendingSource;
35
38
  let bodyOpen = true;
36
39
  let bodyPipe;
40
+ let coordinator;
37
41
  const templateOptions = {
38
42
  runtimeContext,
39
43
  ssrConfig,
@@ -52,6 +56,7 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
52
56
  request,
53
57
  signal: request.signal,
54
58
  nonce: config.nonce,
59
+ progressiveChunkSize: SHELL_PROGRESSIVE_CHUNK_SIZE,
55
60
  rscManifest,
56
61
  rscRoot: rscRoot,
57
62
  routes: runtimeContext.routes,
@@ -80,66 +85,87 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
80
85
  const decoder = new TextDecoder();
81
86
  const encoder = new TextEncoder();
82
87
  const pendingScripts = [];
83
- let buffered = '';
88
+ const buffered = [];
89
+ let bufferedLength = 0;
90
+ let markerTail = '';
84
91
  let shellChunkStatus = ShellChunkStatus.START;
85
92
  let bodyController;
86
- const emit = (chunk, controller)=>{
93
+ let deferredResolversComplete = Promise.resolve();
94
+ coordinator = new DeferredScriptOutputCoordinator((content)=>{
95
+ if (!bodyOpen) return;
96
+ try {
97
+ bodyController.enqueue(encoder.encode(content));
98
+ } catch {
99
+ bodyOpen = false;
100
+ }
101
+ });
102
+ const emit = (chunk)=>{
87
103
  if (shellChunkStatus === ShellChunkStatus.FINISH) {
88
- if (chunk.length > 0) controller.enqueue(encoder.encode(chunk));
104
+ if (chunk.length > 0) coordinator?.writeReact(chunk);
105
+ return;
106
+ }
107
+ const window = markerTail + chunk;
108
+ const windowIndex = window.indexOf(ESCAPED_SHELL_STREAM_END_MARK);
109
+ const markerIndex = bufferedLength - markerTail.length + windowIndex;
110
+ buffered.push(chunk);
111
+ bufferedLength += chunk.length;
112
+ if (-1 === windowIndex) {
113
+ markerTail = window.slice(Math.max(0, window.length - ESCAPED_SHELL_STREAM_END_MARK.length + 1));
89
114
  return;
90
115
  }
91
- buffered += chunk;
92
- const markerIndex = buffered.indexOf(ESCAPED_SHELL_STREAM_END_MARK);
93
- if (-1 === markerIndex) return;
94
- const beforeMark = lifecycle.completedBody(buffered.slice(0, markerIndex), 'shell');
95
- const afterMark = buffered.slice(markerIndex + ESCAPED_SHELL_STREAM_END_MARK.length);
116
+ const shell = buffered.join('');
117
+ const beforeMark = lifecycle.completedBody(shell.slice(0, markerIndex), 'shell');
118
+ const afterMark = shell.slice(markerIndex + ESCAPED_SHELL_STREAM_END_MARK.length);
96
119
  const completedShellBefore = createReplaceHelemt(getHelmetData(extenders))(shellBefore);
97
120
  shellChunkStatus = ShellChunkStatus.FINISH;
98
- buffered = '';
99
- controller.enqueue(encoder.encode(`${completedShellBefore}${beforeMark}${shellAfter}`));
100
- if (afterMark.length > 0) controller.enqueue(encoder.encode(afterMark));
101
- for (const script of pendingScripts)controller.enqueue(encoder.encode(script));
121
+ buffered.length = 0;
122
+ coordinator?.writeReact(`${completedShellBefore}${beforeMark}${shellAfter}`);
123
+ if (afterMark.length > 0) coordinator?.writeReact(afterMark);
124
+ coordinator?.markShellFinished();
125
+ for (const script of pendingScripts)coordinator?.enqueueResolver(script);
102
126
  pendingScripts.length = 0;
103
127
  };
104
128
  const body = new TransformStream({
105
129
  start (controller) {
106
130
  bodyController = controller;
107
131
  },
108
- transform (chunk, controller) {
132
+ transform (chunk) {
109
133
  emit(decoder.decode(chunk, {
110
134
  stream: true
111
- }), controller);
135
+ }));
112
136
  },
113
- flush (controller) {
114
- bodyOpen = false;
115
- emit(decoder.decode(), controller);
137
+ async flush () {
138
+ emit(decoder.decode());
116
139
  if (shellChunkStatus !== ShellChunkStatus.FINISH) throw new Error('React SSR stream ended before the shell marker');
140
+ await deferredResolversComplete;
141
+ coordinator?.finish();
142
+ bodyOpen = false;
117
143
  }
118
144
  });
119
145
  bodyPipe = pendingSource.pipeTo(body.writable);
120
146
  bodyPipe.catch(()=>{});
121
147
  pendingSource = body.readable;
122
- const activeDeferreds = storage.useContext?.()?.activeDeferreds;
148
+ const storageContext = storage.useContext?.();
149
+ const activeDeferreds = storageContext?.activeDeferreds;
150
+ const deferredScriptKeys = storageContext?.deferredScriptKeys;
123
151
  const entries = activeDeferreds instanceof Map ? Array.from(activeDeferreds.entries()) : [];
124
- enqueueFromEntries(entries, config.nonce, (script)=>{
152
+ if (entries.length > 0) deferredResolversComplete = enqueueFromEntries(entries, deferredScriptKeys, config.nonce, (script)=>{
125
153
  if (!bodyOpen) return;
126
- if (shellChunkStatus === ShellChunkStatus.FINISH) try {
127
- bodyController.enqueue(encoder.encode(script));
128
- } catch {
129
- bodyOpen = false;
130
- }
154
+ if (shellChunkStatus === ShellChunkStatus.FINISH) coordinator?.enqueueResolver(script);
131
155
  else pendingScripts.push(script);
132
156
  });
133
157
  return observeSSRStream(pendingSource, {
134
158
  lifecycle,
135
159
  async onError (error) {
136
160
  bodyOpen = false;
161
+ coordinator?.abort();
137
162
  await bodyPipe?.catch(()=>{});
138
163
  reportError(error);
139
164
  },
140
165
  signal: request.signal,
141
166
  async onCancel (reason) {
142
167
  bodyOpen = false;
168
+ coordinator?.abort();
143
169
  await bodyPipe?.catch((error)=>{
144
170
  if (error !== reason) throw error;
145
171
  });
@@ -148,6 +174,7 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
148
174
  });
149
175
  } catch (error) {
150
176
  bodyOpen = false;
177
+ coordinator?.abort();
151
178
  if (void 0 !== pendingSource) try {
152
179
  await pendingSource.cancel(error);
153
180
  await bodyPipe?.catch(()=>{});
@@ -45,23 +45,29 @@ function buildDeferredDataScript(nonce, args) {
45
45
  const nonceAttr = void 0 !== nonce && '' !== nonce ? ` nonce="${nonce}"` : '';
46
46
  return `<script async${nonceAttr} data-fn-name="r" data-script-src="modern-run-router-data-fn" data-fn-args='${escaped}' suppressHydrationWarning>${runRouterDataFnStr}</script>`;
47
47
  }
48
- function enqueueFromEntries(entries, nonce, emit) {
48
+ function enqueueFromEntries(entries, deferredScriptKeys, nonce, emit) {
49
+ const pendingResolvers = [];
49
50
  entries.forEach(([routeId, value])=>{
50
51
  if (!isDeferredDataLike(value)) return;
51
- const pendingKeys = new Set(value.pendingKeys ?? []);
52
- pendingKeys.forEach((key)=>{
52
+ const serializedKeys = void 0 !== deferredScriptKeys ? deferredScriptKeys.get(routeId) ?? [] : value.pendingKeys ?? [];
53
+ serializedKeys.forEach((key)=>{
53
54
  const tracked = value.data?.[key];
54
- if (isPromiseLike(tracked)) tracked.then((val)=>emit(buildDeferredDataScript(nonce, [
55
+ if (isPromiseLike(tracked)) pendingResolvers.push(Promise.resolve(tracked).then((val)=>{
56
+ emit(buildDeferredDataScript(nonce, [
55
57
  routeId,
56
58
  key,
57
59
  val
58
- ])), (err)=>emit(buildDeferredDataScript(nonce, [
60
+ ]));
61
+ }, (err)=>{
62
+ emit(buildDeferredDataScript(nonce, [
59
63
  routeId,
60
64
  key,
61
65
  void 0,
62
66
  toErrorInfo(err)
63
- ])));
67
+ ]));
68
+ }));
64
69
  });
65
70
  });
71
+ return Promise.all(pendingResolvers).then(()=>void 0);
66
72
  }
67
73
  export { buildDeferredDataScript, enqueueFromEntries, escapeAttr, isDeferredDataLike, isPromiseLike, isRecord, toErrorInfo };