17
enableTaint,
18
enableServerComponentKeys,
19
enableRefAsProp,
20
+ enableServerComponentLogs,
21
} from 'shared/ReactFeatureFlags';
22
23
import {
112
113
initAsyncDebugInfo();
114
115
+function patchConsole(consoleInst: typeof console, methodName: string) {
116
+ const descriptor = Object.getOwnPropertyDescriptor(consoleInst, methodName);
117
+ if (
118
+ descriptor &&
119
+ (descriptor.configurable || descriptor.writable) &&
120
+ typeof descriptor.value === 'function'
121
+ ) {
122
+ const originalMethod = descriptor.value;
123
+ const originalName = Object.getOwnPropertyDescriptor(
124
+ // $FlowFixMe[incompatible-call]: We should be able to get descriptors from any function.
125
+ originalMethod,
126
+ 'name',
127
+ );
128
+ const wrapperMethod = function (this: typeof console) {
129
+ const request = resolveRequest();
130
+ if (methodName === 'assert' && arguments[0]) {
131
+ // assert doesn't emit anything unless first argument is falsy so we can skip it.
132
+ } else if (request !== null) {
133
+ // Extract the stack. Not all console logs print the full stack but they have at
134
+ // least the line it was called from. We could optimize transfer by keeping just
135
+ // one stack frame but keeping it simple for now and include all frames.
136
+ let stack = new Error().stack;
137
+ if (stack.startsWith('Error: \n')) {
138
+ stack = stack.slice(8);
139
+ }
140
+ const firstLine = stack.indexOf('\n');
141
+ if (firstLine === -1) {
142
+ stack = '';
143
+ } else {
144
+ // Skip the console wrapper itself.
145
+ stack = stack.slice(firstLine + 1);
146
+ }
147
+ request.pendingChunks++;
148
+ // We don't currently use this id for anything but we emit it so that we can later
149
+ // refer to previous logs in debug info to associate them with a component.
150
+ const id = request.nextChunkId++;
151
+ emitConsoleChunk(request, id, methodName, stack, arguments);
152
+ }
153
+ // $FlowFixMe[prop-missing]
154
+ return originalMethod.apply(this, arguments);
155
+ };
156
+ if (originalName) {
157
+ Object.defineProperty(
158
+ wrapperMethod,
159
+ // $FlowFixMe[cannot-write] yes it is
160
+ 'name',
161
+ originalName,
162
+ );
163
+ }
164
+ Object.defineProperty(consoleInst, methodName, {
165
+ value: wrapperMethod,
166
+ });
167
+ }
168
+}
169
+
170
+if (
171
+ enableServerComponentLogs &&
172
+ __DEV__ &&
173
+ typeof console === 'object' &&
174
+ console !== null
175
+) {
176
+ // Instrument console to capture logs for replaying on the client.
177
+ patchConsole(console, 'assert');
178
+ patchConsole(console, 'debug');
179
+ patchConsole(console, 'dir');
180
+ patchConsole(console, 'dirxml');
181
+ patchConsole(console, 'error');
182
+ patchConsole(console, 'group');
183
+ patchConsole(console, 'groupCollapsed');
184
+ patchConsole(console, 'groupEnd');
185
+ patchConsole(console, 'info');
186
+ patchConsole(console, 'log');
187
+ patchConsole(console, 'table');
188
+ patchConsole(console, 'trace');
189
+ patchConsole(console, 'warn');
190
+}
191
+
192
const ObjectPrototype = Object.prototype;
193
194
type JSONValue =
939
return '$L' + id.toString(16);
940
}
941
942
+function serializeInfinitePromise(): string {
943
+ return '$@';
944
+}
945
+
946
function serializePromiseID(id: number): string {
947
return '$@' + id.toString(16);
948
}
1721
}
1722
1723
function logPostpone(request: Request, reason: string): void {
1642
- const onPostpone = request.onPostpone;
1643
- onPostpone(reason);
1724
+ const prevRequest = currentRequest;
1725
+ currentRequest = null;
1726
+ try {
1727
+ const onPostpone = request.onPostpone;
1728
+ if (supportsRequestStorage) {
1729
+ // Exit the request context while running callbacks.
1730
+ requestStorage.run(undefined, onPostpone, reason);
1731
+ } else {
1732
+ onPostpone(reason);
1733
+ }
1734
+ } finally {
1735
+ currentRequest = prevRequest;
1736
+ }
1737
}
1738
1739
function logRecoverableError(request: Request, error: mixed): string {
1647
- const onError = request.onError;
1648
- const errorDigest = onError(error);
1740
+ const prevRequest = currentRequest;
1741
+ currentRequest = null;
1742
+ let errorDigest;
1743
+ try {
1744
+ const onError = request.onError;
1745
+ if (supportsRequestStorage) {
1746
+ // Exit the request context while running callbacks.
1747
+ errorDigest = requestStorage.run(undefined, onError, error);
1748
+ } else {
1749
+ errorDigest = onError(error);
1750
+ }
1751
+ } finally {
1752
+ currentRequest = prevRequest;
1753
+ }
1754
if (errorDigest != null && typeof errorDigest !== 'string') {
1755
// eslint-disable-next-line react-internal/prod-error-codes
1756
throw new Error(
1880
'emitDebugChunk should never be called in production mode. This is a bug in React.',
1881
);
1882
}
1883
+
1884
// $FlowFixMe[incompatible-type] stringify can return null
1885
const json: string = stringify(debugInfo);
1886
const row = serializeRowHeader('D', id) + json + '\n';
1888
request.completedRegularChunks.push(processedChunk);
1889
}
1890
1891
+function serializeEval(source: string): string {
1892
+ if (!__DEV__) {
1893
+ // These errors should never make it into a build so we don't need to encode them in codes.json
1894
+ // eslint-disable-next-line react-internal/prod-error-codes
1895
+ throw new Error(
1896
+ 'serializeEval should never be called in production mode. This is a bug in React.',
1897
+ );
1898
+ }
1899
+ return '$E' + source;
1900
+}
1901
+
1902
+// This is a forked version of renderModel which should never error, never suspend and is limited
1903
+// in the depth it can encode.
1904
+function renderConsoleValue(
1905
+ request: Request,
1906
+ counter: {objectCount: number},
1907
+ parent:
1908
+ | {+[propertyName: string | number]: ReactClientValue}
1909
+ | $ReadOnlyArray<ReactClientValue>,
1910
+ parentPropertyName: string,
1911
+ value: ReactClientValue,
1912
+): ReactJSONValue {
1913
+ // Make sure that `parent[parentPropertyName]` wasn't JSONified before `value` was passed to us
1914
+ // $FlowFixMe[incompatible-use]
1915
+ const originalValue = parent[parentPropertyName];
1916
+ if (
1917
+ typeof originalValue === 'object' &&
1918
+ originalValue !== value &&
1919
+ !(originalValue instanceof Date)
1920
+ ) {
1921
+ }
1922
+
1923
+ if (value === null) {
1924
+ return null;
1925
+ }
1926
+
1927
+ if (typeof value === 'object') {
1928
+ if (isClientReference(value)) {
1929
+ // We actually have this value on the client so we could import it.
1930
+ // This might be confusing though because on the Server it won't actually
1931
+ // be this value, so if you're debugging client references maybe you'd be
1932
+ // better with a place holder.
1933
+ return serializeClientReference(
1934
+ request,
1935
+ parent,
1936
+ parentPropertyName,
1937
+ (value: any),
1938
+ );
1939
+ }
1940
+
1941
+ if (counter.objectCount > 20) {
1942
+ // We've reached our max number of objects to serialize across the wire so we serialize this
1943
+ // object but no properties inside of it, as a place holder.
1944
+ return Array.isArray(value) ? [] : {};
1945
+ }
1946
+
1947
+ counter.objectCount++;
1948
+
1949
+ const writtenObjects = request.writtenObjects;
1950
+ const existingId = writtenObjects.get(value);
1951
+ // $FlowFixMe[method-unbinding]
1952
+ if (typeof value.then === 'function') {
1953
+ if (existingId !== undefined) {
1954
+ // We've seen this promise before, so we can just refer to the same result.
1955
+ return serializePromiseID(existingId);
1956
+ }
1957
+
1958
+ const thenable: Thenable<any> = (value: any);
1959
+ switch (thenable.status) {
1960
+ case 'fulfilled': {
1961
+ return serializePromiseID(
1962
+ outlineConsoleValue(request, counter, thenable.value),
1963
+ );
1964
+ }
1965
+ case 'rejected': {
1966
+ const x = thenable.reason;
1967
+ request.pendingChunks++;
1968
+ const errorId = request.nextChunkId++;
1969
+ if (
1970
+ enablePostpone &&
1971
+ typeof x === 'object' &&
1972
+ x !== null &&
1973
+ (x: any).$$typeof === REACT_POSTPONE_TYPE
1974
+ ) {
1975
+ const postponeInstance: Postpone = (x: any);
1976
+ // We don't log this postpone.
1977
+ emitPostponeChunk(request, errorId, postponeInstance);
1978
+ } else {
1979
+ // We don't log these errors since they didn't actually throw into Flight.
1980
+ const digest = '';
1981
+ emitErrorChunk(request, errorId, digest, x);
1982
+ }
1983
+ return serializePromiseID(errorId);
1984
+ }
1985
+ }
1986
+ // If it hasn't already resolved (and been instrumented) we just encode an infinite
1987
+ // promise that will never resolve.
1988
+ return serializeInfinitePromise();
1989
+ }
1990
+
1991
+ if (existingId !== undefined && existingId !== -1) {
1992
+ // We've already emitted this as a real object, so we can
1993
+ // just refer to that by its existing ID.
1994
+ return serializeByValueID(existingId);
1995
+ }
1996
+
1997
+ if (isArray(value)) {
1998
+ return value;
1999
+ }
2000
+
2001
+ if (value instanceof Map) {
2002
+ return serializeMap(request, value);
2003
+ }
2004
+ if (value instanceof Set) {
2005
+ return serializeSet(request, value);
2006
+ }
2007
+
2008
+ if (enableBinaryFlight) {
2009
+ if (value instanceof ArrayBuffer) {
2010
+ return serializeTypedArray(request, 'A', new Uint8Array(value));
2011
+ }
2012
+ if (value instanceof Int8Array) {
2013
+ // char
2014
+ return serializeTypedArray(request, 'C', value);
2015
+ }
2016
+ if (value instanceof Uint8Array) {
2017
+ // unsigned char
2018
+ return serializeTypedArray(request, 'c', value);
2019
+ }
2020
+ if (value instanceof Uint8ClampedArray) {
2021
+ // unsigned clamped char
2022
+ return serializeTypedArray(request, 'U', value);
2023
+ }
2024
+ if (value instanceof Int16Array) {
2025
+ // sort
2026
+ return serializeTypedArray(request, 'S', value);
2027
+ }
2028
+ if (value instanceof Uint16Array) {
2029
+ // unsigned short
2030
+ return serializeTypedArray(request, 's', value);
2031
+ }
2032
+ if (value instanceof Int32Array) {
2033
+ // long
2034
+ return serializeTypedArray(request, 'L', value);
2035
+ }
2036
+ if (value instanceof Uint32Array) {
2037
+ // unsigned long
2038
+ return serializeTypedArray(request, 'l', value);
2039
+ }
2040
+ if (value instanceof Float32Array) {
2041
+ // float
2042
+ return serializeTypedArray(request, 'F', value);
2043
+ }
2044
+ if (value instanceof Float64Array) {
2045
+ // double
2046
+ return serializeTypedArray(request, 'd', value);
2047
+ }
2048
+ if (value instanceof BigInt64Array) {
2049
+ // number
2050
+ return serializeTypedArray(request, 'N', value);
2051
+ }
2052
+ if (value instanceof BigUint64Array) {
2053
+ // unsigned number
2054
+ // We use "m" instead of "n" since JSON can start with "null"
2055
+ return serializeTypedArray(request, 'm', value);
2056
+ }
2057
+ if (value instanceof DataView) {
2058
+ return serializeTypedArray(request, 'V', value);
2059
+ }
2060
+ }
2061
+
2062
+ const iteratorFn = getIteratorFn(value);
2063
+ if (iteratorFn) {
2064
+ return Array.from((value: any));
2065
+ }
2066
+
2067
+ // $FlowFixMe[incompatible-return]
2068
+ return value;
2069
+ }
2070
+
2071
+ if (typeof value === 'string') {
2072
+ if (value[value.length - 1] === 'Z') {
2073
+ // Possibly a Date, whose toJSON automatically calls toISOString
2074
+ if (originalValue instanceof Date) {
2075
+ return serializeDateFromDateJSON(value);
2076
+ }
2077
+ }
2078
+ if (value.length >= 1024) {
2079
+ // For large strings, we encode them outside the JSON payload so that we
2080
+ // don't have to double encode and double parse the strings. This can also
2081
+ // be more compact in case the string has a lot of escaped characters.
2082
+ return serializeLargeTextString(request, value);
2083
+ }
2084
+ return escapeStringValue(value);
2085
+ }
2086
+
2087
+ if (typeof value === 'boolean') {
2088
+ return value;
2089
+ }
2090
+
2091
+ if (typeof value === 'number') {
2092
+ return serializeNumber(value);
2093
+ }
2094
+
2095
+ if (typeof value === 'undefined') {
2096
+ return serializeUndefined();
2097
+ }
2098
+
2099
+ if (typeof value === 'function') {
2100
+ if (isClientReference(value)) {
2101
+ return serializeClientReference(
2102
+ request,
2103
+ parent,
2104
+ parentPropertyName,
2105
+ (value: any),
2106
+ );
2107
+ }
2108
+
2109
+ // Serialize the body of the function as an eval so it can be printed.
2110
+ // $FlowFixMe[method-unbinding]
2111
+ return serializeEval('(' + Function.prototype.toString.call(value) + ')');
2112
+ }
2113
+
2114
+ if (typeof value === 'symbol') {
2115
+ const writtenSymbols = request.writtenSymbols;
2116
+ const existingId = writtenSymbols.get(value);
2117
+ if (existingId !== undefined) {
2118
+ return serializeByValueID(existingId);
2119
+ }
2120
+ // $FlowFixMe[incompatible-type] `description` might be undefined
2121
+ const name: string = value.description;
2122
+ // We use the Symbol.for version if it's not a global symbol. Close enough.
2123
+ request.pendingChunks++;
2124
+ const symbolId = request.nextChunkId++;
2125
+ emitSymbolChunk(request, symbolId, name);
2126
+ return serializeByValueID(symbolId);
2127
+ }
2128
+
2129
+ if (typeof value === 'bigint') {
2130
+ return serializeBigInt(value);
2131
+ }
2132
+
2133
+ return 'unknown type ' + typeof value;
2134
+}
2135
+
2136
+function outlineConsoleValue(
2137
+ request: Request,
2138
+ counter: {objectCount: number},
2139
+ model: ReactClientValue,
2140
+): number {
2141
+ if (!__DEV__) {
2142
+ // These errors should never make it into a build so we don't need to encode them in codes.json
2143
+ // eslint-disable-next-line react-internal/prod-error-codes
2144
+ throw new Error(
2145
+ 'outlineConsoleValue should never be called in production mode. This is a bug in React.',
2146
+ );
2147
+ }
2148
+
2149
+ function replacer(
2150
+ this:
2151
+ | {+[key: string | number]: ReactClientValue}
2152
+ | $ReadOnlyArray<ReactClientValue>,
2153
+ parentPropertyName: string,
2154
+ value: ReactClientValue,
2155
+ ): ReactJSONValue {
2156
+ try {
2157
+ return renderConsoleValue(
2158
+ request,
2159
+ counter,
2160
+ this,
2161
+ parentPropertyName,
2162
+ value,
2163
+ );
2164
+ } catch (x) {
2165
+ return 'unknown value';
2166
+ }
2167
+ }
2168
+
2169
+ // $FlowFixMe[incompatible-type] stringify can return null
2170
+ const json: string = stringify(model, replacer);
2171
+
2172
+ request.pendingChunks++;
2173
+ const id = request.nextChunkId++;
2174
+ const row = id.toString(16) + ':' + json + '\n';
2175
+ const processedChunk = stringToChunk(row);
2176
+ request.completedRegularChunks.push(processedChunk);
2177
+ return id;
2178
+}
2179
+
2180
+function emitConsoleChunk(
2181
+ request: Request,
2182
+ id: number,
2183
+ methodName: string,
2184
+ stackTrace: string,
2185
+ args: Array<any>,
2186
+): void {
2187
+ if (!__DEV__) {
2188
+ // These errors should never make it into a build so we don't need to encode them in codes.json
2189
+ // eslint-disable-next-line react-internal/prod-error-codes
2190
+ throw new Error(
2191
+ 'emitConsoleChunk should never be called in production mode. This is a bug in React.',
2192
+ );
2193
+ }
2194
+
2195
+ const counter = {objectCount: 0};
2196
+ function replacer(
2197
+ this:
2198
+ | {+[key: string | number]: ReactClientValue}
2199
+ | $ReadOnlyArray<ReactClientValue>,
2200
+ parentPropertyName: string,
2201
+ value: ReactClientValue,
2202
+ ): ReactJSONValue {
2203
+ try {
2204
+ return renderConsoleValue(
2205
+ request,
2206
+ counter,
2207
+ this,
2208
+ parentPropertyName,
2209
+ value,
2210
+ );
2211
+ } catch (x) {
2212
+ return 'unknown value';
2213
+ }
2214
+ }
2215
+
2216
+ const payload = [methodName, stackTrace];
2217
+ // $FlowFixMe[method-unbinding]
2218
+ payload.push.apply(payload, args);
2219
+ // $FlowFixMe[incompatible-type] stringify can return null
2220
+ const json: string = stringify(payload, replacer);
2221
+ const row = serializeRowHeader('W', id) + json + '\n';
2222
+ const processedChunk = stringToChunk(row);
2223
+ request.completedRegularChunks.push(processedChunk);
2224
+}
2225
+
2226
function forwardDebugInfo(
2227
request: Request,
2228
id: number,