@modern-js/runtime 3.9.1 → 3.9.3

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 (45) hide show
  1. package/dist/cjs/cli/template.server.js +1 -2
  2. package/dist/cjs/core/browser/hydrate.js +13 -11
  3. package/dist/cjs/core/server/requestHandler.js +2 -0
  4. package/dist/cjs/core/server/stream/beforeTemplate.js +3 -3
  5. package/dist/cjs/core/server/stream/createReadableStream.js +51 -8
  6. package/dist/cjs/core/server/stream/createReadableStream.worker.js +37 -15
  7. package/dist/cjs/core/server/stream/deferredScript.js +12 -6
  8. package/dist/cjs/core/server/stream/deferredScriptOutputCoordinator.js +370 -0
  9. package/dist/cjs/core/server/string/loadable.js +4 -4
  10. package/dist/cjs/router/cli/code/nestedRoutes.js +1 -2
  11. package/dist/cjs/router/cli/code/templates.js +1 -1
  12. package/dist/cjs/router/cli/code/utils.js +2 -2
  13. package/dist/cjs/router/runtime/DeferredDataScripts.node.js +13 -10
  14. package/dist/cjs/router/runtime/plugin.node.js +1 -1
  15. package/dist/esm/cli/template.server.mjs +1 -2
  16. package/dist/esm/core/browser/hydrate.mjs +13 -11
  17. package/dist/esm/core/server/requestHandler.mjs +2 -0
  18. package/dist/esm/core/server/stream/beforeTemplate.mjs +3 -3
  19. package/dist/esm/core/server/stream/createReadableStream.mjs +51 -8
  20. package/dist/esm/core/server/stream/createReadableStream.worker.mjs +37 -15
  21. package/dist/esm/core/server/stream/deferredScript.mjs +12 -6
  22. package/dist/esm/core/server/stream/deferredScriptOutputCoordinator.mjs +332 -0
  23. package/dist/esm/core/server/string/loadable.mjs +4 -4
  24. package/dist/esm/router/cli/code/nestedRoutes.mjs +1 -2
  25. package/dist/esm/router/cli/code/templates.mjs +1 -1
  26. package/dist/esm/router/cli/code/utils.mjs +2 -2
  27. package/dist/esm/router/runtime/DeferredDataScripts.node.mjs +13 -10
  28. package/dist/esm/router/runtime/plugin.node.mjs +1 -1
  29. package/dist/esm-node/cli/template.server.mjs +1 -2
  30. package/dist/esm-node/core/browser/hydrate.mjs +13 -11
  31. package/dist/esm-node/core/server/requestHandler.mjs +2 -0
  32. package/dist/esm-node/core/server/stream/beforeTemplate.mjs +3 -3
  33. package/dist/esm-node/core/server/stream/createReadableStream.mjs +51 -8
  34. package/dist/esm-node/core/server/stream/createReadableStream.worker.mjs +37 -15
  35. package/dist/esm-node/core/server/stream/deferredScript.mjs +12 -6
  36. package/dist/esm-node/core/server/stream/deferredScriptOutputCoordinator.mjs +333 -0
  37. package/dist/esm-node/core/server/string/loadable.mjs +4 -4
  38. package/dist/esm-node/router/cli/code/nestedRoutes.mjs +1 -2
  39. package/dist/esm-node/router/cli/code/templates.mjs +1 -1
  40. package/dist/esm-node/router/cli/code/utils.mjs +2 -2
  41. package/dist/esm-node/router/runtime/DeferredDataScripts.node.mjs +13 -10
  42. package/dist/esm-node/router/runtime/plugin.node.mjs +1 -1
  43. package/dist/types/core/server/stream/deferredScript.d.ts +1 -1
  44. package/dist/types/core/server/stream/deferredScriptOutputCoordinator.d.ts +72 -0
  45. package/package.json +12 -12
@@ -0,0 +1,333 @@
1
+ import "node:module";
2
+ import { SSR_HYDRATION_ID_PREFIX } from "@modern-js/utils/universal/constants";
3
+ const VOID_ELEMENTS = new Set([
4
+ 'area',
5
+ 'base',
6
+ 'br',
7
+ 'col',
8
+ 'embed',
9
+ 'hr',
10
+ 'img',
11
+ 'input',
12
+ 'link',
13
+ 'meta',
14
+ 'param',
15
+ 'source',
16
+ 'track',
17
+ 'wbr'
18
+ ]);
19
+ const RAW_TEXT_ELEMENTS = new Set([
20
+ 'iframe',
21
+ 'noembed',
22
+ 'noframes',
23
+ "script",
24
+ 'style',
25
+ 'textarea',
26
+ 'title',
27
+ 'xmp'
28
+ ]);
29
+ const REACT_SEGMENT_ID_PATTERN = new RegExp(`^${SSR_HYDRATION_ID_PREFIX}S:[0-9a-f]+$`);
30
+ const TABLE_SEGMENT_CHILDREN = new Set([
31
+ 'tbody',
32
+ 'tr',
33
+ 'colgroup'
34
+ ]);
35
+ function isAsciiAlpha(char) {
36
+ return /[A-Za-z]/.test(char);
37
+ }
38
+ function isWhitespace(char) {
39
+ return /[\t\n\f\r ]/.test(char);
40
+ }
41
+ function getSuffix(value, pattern) {
42
+ const start = Math.max(0, value.length - pattern.length);
43
+ for(let index = start; index < value.length; index++){
44
+ const suffix = value.slice(index);
45
+ if (pattern.startsWith(suffix)) return suffix;
46
+ }
47
+ return '';
48
+ }
49
+ class DeferredScriptOutputCoordinator {
50
+ enqueueResolver(script) {
51
+ if (this.closed) return;
52
+ this.pendingResolvers.push(script);
53
+ this.flushResolvers();
54
+ }
55
+ markShellFinished() {
56
+ if (this.closed) return;
57
+ this.shellFinished = true;
58
+ this.flushResolvers();
59
+ }
60
+ writeReact(chunk) {
61
+ if (this.closed || !chunk) return;
62
+ this.flushResolvers();
63
+ let emittedUntil = 0;
64
+ for(let index = 0; index < chunk.length; index++){
65
+ const wasSafe = this.isSafeBoundary();
66
+ this.consume(chunk[index]);
67
+ if (!wasSafe && this.isSafeBoundary() && this.pendingResolvers.length) {
68
+ this.emit(chunk.slice(emittedUntil, index + 1));
69
+ emittedUntil = index + 1;
70
+ this.flushResolvers();
71
+ }
72
+ }
73
+ if (emittedUntil < chunk.length) this.emit(chunk.slice(emittedUntil));
74
+ }
75
+ finish() {
76
+ if (this.closed) return;
77
+ this.flushResolvers();
78
+ this.closed = true;
79
+ if (this.pendingResolvers.length > 0) throw new Error("Cannot emit deferred resolver scripts because the React stream ended outside a safe HTML boundary.");
80
+ }
81
+ abort() {
82
+ this.closed = true;
83
+ this.pendingResolvers.length = 0;
84
+ }
85
+ isSafeBoundary() {
86
+ return this.shellFinished && 'data' === this.state && 0 === this.openElements.length && !this.awaitingContinuationProtocol;
87
+ }
88
+ flushResolvers() {
89
+ if (!this.isSafeBoundary() || 0 === this.pendingResolvers.length) return;
90
+ while(this.pendingResolvers.length > 0){
91
+ const resolver = this.pendingResolvers.shift();
92
+ if (resolver) this.emit(resolver);
93
+ }
94
+ }
95
+ consume(char) {
96
+ switch(this.state){
97
+ case 'data':
98
+ if ('<' === char) this.state = 'tag-open';
99
+ return;
100
+ case 'tag-open':
101
+ if ('/' === char) {
102
+ this.currentTagName = '';
103
+ this.state = 'end-tag-name';
104
+ } else if ('!' === char) {
105
+ this.declarationPrefix = '';
106
+ this.state = 'markup-declaration';
107
+ } else if ('?' === char) this.state = 'bogus-comment';
108
+ else if (isAsciiAlpha(char)) {
109
+ this.startTag(char);
110
+ this.state = 'start-tag-name';
111
+ } else this.state = 'data';
112
+ return;
113
+ case 'start-tag-name':
114
+ if (isWhitespace(char)) this.state = 'before-attribute-name';
115
+ else if ('/' === char) this.state = 'self-closing-start-tag';
116
+ else if ('>' === char) this.completeStartTag(false);
117
+ else this.currentTagName += char.toLowerCase();
118
+ return;
119
+ case 'before-attribute-name':
120
+ if (isWhitespace(char)) return;
121
+ if ('/' === char) this.state = 'self-closing-start-tag';
122
+ else if ('>' === char) this.completeStartTag(false);
123
+ else {
124
+ this.startAttribute(char);
125
+ this.state = 'attribute-name';
126
+ }
127
+ return;
128
+ case 'attribute-name':
129
+ if (isWhitespace(char)) this.state = 'after-attribute-name';
130
+ else if ('=' === char) {
131
+ this.currentAttributeHasValue = true;
132
+ this.state = 'before-attribute-value';
133
+ } else if ('/' === char) {
134
+ this.completeAttribute();
135
+ this.state = 'self-closing-start-tag';
136
+ } else if ('>' === char) this.completeStartTag(false);
137
+ else this.currentAttributeName += char.toLowerCase();
138
+ return;
139
+ case 'after-attribute-name':
140
+ if (isWhitespace(char)) return;
141
+ if ('=' === char) {
142
+ this.currentAttributeHasValue = true;
143
+ this.state = 'before-attribute-value';
144
+ } else if ('/' === char) {
145
+ this.completeAttribute();
146
+ this.state = 'self-closing-start-tag';
147
+ } else if ('>' === char) this.completeStartTag(false);
148
+ else {
149
+ this.completeAttribute();
150
+ this.startAttribute(char);
151
+ this.state = 'attribute-name';
152
+ }
153
+ return;
154
+ case 'before-attribute-value':
155
+ if (isWhitespace(char)) return;
156
+ if ('"' === char) this.state = 'attribute-value-double-quoted';
157
+ else if ("'" === char) this.state = 'attribute-value-single-quoted';
158
+ else if ('>' === char) this.completeStartTag(false);
159
+ else {
160
+ this.currentAttributeValue += char;
161
+ this.state = 'attribute-value-unquoted';
162
+ }
163
+ return;
164
+ case 'attribute-value-double-quoted':
165
+ if ('"' === char) {
166
+ this.completeAttribute();
167
+ this.state = 'before-attribute-name';
168
+ } else this.currentAttributeValue += char;
169
+ return;
170
+ case 'attribute-value-single-quoted':
171
+ if ("'" === char) {
172
+ this.completeAttribute();
173
+ this.state = 'before-attribute-name';
174
+ } else this.currentAttributeValue += char;
175
+ return;
176
+ case 'attribute-value-unquoted':
177
+ if (isWhitespace(char)) {
178
+ this.completeAttribute();
179
+ this.state = 'before-attribute-name';
180
+ } else if ('>' === char) this.completeStartTag(false);
181
+ else this.currentAttributeValue += char;
182
+ return;
183
+ case 'self-closing-start-tag':
184
+ if ('>' === char) this.completeStartTag(true);
185
+ else if (!isWhitespace(char)) {
186
+ this.startAttribute(char);
187
+ this.state = 'attribute-name';
188
+ }
189
+ return;
190
+ case 'end-tag-name':
191
+ if (isWhitespace(char)) this.state = 'after-end-tag-name';
192
+ else if ('>' === char) this.completeEndTag();
193
+ else this.currentTagName += char.toLowerCase();
194
+ return;
195
+ case 'after-end-tag-name':
196
+ if ('>' === char) this.completeEndTag();
197
+ return;
198
+ case 'markup-declaration':
199
+ if (this.declarationPrefix.length < 2 && '-' === char) {
200
+ this.declarationPrefix += char;
201
+ if ('--' === this.declarationPrefix) {
202
+ this.commentTail = '';
203
+ this.state = 'comment';
204
+ }
205
+ } else this.state = '>' === char ? 'data' : 'declaration';
206
+ return;
207
+ case 'declaration':
208
+ if ('>' === char) this.state = 'data';
209
+ return;
210
+ case 'comment':
211
+ this.commentTail = `${this.commentTail}${char}`.slice(-3);
212
+ if ('-->' === this.commentTail) this.state = 'data';
213
+ return;
214
+ case 'bogus-comment':
215
+ if ('>' === char) this.state = 'data';
216
+ return;
217
+ case 'raw-text':
218
+ this.consumeRawText(char);
219
+ return;
220
+ case 'raw-end-tag':
221
+ if ('>' === char) this.completeRawTextEndTag();
222
+ else if (!isWhitespace(char)) {
223
+ this.state = 'raw-text';
224
+ this.rawTextSearch = getSuffix(`${this.rawTextSearch}${char.toLowerCase()}`, `</${this.rawTextElement}`);
225
+ }
226
+ }
227
+ }
228
+ startTag(char) {
229
+ this.currentTagName = char.toLowerCase();
230
+ this.currentAttributes = new Map();
231
+ this.currentAttributeName = '';
232
+ this.currentAttributeValue = '';
233
+ this.currentAttributeHasValue = false;
234
+ }
235
+ startAttribute(char) {
236
+ this.currentAttributeName = char.toLowerCase();
237
+ this.currentAttributeValue = '';
238
+ this.currentAttributeHasValue = false;
239
+ }
240
+ completeAttribute() {
241
+ if (!this.currentAttributeName) return;
242
+ this.currentAttributes.set(this.currentAttributeName, this.currentAttributeHasValue ? this.currentAttributeValue : true);
243
+ this.currentAttributeName = '';
244
+ this.currentAttributeValue = '';
245
+ this.currentAttributeHasValue = false;
246
+ }
247
+ completeStartTag(selfClosing) {
248
+ this.completeAttribute();
249
+ const tagName = this.currentTagName;
250
+ const id = this.currentAttributes.get('id');
251
+ const hasReactSegmentId = 'string' == typeof id && REACT_SEGMENT_ID_PATTERN.test(id);
252
+ const hasHiddenAttribute = this.currentAttributes.has('hidden');
253
+ const isHiddenSegmentTable = 'table' === tagName && hasHiddenAttribute && !hasReactSegmentId;
254
+ const isTableSegmentChild = hasReactSegmentId && TABLE_SEGMENT_CHILDREN.has(tagName) && this.openElements[this.openElements.length - 1]?.isSegmentTableWrapper === true;
255
+ const isContinuationContainer = hasReactSegmentId && ('div' === tagName && hasHiddenAttribute || ('svg' === tagName || 'math' === tagName) && void 0 !== id && 'true' === this.currentAttributes.get('aria-hidden') && 'display:none' === this.currentAttributes.get('style') || 'table' === tagName && hasHiddenAttribute || isTableSegmentChild);
256
+ this.continuationProtocolScript = "script" === tagName && this.awaitingContinuationProtocol;
257
+ this.continuationProtocolTail = '';
258
+ this.hasContinuationProtocolCall = false;
259
+ if (!selfClosing && !VOID_ELEMENTS.has(tagName)) this.openElements.push({
260
+ tagName,
261
+ isContinuationContainer,
262
+ isSegmentTableWrapper: isHiddenSegmentTable
263
+ });
264
+ this.currentTagName = '';
265
+ this.currentAttributes = new Map();
266
+ this.currentAttributeName = '';
267
+ this.currentAttributeValue = '';
268
+ this.currentAttributeHasValue = false;
269
+ if (!selfClosing && RAW_TEXT_ELEMENTS.has(tagName)) {
270
+ this.rawTextElement = tagName;
271
+ this.rawTextSearch = '';
272
+ this.state = 'raw-text';
273
+ } else this.state = 'data';
274
+ }
275
+ completeEndTag() {
276
+ this.closeOpenElement(this.currentTagName);
277
+ this.currentTagName = '';
278
+ this.state = 'data';
279
+ }
280
+ consumeRawText(char) {
281
+ const rawTextElement = this.rawTextElement;
282
+ if (!rawTextElement) {
283
+ this.state = 'data';
284
+ return;
285
+ }
286
+ const endTagPrefix = `</${rawTextElement}`;
287
+ if (this.continuationProtocolScript) {
288
+ this.continuationProtocolTail = `${this.continuationProtocolTail}${char}`.slice(-4);
289
+ if (/\$(?:RC|RS|RX|RB|RR)\b/.test(this.continuationProtocolTail)) this.hasContinuationProtocolCall = true;
290
+ }
291
+ this.rawTextSearch = getSuffix(`${this.rawTextSearch}${char.toLowerCase()}`, endTagPrefix);
292
+ if (this.rawTextSearch === endTagPrefix) this.state = 'raw-end-tag';
293
+ }
294
+ completeRawTextEndTag() {
295
+ const rawTextElement = this.rawTextElement;
296
+ if (rawTextElement) this.closeOpenElement(rawTextElement);
297
+ if ("script" === rawTextElement && this.continuationProtocolScript && this.hasContinuationProtocolCall) this.awaitingContinuationProtocol = false;
298
+ this.rawTextElement = void 0;
299
+ this.rawTextSearch = '';
300
+ this.continuationProtocolScript = false;
301
+ this.continuationProtocolTail = '';
302
+ this.hasContinuationProtocolCall = false;
303
+ this.state = 'data';
304
+ }
305
+ closeOpenElement(tagName) {
306
+ const matchingIndex = this.openElements.map((element)=>element.tagName).lastIndexOf(tagName);
307
+ if (-1 === matchingIndex) return;
308
+ const closedElements = this.openElements.splice(matchingIndex);
309
+ const closedContinuation = closedElements.some((element)=>element.isContinuationContainer);
310
+ if (closedContinuation) this.awaitingContinuationProtocol = true;
311
+ }
312
+ constructor(emit){
313
+ this.emit = emit;
314
+ this.pendingResolvers = [];
315
+ this.openElements = [];
316
+ this.shellFinished = false;
317
+ this.closed = false;
318
+ this.state = 'data';
319
+ this.currentTagName = '';
320
+ this.currentAttributes = new Map();
321
+ this.currentAttributeName = '';
322
+ this.currentAttributeValue = '';
323
+ this.currentAttributeHasValue = false;
324
+ this.declarationPrefix = '';
325
+ this.commentTail = '';
326
+ this.rawTextSearch = '';
327
+ this.continuationProtocolScript = false;
328
+ this.continuationProtocolTail = '';
329
+ this.hasContinuationProtocolCall = false;
330
+ this.awaitingContinuationProtocol = false;
331
+ }
332
+ }
333
+ export { DeferredScriptOutputCoordinator };
@@ -11,8 +11,8 @@ const extname = (uri)=>{
11
11
  const generateChunks = (chunks, ext)=>chunks.filter((chunk)=>Boolean(chunk.url)).filter((chunk)=>extname(chunk.url).slice(1) === ext);
12
12
  const checkIsInline = (chunk, enableInline)=>{
13
13
  if ('production' !== process.env.NODE_ENV) return false;
14
- if (enableInline instanceof RegExp) return enableInline.test(chunk.url);
15
- return Boolean(enableInline);
14
+ else if (enableInline instanceof RegExp) return enableInline.test(chunk.url);
15
+ else return Boolean(enableInline);
16
16
  };
17
17
  const readAsset = async (chunk)=>{
18
18
  const fs = await import("fs/promises");
@@ -80,7 +80,7 @@ class LoadableCollector {
80
80
  const scripts = await Promise.all(chunks.filter((chunk)=>!existedScript.includes(chunk.url) && !this.existsAssets?.includes(chunk.path)).map(async (chunk)=>{
81
81
  const script = `<script${attributes} src="${chunk.url}"></script>`;
82
82
  if (checkIsNode() && checkIsInline(chunk, inlineStyles)) return readAsset(chunk).then((content)=>`<script>${content}</script>`).catch((_)=>script);
83
- return script;
83
+ else return script;
84
84
  }));
85
85
  chunkSet.jsChunk += scripts.filter((script)=>Boolean(script)).join('');
86
86
  }
@@ -91,7 +91,7 @@ class LoadableCollector {
91
91
  const css = await Promise.all(chunks.filter((chunk)=>!hasStylesheetLink(template, chunk.url) && !this.existsAssets?.includes(chunk.path)).map(async (chunk)=>{
92
92
  const link = `<link${atrributes} href="${chunk.url}" rel="stylesheet" />`;
93
93
  if (checkIsNode() && checkIsInline(chunk, inlineStyles)) return readAsset(chunk).then((content)=>`<style>${content}</style>`).catch((_)=>link);
94
- return link;
94
+ else return link;
95
95
  }));
96
96
  chunkSet.cssChunk += css.filter((css)=>Boolean(css)).join('');
97
97
  }
@@ -43,8 +43,7 @@ const optimizeRoute = (routeTree)=>{
43
43
  children: optimizedChildren
44
44
  }
45
45
  ];
46
- }
47
- {
46
+ } else {
48
47
  const newRoutes = children.map((child)=>{
49
48
  const routePath = routeTree.path ? `${routeTree.path}${child.path ? `/${child.path}` : ''}` : child.path || '';
50
49
  const newRoute = {
@@ -376,7 +376,7 @@ const runtimeGlobalContext = async ({ entryName, metaName, srcDirectory, nestedR
376
376
  enableRsc: true,
377
377
  });
378
378
  `;
379
- return `${imports.join('\n')}
379
+ else return `${imports.join('\n')}
380
380
 
381
381
  import { routes } from './routes';
382
382
 
@@ -11,7 +11,7 @@ const walkDirectory = (dir)=>fs.readdirSync(dir).reduce((previous, filename)=>{
11
11
  ...previous,
12
12
  ...walkDirectory(filePath)
13
13
  ];
14
- return [
14
+ else return [
15
15
  ...previous,
16
16
  filePath
17
17
  ];
@@ -28,7 +28,7 @@ const isPageComponentFile = (filePath)=>{
28
28
  };
29
29
  const replaceWithAlias = (base, filePath, alias)=>{
30
30
  if (filePath.includes(base)) return normalizeToPosixPath(path.join(alias, path.relative(base, filePath)));
31
- return filePath;
31
+ else return filePath;
32
32
  };
33
33
  const parseModule = async ({ source, filename })=>{
34
34
  let content = source;
@@ -14,7 +14,8 @@ const DeferredDataScripts = (props)=>{
14
14
  }, []);
15
15
  const deferredScripts = useMemo(()=>{
16
16
  if (!staticContext) return null;
17
- const activeDeferreds = storage.useContext().activeDeferreds;
17
+ const requestStorage = storage.useContext();
18
+ const activeDeferreds = requestStorage.activeDeferreds;
18
19
  const _ROUTER_DATA = {
19
20
  loaderData: staticContext.loaderData,
20
21
  errors: serializeErrors(staticContext.errors)
@@ -27,6 +28,7 @@ const DeferredDataScripts = (props)=>{
27
28
  const deferredDataScripts = [];
28
29
  const initialScripts = Array.from(activeDeferreds.entries()).map(([routeId, deferredData])=>{
29
30
  const pendingKeys = new Set(deferredData.pendingKeys);
31
+ requestStorage.deferredScriptKeys?.set(routeId, Array.from(pendingKeys));
30
32
  const { deferredKeys } = deferredData;
31
33
  const deferredKeyPromiseManifests = deferredKeys.map((key)=>{
32
34
  if (pendingKeys.has(key)) return {
@@ -37,7 +39,7 @@ const DeferredDataScripts = (props)=>{
37
39
  serializeJson(key)
38
40
  ]
39
41
  };
40
- {
42
+ else {
41
43
  const trackedPromise = deferredData.data[key];
42
44
  if (void 0 !== trackedPromise._error) {
43
45
  const error = {
@@ -52,15 +54,16 @@ const DeferredDataScripts = (props)=>{
52
54
  serializeJson(error)
53
55
  ]
54
56
  };
57
+ } else {
58
+ if (void 0 === trackedPromise._data) throw new Error(`The deferred data for ${key} was not resolved, did you forget to return data from a deferred promise`);
59
+ return {
60
+ key,
61
+ routerDataFnName: 'p',
62
+ routerDataFnArgs: [
63
+ serializeJson(trackedPromise._data)
64
+ ]
65
+ };
55
66
  }
56
- if (void 0 === trackedPromise._data) throw new Error(`The deferred data for ${key} was not resolved, did you forget to return data from a deferred promise`);
57
- return {
58
- key,
59
- routerDataFnName: 'p',
60
- routerDataFnArgs: [
61
- serializeJson(trackedPromise._data)
62
- ]
63
- };
64
67
  }
65
68
  });
66
69
  return {
@@ -107,7 +107,7 @@ const routerPlugin = (userConfig = {})=>({
107
107
  }) : /*#__PURE__*/ jsx(RSCStaticRouter, {
108
108
  basename: basename
109
109
  });
110
- {
110
+ else {
111
111
  const routerWrapper = /*#__PURE__*/ jsxs(Fragment, {
112
112
  children: [
113
113
  /*#__PURE__*/ jsx(StaticRouterProvider, {
@@ -12,4 +12,4 @@ export declare function toErrorInfo(error: unknown): {
12
12
  stack?: string;
13
13
  };
14
14
  export declare function buildDeferredDataScript(nonce: string | undefined, args: unknown[]): string;
15
- export declare function enqueueFromEntries(entries: Array<[string, unknown]>, nonce: string | undefined, emit: (script: string) => void): void;
15
+ export declare function enqueueFromEntries(entries: Array<[string, unknown]>, deferredScriptKeys: Map<string, string[]> | undefined, nonce: string | undefined, emit: (script: string) => void): Promise<void>;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Serializes the router's deferred-data resolver scripts with React's Fizz
3
+ * output without rewriting either producer's bytes.
4
+ *
5
+ * Why this exists:
6
+ * - A resolver script settles the client-side Promise created for a
7
+ * `defer()` key. It must be sent when that individual server Promise
8
+ * settles; waiting for the whole React stream would unnecessarily delay
9
+ * hydration and interactivity behind unrelated Suspense boundaries.
10
+ * - React and the resolver callbacks are independent stream producers. A
11
+ * direct `write()` can therefore land in a partial HTML tag, a hidden Fizz
12
+ * segment container, or between that container and its continuation
13
+ * instruction.
14
+ * - React's public streaming callbacks expose shell/all-ready lifecycle
15
+ * events, not a safe byte-level insertion point. React chunk sizes and
16
+ * `progressiveChunkSize` are buffering heuristics, not HTML or Fizz
17
+ * boundaries.
18
+ *
19
+ * The coordinator deliberately recognizes the Fizz continuation containers
20
+ * and instructions used by the supported React stream format. This is a
21
+ * compatibility boundary: when the supported React/Fizz output protocol is
22
+ * changed, update this recognizer and its Node, Edge, HTML, SVG, MathML, and
23
+ * table continuation tests together. It is not intended to be a general HTML
24
+ * rewriter.
25
+ *
26
+ * Shell completion only makes a resolver eligible to send. The resolver is
27
+ * emitted after a root-level HTML boundary and, for a continuation segment,
28
+ * after React's matching continuation instruction.
29
+ */
30
+ export declare class DeferredScriptOutputCoordinator {
31
+ private readonly emit;
32
+ private readonly pendingResolvers;
33
+ private readonly openElements;
34
+ private shellFinished;
35
+ private closed;
36
+ private state;
37
+ private currentTagName;
38
+ private currentAttributes;
39
+ private currentAttributeName;
40
+ private currentAttributeValue;
41
+ private currentAttributeHasValue;
42
+ private declarationPrefix;
43
+ private commentTail;
44
+ private rawTextElement;
45
+ private rawTextSearch;
46
+ private continuationProtocolScript;
47
+ private continuationProtocolTail;
48
+ private hasContinuationProtocolCall;
49
+ private awaitingContinuationProtocol;
50
+ constructor(emit: (chunk: string) => void);
51
+ enqueueResolver(script: string): void;
52
+ /**
53
+ * Keep this separate from the ShellChunkStatus assignment. The caller sets
54
+ * FINISH at the existing marker position, then calls this after the marker
55
+ * chunk's afterMark bytes have been observed.
56
+ */
57
+ markShellFinished(): void;
58
+ writeReact(chunk: string): void;
59
+ finish(): void;
60
+ abort(): void;
61
+ private isSafeBoundary;
62
+ private flushResolvers;
63
+ private consume;
64
+ private startTag;
65
+ private startAttribute;
66
+ private completeAttribute;
67
+ private completeStartTag;
68
+ private completeEndTag;
69
+ private consumeRawText;
70
+ private completeRawTextEndTag;
71
+ private closeOpenElement;
72
+ }
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  "modern",
16
16
  "modern.js"
17
17
  ],
18
- "version": "3.9.1",
18
+ "version": "3.9.3",
19
19
  "engines": {
20
20
  "node": ">=20"
21
21
  },
@@ -215,12 +215,12 @@
215
215
  "isbot": "3.8.0",
216
216
  "react-helmet": "^6.1.0",
217
217
  "react-is": "^18.3.1",
218
- "@modern-js/plugin": "3.9.1",
219
- "@modern-js/runtime-utils": "3.9.1",
220
- "@modern-js/plugin-data-loader": "3.9.1",
221
- "@modern-js/types": "3.9.1",
222
- "@modern-js/render": "3.9.1",
223
- "@modern-js/utils": "3.9.1"
218
+ "@modern-js/plugin": "3.9.3",
219
+ "@modern-js/plugin-data-loader": "3.9.3",
220
+ "@modern-js/render": "3.9.3",
221
+ "@modern-js/runtime-utils": "3.9.3",
222
+ "@modern-js/types": "3.9.3",
223
+ "@modern-js/utils": "3.9.3"
224
224
  },
225
225
  "peerDependencies": {
226
226
  "react": ">=18.3.1",
@@ -228,10 +228,10 @@
228
228
  },
229
229
  "devDependencies": {
230
230
  "@remix-run/web-fetch": "^4.1.3",
231
- "@rsbuild/core": "2.2.4",
232
- "@rslib/core": "1.0.0",
233
- "@testing-library/dom": "^10.4.1",
234
- "@testing-library/react": "^16.3.2",
231
+ "@rsbuild/core": "2.2.9",
232
+ "@rslib/core": "1.0.1",
233
+ "@testing-library/dom": "^10.4.2",
234
+ "@testing-library/react": "^16.3.3",
235
235
  "@types/cookie": "0.6.0",
236
236
  "@types/invariant": "^2.2.37",
237
237
  "@types/loadable__server": "5.12.11",
@@ -240,7 +240,7 @@
240
240
  "react-dom": "^19.2.8",
241
241
  "ts-node": "^10.9.2",
242
242
  "typescript": "^5",
243
- "@modern-js/app-tools": "3.9.1",
243
+ "@modern-js/app-tools": "3.9.3",
244
244
  "@modern-js/rslib": "2.68.10",
245
245
  "@scripts/rstest-config": "2.66.0"
246
246
  },