@modern-js/runtime 3.9.2 → 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.
- package/dist/cjs/core/server/requestHandler.js +2 -0
- package/dist/cjs/core/server/stream/createReadableStream.js +51 -8
- package/dist/cjs/core/server/stream/createReadableStream.worker.js +37 -15
- package/dist/cjs/core/server/stream/deferredScript.js +12 -6
- package/dist/cjs/core/server/stream/deferredScriptOutputCoordinator.js +370 -0
- package/dist/cjs/router/runtime/DeferredDataScripts.node.js +3 -1
- package/dist/esm/core/server/requestHandler.mjs +2 -0
- package/dist/esm/core/server/stream/createReadableStream.mjs +51 -8
- package/dist/esm/core/server/stream/createReadableStream.worker.mjs +37 -15
- package/dist/esm/core/server/stream/deferredScript.mjs +12 -6
- package/dist/esm/core/server/stream/deferredScriptOutputCoordinator.mjs +332 -0
- package/dist/esm/router/runtime/DeferredDataScripts.node.mjs +3 -1
- package/dist/esm-node/core/server/requestHandler.mjs +2 -0
- package/dist/esm-node/core/server/stream/createReadableStream.mjs +51 -8
- package/dist/esm-node/core/server/stream/createReadableStream.worker.mjs +37 -15
- package/dist/esm-node/core/server/stream/deferredScript.mjs +12 -6
- package/dist/esm-node/core/server/stream/deferredScriptOutputCoordinator.mjs +333 -0
- package/dist/esm-node/router/runtime/DeferredDataScripts.node.mjs +3 -1
- package/dist/types/core/server/stream/deferredScript.d.ts +1 -1
- package/dist/types/core/server/stream/deferredScriptOutputCoordinator.d.ts +72 -0
- package/package.json +12 -12
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import "node:module";
|
|
2
2
|
import { PassThrough, Readable, Transform } from "stream";
|
|
3
|
+
import { StringDecoder } from "string_decoder";
|
|
3
4
|
import { storage } from "@modern-js/runtime-utils/node";
|
|
4
5
|
import { SSR_HYDRATION_ID_PREFIX } from "@modern-js/utils/universal/constants";
|
|
5
6
|
import { ESCAPED_SHELL_STREAM_END_MARK } from "../../../common.mjs";
|
|
@@ -7,6 +8,7 @@ import { RenderLevel } from "../../constants.mjs";
|
|
|
7
8
|
import { getGlobalInternalRuntimeContext } from "../../context/index.mjs";
|
|
8
9
|
import { getMonitors } from "../../context/monitors/index.mjs";
|
|
9
10
|
import { enqueueFromEntries } from "./deferredScript.mjs";
|
|
11
|
+
import { DeferredScriptOutputCoordinator } from "./deferredScriptOutputCoordinator.mjs";
|
|
10
12
|
import { ShellChunkStatus, getReadableStreamFromString, resolveStreamingMode } from "./shared.mjs";
|
|
11
13
|
import { getTemplates } from "./template.mjs";
|
|
12
14
|
const defaultExtender = {
|
|
@@ -55,7 +57,14 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
|
|
|
55
57
|
entryName,
|
|
56
58
|
styledComponentsStyleTags
|
|
57
59
|
}).then(({ shellAfter, shellBefore })=>{
|
|
60
|
+
const decoder = new StringDecoder('utf8');
|
|
58
61
|
const pendingScripts = [];
|
|
62
|
+
const outputState = {};
|
|
63
|
+
let deferredResolversComplete = Promise.resolve();
|
|
64
|
+
const writeReact = (content)=>{
|
|
65
|
+
if (!outputState.coordinator) throw new Error("Deferred script coordinator is not ready");
|
|
66
|
+
outputState.coordinator.writeReact(content);
|
|
67
|
+
};
|
|
59
68
|
const body = new Transform({
|
|
60
69
|
transform (chunk, _encoding, callback) {
|
|
61
70
|
try {
|
|
@@ -67,17 +76,49 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
|
|
|
67
76
|
const beforeMark = concatedChunk.slice(0, markerIndex);
|
|
68
77
|
const afterMark = concatedChunk.slice(markerIndex + ESCAPED_SHELL_STREAM_END_MARK.length);
|
|
69
78
|
shellChunkStatus = ShellChunkStatus.FINISH;
|
|
70
|
-
|
|
71
|
-
if (afterMark)
|
|
72
|
-
if (
|
|
79
|
+
writeReact(`${shellBefore}${beforeMark}${shellAfter}`);
|
|
80
|
+
if (afterMark) writeReact(afterMark);
|
|
81
|
+
if (outputState.coordinator) {
|
|
82
|
+
outputState.coordinator.markShellFinished();
|
|
83
|
+
for (const script of pendingScripts)outputState.coordinator.enqueueResolver(script);
|
|
84
|
+
pendingScripts.length = 0;
|
|
85
|
+
}
|
|
73
86
|
}
|
|
74
|
-
} else
|
|
87
|
+
} else {
|
|
88
|
+
const decodedChunk = decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
89
|
+
writeReact(decodedChunk);
|
|
90
|
+
}
|
|
75
91
|
callback();
|
|
76
92
|
} catch (e) {
|
|
77
93
|
e instanceof Error ? callback(e) : callback(new Error('Received unknown error when streaming'));
|
|
78
94
|
}
|
|
95
|
+
},
|
|
96
|
+
flush (callback) {
|
|
97
|
+
try {
|
|
98
|
+
writeReact(decoder.end());
|
|
99
|
+
} catch (error) {
|
|
100
|
+
callback(error instanceof Error ? error : new Error('Received unknown error when streaming'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
deferredResolversComplete.then(()=>{
|
|
104
|
+
try {
|
|
105
|
+
outputState.coordinator?.finish();
|
|
106
|
+
callback();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
callback(error instanceof Error ? error : new Error('Received unknown error when streaming'));
|
|
109
|
+
}
|
|
110
|
+
}, (error)=>{
|
|
111
|
+
callback(error instanceof Error ? error : new Error('Received unknown error when streaming'));
|
|
112
|
+
});
|
|
79
113
|
}
|
|
80
114
|
});
|
|
115
|
+
const coordinator = new DeferredScriptOutputCoordinator((content)=>{
|
|
116
|
+
body.push(content);
|
|
117
|
+
});
|
|
118
|
+
outputState.coordinator = coordinator;
|
|
119
|
+
body.once('close', ()=>{
|
|
120
|
+
coordinator.abort();
|
|
121
|
+
});
|
|
81
122
|
const passThrough = new PassThrough();
|
|
82
123
|
const stream = Readable.toWeb(body);
|
|
83
124
|
resolve(stream);
|
|
@@ -85,23 +126,25 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
|
|
|
85
126
|
extenders.forEach((extender)=>{
|
|
86
127
|
if (extender.processStream) processedStream = extender.processStream(processedStream);
|
|
87
128
|
});
|
|
88
|
-
reactStreamingPipe(passThrough);
|
|
89
|
-
processedStream.pipe(body);
|
|
90
129
|
try {
|
|
91
130
|
const storageContext = storage.useContext?.();
|
|
92
131
|
const activeDeferreds = storageContext?.activeDeferreds;
|
|
132
|
+
const deferredScriptKeys = storageContext?.deferredScriptKeys;
|
|
93
133
|
const entries = activeDeferreds instanceof Map ? Array.from(activeDeferreds.entries()) : [];
|
|
94
134
|
if (entries.length > 0) {
|
|
95
135
|
const enqueueScript = (s)=>{
|
|
96
|
-
if (
|
|
136
|
+
if (!outputState.coordinator) throw new Error("Deferred script coordinator is not ready");
|
|
137
|
+
if (shellChunkStatus === ShellChunkStatus.FINISH) outputState.coordinator.enqueueResolver(s);
|
|
97
138
|
else pendingScripts.push(s);
|
|
98
139
|
};
|
|
99
|
-
enqueueFromEntries(entries, config.nonce,
|
|
140
|
+
deferredResolversComplete = enqueueFromEntries(entries, deferredScriptKeys, config.nonce, enqueueScript);
|
|
100
141
|
}
|
|
101
142
|
} catch (err) {
|
|
102
143
|
const monitors = getMonitors();
|
|
103
144
|
monitors.error("cannot inject router data script", err);
|
|
104
145
|
}
|
|
146
|
+
reactStreamingPipe(passThrough);
|
|
147
|
+
processedStream.pipe(body);
|
|
105
148
|
});
|
|
106
149
|
},
|
|
107
150
|
onShellError (error) {
|
|
@@ -4,6 +4,7 @@ import { storage } from "@modern-js/runtime-utils/node";
|
|
|
4
4
|
import { ESCAPED_SHELL_STREAM_END_MARK } from "../../../common.mjs";
|
|
5
5
|
import { RenderLevel } from "../../constants.mjs";
|
|
6
6
|
import { enqueueFromEntries } from "./deferredScript.mjs";
|
|
7
|
+
import { DeferredScriptOutputCoordinator } from "./deferredScriptOutputCoordinator.mjs";
|
|
7
8
|
import { ShellChunkStatus, encodeForWebStream, getReadableStreamFromString, resolveStreamingMode } from "./shared.mjs";
|
|
8
9
|
import { getTemplates } from "./template.mjs";
|
|
9
10
|
const createReadableStreamFromElement = async (request, rootElement, options)=>{
|
|
@@ -36,10 +37,13 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
|
|
|
36
37
|
const { waitForAllReady } = resolveStreamingMode(request, forceStreamToString);
|
|
37
38
|
if (waitForAllReady) await readableOriginal.allReady;
|
|
38
39
|
const reader = readableOriginal.getReader();
|
|
40
|
+
let coordinator;
|
|
39
41
|
const stream = new ReadableStream({
|
|
40
42
|
start (controller) {
|
|
43
|
+
const decoder = new TextDecoder();
|
|
41
44
|
const pendingScripts = [];
|
|
42
45
|
let isClosed = false;
|
|
46
|
+
let deferredResolversComplete = Promise.resolve();
|
|
43
47
|
const safeEnqueue = (chunk)=>{
|
|
44
48
|
if (isClosed) return;
|
|
45
49
|
try {
|
|
@@ -56,38 +60,55 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
|
|
|
56
60
|
} catch {}
|
|
57
61
|
}
|
|
58
62
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
};
|
|
63
|
-
const enqueueScript = (script)=>{
|
|
64
|
-
if (shellChunkStatus === ShellChunkStatus.FINISH) safeEnqueue(encodeForWebStream(script));
|
|
65
|
-
else pendingScripts.push(script);
|
|
66
|
-
};
|
|
63
|
+
coordinator = new DeferredScriptOutputCoordinator((content)=>{
|
|
64
|
+
safeEnqueue(encodeForWebStream(content));
|
|
65
|
+
});
|
|
67
66
|
const storageContext = storage.useContext?.();
|
|
68
67
|
const activeDeferreds = storageContext?.activeDeferreds;
|
|
68
|
+
const deferredScriptKeys = storageContext?.deferredScriptKeys;
|
|
69
69
|
const entries = activeDeferreds instanceof Map ? Array.from(activeDeferreds.entries()) : [];
|
|
70
|
-
if (entries.length > 0) enqueueFromEntries(entries, config.nonce,
|
|
70
|
+
if (entries.length > 0) deferredResolversComplete = enqueueFromEntries(entries, deferredScriptKeys, config.nonce, (script)=>{
|
|
71
|
+
if (!coordinator) throw new Error("Deferred script coordinator is not ready");
|
|
72
|
+
if (shellChunkStatus === ShellChunkStatus.FINISH) coordinator.enqueueResolver(script);
|
|
73
|
+
else pendingScripts.push(script);
|
|
74
|
+
});
|
|
71
75
|
async function push() {
|
|
72
76
|
try {
|
|
73
77
|
const { done, value } = await reader.read();
|
|
74
|
-
if (done)
|
|
78
|
+
if (done) {
|
|
79
|
+
const trailingText = decoder.decode();
|
|
80
|
+
if (trailingText) coordinator?.writeReact(trailingText);
|
|
81
|
+
await deferredResolversComplete;
|
|
82
|
+
coordinator?.finish();
|
|
83
|
+
closeController();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
75
86
|
if (isClosed) return;
|
|
76
87
|
if (shellChunkStatus !== ShellChunkStatus.FINISH) {
|
|
77
|
-
chunkVec.push(
|
|
88
|
+
chunkVec.push(decoder.decode(value, {
|
|
89
|
+
stream: true
|
|
90
|
+
}));
|
|
78
91
|
const concatedChunk = chunkVec.join('');
|
|
79
92
|
const markerIndex = concatedChunk.indexOf(ESCAPED_SHELL_STREAM_END_MARK);
|
|
80
93
|
if (-1 !== markerIndex) {
|
|
81
94
|
const beforeMark = concatedChunk.slice(0, markerIndex);
|
|
82
95
|
const afterMark = concatedChunk.slice(markerIndex + ESCAPED_SHELL_STREAM_END_MARK.length);
|
|
83
96
|
shellChunkStatus = ShellChunkStatus.FINISH;
|
|
84
|
-
|
|
85
|
-
if (afterMark)
|
|
86
|
-
|
|
97
|
+
coordinator?.writeReact(`${shellBefore}${beforeMark}${shellAfter}`);
|
|
98
|
+
if (afterMark) coordinator?.writeReact(afterMark);
|
|
99
|
+
coordinator?.markShellFinished();
|
|
100
|
+
for (const script of pendingScripts)coordinator?.enqueueResolver(script);
|
|
101
|
+
pendingScripts.length = 0;
|
|
87
102
|
}
|
|
88
|
-
} else
|
|
103
|
+
} else {
|
|
104
|
+
const decodedChunk = decoder.decode(value, {
|
|
105
|
+
stream: true
|
|
106
|
+
});
|
|
107
|
+
if (decodedChunk) coordinator?.writeReact(decodedChunk);
|
|
108
|
+
}
|
|
89
109
|
if (!isClosed) push();
|
|
90
110
|
} catch (error) {
|
|
111
|
+
coordinator?.abort();
|
|
91
112
|
if (!isClosed) {
|
|
92
113
|
isClosed = true;
|
|
93
114
|
try {
|
|
@@ -99,6 +120,7 @@ const createReadableStreamFromElement = async (request, rootElement, options)=>{
|
|
|
99
120
|
push();
|
|
100
121
|
},
|
|
101
122
|
cancel (reason) {
|
|
123
|
+
coordinator?.abort();
|
|
102
124
|
reader.cancel(reason).catch(()=>{});
|
|
103
125
|
}
|
|
104
126
|
});
|
|
@@ -41,23 +41,29 @@ function buildDeferredDataScript(nonce, args) {
|
|
|
41
41
|
const nonceAttr = nonce ? ` nonce="${nonce}"` : '';
|
|
42
42
|
return `<script async${nonceAttr} data-fn-name="r" data-script-src="modern-run-router-data-fn" data-fn-args='${escaped}' suppressHydrationWarning>${runRouterDataFnStr}</script>`;
|
|
43
43
|
}
|
|
44
|
-
function enqueueFromEntries(entries, nonce, emit) {
|
|
44
|
+
function enqueueFromEntries(entries, deferredScriptKeys, nonce, emit) {
|
|
45
|
+
const pendingResolvers = [];
|
|
45
46
|
entries.forEach(([routeId, value])=>{
|
|
46
47
|
if (!isDeferredDataLike(value)) return;
|
|
47
|
-
const
|
|
48
|
-
|
|
48
|
+
const serializedKeys = deferredScriptKeys ? deferredScriptKeys.get(routeId) ?? [] : value.pendingKeys ?? [];
|
|
49
|
+
serializedKeys.forEach((key)=>{
|
|
49
50
|
const tracked = value.data?.[key];
|
|
50
|
-
if (isPromiseLike(tracked)) tracked.then((val)=>
|
|
51
|
+
if (isPromiseLike(tracked)) pendingResolvers.push(Promise.resolve(tracked).then((val)=>{
|
|
52
|
+
emit(buildDeferredDataScript(nonce, [
|
|
51
53
|
routeId,
|
|
52
54
|
key,
|
|
53
55
|
val
|
|
54
|
-
]))
|
|
56
|
+
]));
|
|
57
|
+
}, (err)=>{
|
|
58
|
+
emit(buildDeferredDataScript(nonce, [
|
|
55
59
|
routeId,
|
|
56
60
|
key,
|
|
57
61
|
void 0,
|
|
58
62
|
toErrorInfo(err)
|
|
59
|
-
]))
|
|
63
|
+
]));
|
|
64
|
+
}));
|
|
60
65
|
});
|
|
61
66
|
});
|
|
67
|
+
return Promise.all(pendingResolvers).then(()=>void 0);
|
|
62
68
|
}
|
|
63
69
|
export { buildDeferredDataScript, enqueueFromEntries, escapeAttr, isDeferredDataLike, isPromiseLike, isRecord, toErrorInfo };
|
|
@@ -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 };
|
|
@@ -14,7 +14,8 @@ const DeferredDataScripts = (props)=>{
|
|
|
14
14
|
}, []);
|
|
15
15
|
const deferredScripts = useMemo(()=>{
|
|
16
16
|
if (!staticContext) return null;
|
|
17
|
-
const
|
|
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 {
|
|
@@ -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>;
|