-
Notifications
You must be signed in to change notification settings - Fork 0
[Flight] Walk parsed JSON instead of using reviver for parsing RSC payload #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1038,7 +1038,7 @@ | |
| chunk.value = null; | ||
| chunk.reason = null; | ||
| try { | ||
| var value = JSON.parse(resolvedModel, chunk._response._fromJSON), | ||
| var value = parseModel(chunk._response, resolvedModel), | ||
| resolveListeners = chunk.value; | ||
| null !== resolveListeners && | ||
| ((chunk.value = null), | ||
|
|
@@ -1470,6 +1470,7 @@ | |
| get: function () { | ||
| return "This object has been omitted by React in the console log to avoid sending too much data from the server. Try logging smaller or more specific objects."; | ||
| }, | ||
| set: function () {}, | ||
| enumerable: !0, | ||
| configurable: !1 | ||
| }), | ||
|
|
@@ -1510,7 +1511,6 @@ | |
| this._nonce = nonce; | ||
| this._chunks = chunks; | ||
| this._stringDecoder = new TextDecoder(); | ||
| this._fromJSON = null; | ||
| this._rowLength = this._rowTag = this._rowID = this._rowState = 0; | ||
| this._buffer = []; | ||
| this._tempRefs = temporaryReferences; | ||
|
|
@@ -1525,7 +1525,6 @@ | |
| this._replayConsole = replayConsole; | ||
| this._rootEnvironmentName = | ||
| void 0 === environmentName ? "Server" : environmentName; | ||
| this._fromJSON = createFromJSONCallback(this); | ||
| } | ||
| function resolveModel(response, id, model) { | ||
| var chunks = response._chunks, | ||
|
|
@@ -1554,7 +1553,7 @@ | |
| function resolveModule(response, id, model) { | ||
| var chunks = response._chunks, | ||
| chunk = chunks.get(id); | ||
| model = JSON.parse(model, response._fromJSON); | ||
| model = parseModel(response, model); | ||
| var clientReference = resolveClientReference( | ||
| response._bundlerConfig, | ||
| model | ||
|
|
@@ -1801,7 +1800,7 @@ | |
| return response; | ||
| } | ||
| function resolveHint(response, code, model) { | ||
| response = JSON.parse(model, response._fromJSON); | ||
| response = parseModel(response, model); | ||
| model = ReactDOMSharedInternals.d; | ||
| switch (code) { | ||
| case "D": | ||
|
|
@@ -1959,7 +1958,7 @@ | |
| } | ||
| function resolveConsoleEntry(response, value) { | ||
| if (response._replayConsole) { | ||
| var payload = JSON.parse(value, response._fromJSON); | ||
| var payload = parseModel(response, value); | ||
| value = payload[0]; | ||
| var stackTrace = payload[1], | ||
| owner = payload[2], | ||
|
|
@@ -2120,67 +2119,90 @@ | |
| resolveModel(response, id, row); | ||
| } | ||
| } | ||
| function createFromJSONCallback(response) { | ||
| return function (key, value) { | ||
| if ("string" === typeof value) | ||
| return parseModelString(response, this, key, value); | ||
| if ("object" === typeof value && null !== value) { | ||
| function parseModel(response, json) { | ||
| json = JSON.parse(json); | ||
| return reviveModel(response, json, { "": json }, ""); | ||
| } | ||
| function reviveModel(response, value, parentObject, key) { | ||
| if ("string" === typeof value) | ||
| return "$" === value[0] | ||
| ? parseModelString(response, parentObject, key, value) | ||
| : value; | ||
| if ("object" !== typeof value || null === value) return value; | ||
| if (isArrayImpl(value)) { | ||
| for (var i = 0; i < value.length; i++) | ||
| value[i] = reviveModel(response, value[i], value, "" + i); | ||
| if (value[0] === REACT_ELEMENT_TYPE) { | ||
| if (value[0] === REACT_ELEMENT_TYPE) | ||
| if ( | ||
| ((key = value[4]), | ||
| (value = { | ||
| b: { | ||
| i = value[4]; | ||
| value = { | ||
| $$typeof: REACT_ELEMENT_TYPE, | ||
| type: value[1], | ||
| key: value[2], | ||
| props: value[3], | ||
| _owner: null === key ? response._debugRootOwner : key | ||
| }), | ||
| _owner: null === i ? response._debugRootOwner : i | ||
| }; | ||
| Object.defineProperty(value, "ref", { | ||
| enumerable: !1, | ||
| get: nullRefGetter | ||
| }), | ||
| (value._store = {}), | ||
| }); | ||
| value._store = {}; | ||
| Object.defineProperty(value._store, "validated", { | ||
| configurable: !1, | ||
| enumerable: !1, | ||
| writable: !0, | ||
| value: 1 | ||
| }), | ||
| }); | ||
| Object.defineProperty(value, "_debugInfo", { | ||
| configurable: !1, | ||
| enumerable: !1, | ||
| writable: !0, | ||
| value: null | ||
| }), | ||
| null !== initializingHandler) | ||
| ) { | ||
| var handler = initializingHandler; | ||
| initializingHandler = handler.parent; | ||
| handler.errored | ||
| ? ((key = new ReactPromise( | ||
| }); | ||
| if (null !== initializingHandler) { | ||
| i = initializingHandler; | ||
| initializingHandler = i.parent; | ||
| if (i.errored) { | ||
| response = new ReactPromise( | ||
| "rejected", | ||
| null, | ||
| handler.value, | ||
| i.value, | ||
| response | ||
| )), | ||
| (value = { | ||
| ); | ||
| value = { | ||
| name: getComponentNameFromType(value.type) || "", | ||
| owner: value._owner | ||
| }), | ||
| (key._debugInfo = [value]), | ||
| (value = createLazyChunkWrapper(key))) | ||
| : 0 < handler.deps && | ||
| ((key = new ReactPromise("blocked", null, null, response)), | ||
| (handler.value = value), | ||
| (handler.chunk = key), | ||
| (value = Object.freeze.bind(Object, value.props)), | ||
| key.then(value, value), | ||
| (value = createLazyChunkWrapper(key))); | ||
| } else Object.freeze(value.props); | ||
| return value; | ||
| }; | ||
| response._debugInfo = [value]; | ||
| response = createLazyChunkWrapper(response); | ||
| break b; | ||
| } | ||
| if (0 < i.deps) { | ||
| response = new ReactPromise("blocked", null, null, response); | ||
| i.value = value; | ||
| i.chunk = response; | ||
| value = Object.freeze.bind(Object, value.props); | ||
| response.then(value, value); | ||
| response = createLazyChunkWrapper(response); | ||
| break b; | ||
| } | ||
| } else Object.freeze(value.props); | ||
| response = value; | ||
| } | ||
| else response = value; | ||
| return response; | ||
| } | ||
| return value; | ||
| }; | ||
| } | ||
| for (i in value) | ||
| "__proto__" === i | ||
| ? delete value[i] | ||
| : ((parentObject = reviveModel(response, value[i], value, i)), | ||
| void 0 !== parentObject | ||
| ? (value[i] = parentObject) | ||
| : delete value[i]); | ||
|
Comment on lines
+2132
to
+2204
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Demonstrates the strict-mode accessor write and inherited-key hazards.
node <<'NODE'
"use strict";
function installOmittedAccessor(parentObject, key) {
Object.defineProperty(parentObject, key, {
get: function () {
return "omitted";
},
enumerable: true,
configurable: false
});
return null;
}
const payload = { omitted: "$Y" };
try {
payload.omitted = installOmittedAccessor(payload, "omitted");
console.log("assignment did not throw");
} catch (error) {
console.log("strict accessor assignment:", error.constructor.name);
}
Object.prototype.__rscInherited = "$undefined";
const model = JSON.parse('{"own":1}');
const keys = [];
for (const key in model) keys.push(key);
delete Object.prototype.__rscInherited;
console.log("for-in keys:", keys.join(","));
NODERepository: shakacode/react_on_rails_rsc Length of output: 141 🏁 Script executed: fd -t f "react-server-dom-webpack-client.browser.development.js"Repository: shakacode/react_on_rails_rsc Length of output: 159 🏁 Script executed: # Check file size first
wc -l src/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.jsRepository: shakacode/react_on_rails_rsc Length of output: 164 🏁 Script executed: # Read the section mentioned in the review comment (lines 2131-2203)
sed -n '2131,2203p' src/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.jsRepository: shakacode/react_on_rails_rsc Length of output: 2720 🏁 Script executed: # Check for "use strict" at the beginning of the file
head -20 src/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.jsRepository: shakacode/react_on_rails_rsc Length of output: 632 🏁 Script executed: # Find the parseModelString function to see the "$Y" case
rg -n "parseModelString" src/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.js | head -20Repository: shakacode/react_on_rails_rsc Length of output: 214 🏁 Script executed: # Read the parseModelString function (starting at line 1352)
sed -n '1352,1500p' src/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.jsRepository: shakacode/react_on_rails_rsc Length of output: 4741 🏁 Script executed: # Find the reviveModel function
rg -n "function reviveModel" src/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.jsRepository: shakacode/react_on_rails_rsc Length of output: 139 🏁 Script executed: # Read the reviveModel function (starting at line 2125)
sed -n '2125,2210p' src/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.development.jsRepository: shakacode/react_on_rails_rsc Length of output: 3158 Prevent strict-mode TypeError when reviveModel assigns to a When Guard revived property assignments and use own-property-only iteration: Suggested fix function parseModel(response, json) {
json = JSON.parse(json);
return reviveModel(response, json, { "": json }, "");
}
+ function setRevivedProperty(target, key, revivedValue) {
+ var descriptor = Object.getOwnPropertyDescriptor(target, key);
+ if (!descriptor || descriptor.writable || descriptor.set)
+ target[key] = revivedValue;
+ }
function reviveModel(response, value, parentObject, key) {
if ("string" === typeof value)
return "$" === value[0]
? parseModelString(response, parentObject, key, value)
@@
if ("object" !== typeof value || null === value) return value;
if (isArrayImpl(value)) {
for (var i = 0; i < value.length; i++)
- value[i] = reviveModel(response, value[i], value, "" + i);
+ setRevivedProperty(
+ value,
+ "" + i,
+ reviveModel(response, value[i], value, "" + i)
+ );
@@
- for (i in value)
- "__proto__" === i
- ? delete value[i]
- : ((parentObject = reviveModel(response, value[i], value, i)),
- void 0 !== parentObject
- ? (value[i] = parentObject)
- : delete value[i]);
+ for (i in value)
+ if (hasOwnProperty.call(value, i))
+ "__proto__" === i
+ ? delete value[i]
+ : ((parentObject = reviveModel(response, value[i], value, i)),
+ void 0 !== parentObject
+ ? setRevivedProperty(value, i, parentObject)
+ : delete value[i]); |
||
| return value; | ||
| } | ||
| function createResponseFromOptions(options) { | ||
| return new ResponseInstance( | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -665,7 +665,7 @@ function initializeModelChunk(chunk) { | |||||||||||||
| chunk.value = null; | ||||||||||||||
| chunk.reason = null; | ||||||||||||||
| try { | ||||||||||||||
| var value = JSON.parse(resolvedModel, chunk._response._fromJSON), | ||||||||||||||
| var value = parseModel(chunk._response, resolvedModel), | ||||||||||||||
| resolveListeners = chunk.value; | ||||||||||||||
| null !== resolveListeners && | ||||||||||||||
| ((chunk.value = null), | ||||||||||||||
|
|
@@ -1033,11 +1033,9 @@ function ResponseInstance( | |||||||||||||
| this._nonce = nonce; | ||||||||||||||
| this._chunks = chunks; | ||||||||||||||
| this._stringDecoder = new TextDecoder(); | ||||||||||||||
| this._fromJSON = null; | ||||||||||||||
| this._rowLength = this._rowTag = this._rowID = this._rowState = 0; | ||||||||||||||
| this._buffer = []; | ||||||||||||||
| this._tempRefs = temporaryReferences; | ||||||||||||||
| this._fromJSON = createFromJSONCallback(this); | ||||||||||||||
| } | ||||||||||||||
| function resolveBuffer(response, id, buffer) { | ||||||||||||||
| var chunks = response._chunks, | ||||||||||||||
|
|
@@ -1049,7 +1047,7 @@ function resolveBuffer(response, id, buffer) { | |||||||||||||
| function resolveModule(response, id, model) { | ||||||||||||||
| var chunks = response._chunks, | ||||||||||||||
| chunk = chunks.get(id); | ||||||||||||||
| model = JSON.parse(model, response._fromJSON); | ||||||||||||||
| model = parseModel(response, model); | ||||||||||||||
| var clientReference = resolveClientReference(response._bundlerConfig, model); | ||||||||||||||
| if ((model = preloadModule(clientReference))) { | ||||||||||||||
| if (chunk) { | ||||||||||||||
|
|
@@ -1357,7 +1355,7 @@ function processFullBinaryRow(response, id, tag, buffer, chunk) { | |||||||||||||
| case 72: | ||||||||||||||
| id = buffer[0]; | ||||||||||||||
| buffer = buffer.slice(1); | ||||||||||||||
| response = JSON.parse(buffer, response._fromJSON); | ||||||||||||||
| response = parseModel(response, buffer); | ||||||||||||||
| buffer = ReactDOMSharedInternals.d; | ||||||||||||||
| switch (id) { | ||||||||||||||
| case "D": | ||||||||||||||
|
|
@@ -1447,45 +1445,58 @@ function processFullBinaryRow(response, id, tag, buffer, chunk) { | |||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| function createFromJSONCallback(response) { | ||||||||||||||
| return function (key, value) { | ||||||||||||||
| if ("string" === typeof value) | ||||||||||||||
| return parseModelString(response, this, key, value); | ||||||||||||||
| if ("object" === typeof value && null !== value) { | ||||||||||||||
| if (value[0] === REACT_ELEMENT_TYPE) { | ||||||||||||||
| if ( | ||||||||||||||
| ((key = { | ||||||||||||||
| function parseModel(response, json) { | ||||||||||||||
| json = JSON.parse(json); | ||||||||||||||
| return reviveModel(response, json, { "": json }, ""); | ||||||||||||||
| } | ||||||||||||||
| function reviveModel(response, value, parentObject, key) { | ||||||||||||||
| if ("string" === typeof value) | ||||||||||||||
| return "$" === value[0] | ||||||||||||||
| ? parseModelString(response, parentObject, key, value) | ||||||||||||||
| : value; | ||||||||||||||
| if ("object" !== typeof value || null === value) return value; | ||||||||||||||
| if (isArrayImpl(value)) { | ||||||||||||||
| for (var i = 0; i < value.length; i++) | ||||||||||||||
| value[i] = reviveModel(response, value[i], value, "" + i); | ||||||||||||||
| if (value[0] === REACT_ELEMENT_TYPE) { | ||||||||||||||
| if (value[0] === REACT_ELEMENT_TYPE) | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant check (compiler artifact) — the outer This appears identically in all 8 compiled files. It looks like a decompiler/compiler artifact in the upstream fork's output (
Suggested change
(i.e. remove the redundant outer
Comment on lines
+1461
to
+1462
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The outer The correct structure (matching the upstream intent) should collapse these into a single check with the labeled block applied directly:
Suggested change
And remove the outer |
||||||||||||||
| b: { | ||||||||||||||
| value = { | ||||||||||||||
| $$typeof: REACT_ELEMENT_TYPE, | ||||||||||||||
| type: value[1], | ||||||||||||||
| key: value[2], | ||||||||||||||
| ref: null, | ||||||||||||||
| props: value[3] | ||||||||||||||
| }), | ||||||||||||||
| null !== initializingHandler) | ||||||||||||||
| ) | ||||||||||||||
| if ( | ||||||||||||||
| ((value = initializingHandler), | ||||||||||||||
| (initializingHandler = value.parent), | ||||||||||||||
| value.errored) | ||||||||||||||
| ) | ||||||||||||||
| (key = new ReactPromise("rejected", null, value.value, response)), | ||||||||||||||
| (key = createLazyChunkWrapper(key)); | ||||||||||||||
| else if (0 < value.deps) { | ||||||||||||||
| var blockedChunk = new ReactPromise( | ||||||||||||||
| "blocked", | ||||||||||||||
| null, | ||||||||||||||
| null, | ||||||||||||||
| response | ||||||||||||||
| ); | ||||||||||||||
| value.value = key; | ||||||||||||||
| value.chunk = blockedChunk; | ||||||||||||||
| key = createLazyChunkWrapper(blockedChunk); | ||||||||||||||
| }; | ||||||||||||||
| if (null !== initializingHandler) { | ||||||||||||||
| i = initializingHandler; | ||||||||||||||
| initializingHandler = i.parent; | ||||||||||||||
| if (i.errored) { | ||||||||||||||
| response = new ReactPromise("rejected", null, i.value, response); | ||||||||||||||
| response = createLazyChunkWrapper(response); | ||||||||||||||
| break b; | ||||||||||||||
| } | ||||||||||||||
| if (0 < i.deps) { | ||||||||||||||
| response = new ReactPromise("blocked", null, null, response); | ||||||||||||||
| i.value = value; | ||||||||||||||
| i.chunk = response; | ||||||||||||||
| response = createLazyChunkWrapper(response); | ||||||||||||||
| break b; | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| } else key = value; | ||||||||||||||
| return key; | ||||||||||||||
| response = value; | ||||||||||||||
| } | ||||||||||||||
| else response = value; | ||||||||||||||
|
Comment on lines
+1461
to
+1489
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The inner |
||||||||||||||
| return response; | ||||||||||||||
| } | ||||||||||||||
| return value; | ||||||||||||||
| }; | ||||||||||||||
| } | ||||||||||||||
| for (i in value) | ||||||||||||||
| "__proto__" === i | ||||||||||||||
| ? delete value[i] | ||||||||||||||
| : ((parentObject = reviveModel(response, value[i], value, i)), | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Same pattern exists in all 8 compiled files — it's a minifier artifact (variable reuse). A simple rename to |
||||||||||||||
| void 0 !== parentObject ? (value[i] = parentObject) : delete value[i]); | ||||||||||||||
| return value; | ||||||||||||||
| } | ||||||||||||||
| function createResponseFromOptions(options) { | ||||||||||||||
| return new ResponseInstance( | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The empty setter prevents a
TypeErrorin strict mode when consuming code (e.g., a serializer or proxy) tries to assign to the omitted-object placeholder property. Without it,configurable: false+ no setter = silent failure in sloppy mode but a throw in strict mode. Intentional and correct — just confirm this matches the upstream source in the React fork.