530
status: 0 | 1 | 3 | 4 | 5,
531
model: ReactClientValue,
532
ping: () => void,
533
- toJSON: (key: string, value: ReactClientValue) => ReactJSONValue,
533
keyPath: ReactKey, // parent server component keys
534
implicitSlot: boolean, // true if the root server component of this sequence had a null key
535
formatContext: FormatContext, // an approximate parent context from host components
2760
implicitSlot,
2761
formatContext: formatContext,
2762
ping: () => pingTask(request, task),
2764
- toJSON: function (
2765
- this:
2766
- | {+[key: string | number]: ReactClientValue}
2767
- | $ReadOnlyArray<ReactClientValue>,
2768
- parentPropertyName: string,
2769
- value: ReactClientValue,
2770
- ): ReactJSONValue {
2771
- const parent = this;
2772
- // Make sure that `parent[parentPropertyName]` wasn't JSONified before `value` was passed to us
2773
- if (__DEV__) {
2774
- // $FlowFixMe[incompatible-use]
2775
- const originalValue = parent[parentPropertyName];
2776
- if (
2777
- typeof originalValue === 'object' &&
2778
- originalValue !== value &&
2779
- !(originalValue instanceof Date)
2780
- ) {
2781
- // Call with the server component as the currently rendering component
2782
- // for context.
2783
- callWithDebugContextInDEV(request, task, () => {
2784
- if (objectName(originalValue) !== 'Object') {
2785
- const jsxParentType = jsxChildrenParents.get(parent);
2786
- if (typeof jsxParentType === 'string') {
2787
- console.error(
2788
- '%s objects cannot be rendered as text children. Try formatting it using toString().%s',
2789
- objectName(originalValue),
2790
- describeObjectForErrorMessage(parent, parentPropertyName),
2791
- );
2792
- } else {
2793
- console.error(
2794
- 'Only plain objects can be passed to Client Components from Server Components. ' +
2795
- '%s objects are not supported.%s',
2796
- objectName(originalValue),
2797
- describeObjectForErrorMessage(parent, parentPropertyName),
2798
- );
2799
- }
2800
- } else {
2801
- console.error(
2802
- 'Only plain objects can be passed to Client Components from Server Components. ' +
2803
- 'Objects with toJSON methods are not supported. Convert it manually ' +
2804
- 'to a simple value before passing it to props.%s',
2805
- describeObjectForErrorMessage(parent, parentPropertyName),
2806
- );
2807
- }
2808
- });
2809
- }
2810
- }
2811
- return renderModel(request, task, parent, parentPropertyName, value);
2812
- },
2763
thenableState: null,
2764
} as Omit<
2765
Task,
2787
return task;
2788
}
2789
2790
+function resolveModel(
2791
+ request: Request,
2792
+ task: Task,
2793
+ parent:
2794
+ | {+[key: string | number]: ReactClientValue}
2795
+ | $ReadOnlyArray<ReactClientValue>,
2796
+ parentPropertyName: string,
2797
+ value: ReactClientValue,
2798
+): ReactJSONValue {
2799
+ // Replicate JSON.stringify's toJSON semantics: if the value has a toJSON
2800
+ // method, call it first. In practice this only matters for Date objects whose
2801
+ // toJSON calls toISOString. Custom toJSON objects are not supported and will
2802
+ // trigger a DEV warning below.
2803
+ let jsonValue: ReactClientValue = value;
2804
+ if (
2805
+ value !== null &&
2806
+ typeof value === 'object' &&
2807
+ // $FlowFixMe[method-unbinding]
2808
+ typeof value.toJSON === 'function'
2809
+ ) {
2810
+ // $FlowFixMe[incompatible-use]
2811
+ jsonValue = value.toJSON(parentPropertyName);
2812
+ }
2813
+
2814
+ if (__DEV__) {
2815
+ // $FlowFixMe[incompatible-use]
2816
+ const originalValue = parent[parentPropertyName];
2817
+ if (
2818
+ typeof originalValue === 'object' &&
2819
+ originalValue !== jsonValue &&
2820
+ !(originalValue instanceof Date)
2821
+ ) {
2822
+ // Call with the server component as the currently rendering component
2823
+ // for context.
2824
+ callWithDebugContextInDEV(request, task, () => {
2825
+ if (objectName(originalValue) !== 'Object') {
2826
+ const jsxParentType = jsxChildrenParents.get(parent);
2827
+ if (typeof jsxParentType === 'string') {
2828
+ console.error(
2829
+ '%s objects cannot be rendered as text children. Try formatting it using toString().%s',
2830
+ objectName(originalValue),
2831
+ describeObjectForErrorMessage(parent, parentPropertyName),
2832
+ );
2833
+ } else {
2834
+ console.error(
2835
+ 'Only plain objects can be passed to Client Components from Server Components. ' +
2836
+ '%s objects are not supported.%s',
2837
+ objectName(originalValue),
2838
+ describeObjectForErrorMessage(parent, parentPropertyName),
2839
+ );
2840
+ }
2841
+ } else {
2842
+ console.error(
2843
+ 'Only plain objects can be passed to Client Components from Server Components. ' +
2844
+ 'Objects with toJSON methods are not supported. Convert it manually ' +
2845
+ 'to a simple value before passing it to props.%s',
2846
+ describeObjectForErrorMessage(parent, parentPropertyName),
2847
+ );
2848
+ }
2849
+ });
2850
+ }
2851
+ }
2852
+
2853
+ const rendered = renderModel(
2854
+ request,
2855
+ task,
2856
+ parent,
2857
+ parentPropertyName,
2858
+ jsonValue,
2859
+ );
2860
+
2861
+ if (rendered === null || typeof rendered !== 'object') {
2862
+ return rendered;
2863
+ }
2864
+
2865
+ if (isArray(rendered)) {
2866
+ const resolved: Array<ReactJSONValue> = [];
2867
+ for (let i = 0; i < rendered.length; i++) {
2868
+ resolved[i] = resolveModel(request, task, rendered, '' + i, rendered[i]);
2869
+ }
2870
+ return resolved;
2871
+ }
2872
+
2873
+ // Use `{}` for fast properties; `__proto__` is handled below because simple
2874
+ // assignment would hit Object.prototype's setter instead of creating a key.
2875
+ const resolved: {[key: string]: ReactJSONValue} = {} as any;
2876
+ for (const key in rendered) {
2877
+ if (hasOwnProperty.call(rendered, key)) {
2878
+ const resolvedValue = resolveModel(
2879
+ request,
2880
+ task,
2881
+ rendered,
2882
+ key,
2883
+ rendered[key],
2884
+ );
2885
+ if (key === __PROTO__) {
2886
+ // Match JSON's ordinary data-property semantics for this legacy key.
2887
+ Object.defineProperty(resolved, key, {
2888
+ value: resolvedValue,
2889
+ enumerable: true,
2890
+ writable: true,
2891
+ configurable: true,
2892
+ });
2893
+ } else {
2894
+ resolved[key] = resolvedValue;
2895
+ }
2896
+ }
2897
+ }
2898
+ return resolved;
2899
+}
2900
+
2901
function serializeByValueID(id: number): string {
2902
return '$' + id.toString(16);
2903
}
3679
// TODO: Pop this. Since we currently don't have a point where we can pop the stack
3680
// this debug information will be used for errors inside sibling properties that
3681
// are not elements. Leading to the wrong attribution on the server. We could fix
3621
- // that if we switch to a proper stack instead of JSON.stringify's trampoline.
3682
+ // that if we switch to a proper stack instead of resolveModel's recursive walk.
3683
// Attribution on the client is still correct since it has a pop.
3684
}
3685
5877
return;
5878
}
5879
// For anything else we need to try to serialize it using JSON.
5880
+ // We resolve the model tree first in pure JS to avoid the C++->JS boundary
5881
+ // overhead of JSON.stringify's replacer callback.
5882
+ const resolvedModel = resolveModel(request, task, {'': value}, '', value);
5883
// $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
5820
- const json: string = stringify(value, task.toJSON);
5884
+ const json: string = stringify(resolvedModel);
5885
emitModelChunk(request, task.id, json);
5886
}
5887
5927
try {
5928
// Track the root so we know that we have to emit this object even though it
5929
// already has an ID. This is needed because we might see this object twice
5866
- // in the same toJSON if it is cyclic.
5930
+ // in the same resolveModel walk if it is cyclic.
5931
modelRoot = task.model;
5932
5933
if (__DEV__) {
5986
// This is simulating what the JSON loop would do if this was part of it.
5987
emitChunk(request, task, resolvedModel);
5988
} else {
5925
- // If the value is a string, it means it's a terminal value and we already escaped it
5926
- // We don't need to escape it again so it's not passed the toJSON replacer.
5989
+ // If the value is a string, it means it's a terminal value and we already escaped it.
5990
+ // We don't need to escape it again so it's not passed through resolveModel.
5991
// $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
5992
const json: string = stringify(resolvedModel);
5993
emitModelChunk(request, task.id, json);