[Flight] Serialize Date (#26622)
This is kind of annoying because Date implements toJSON so JSON.stringify turns it into a string before calling our replacer function.
This commit is contained in:
parent
96fd2fb726
commit
c6db19f9cd
|
@ -580,6 +580,10 @@ export function parseModelString(
|
|||
// Special encoding for `undefined` which can't be serialized as JSON otherwise.
|
||||
return undefined;
|
||||
}
|
||||
case 'D': {
|
||||
// Date
|
||||
return new Date(Date.parse(value.substring(2)));
|
||||
}
|
||||
case 'n': {
|
||||
// BigInt
|
||||
return BigInt(value.substring(2));
|
||||
|
|
|
@ -101,6 +101,12 @@ function serializeUndefined(): string {
|
|||
return '$undefined';
|
||||
}
|
||||
|
||||
function serializeDateFromDateJSON(dateJSON: string): string {
|
||||
// JSON.stringify automatically calls Date.prototype.toJSON which calls toISOString.
|
||||
// We need only tack on a $D prefix.
|
||||
return '$D' + dateJSON;
|
||||
}
|
||||
|
||||
function serializeBigInt(n: bigint): string {
|
||||
return '$n' + n.toString(10);
|
||||
}
|
||||
|
@ -133,10 +139,16 @@ export function processReply(
|
|||
value: ReactServerValue,
|
||||
): ReactJSONValue {
|
||||
const parent = this;
|
||||
|
||||
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
|
||||
if (__DEV__) {
|
||||
// $FlowFixMe[incompatible-use]
|
||||
const originalValue = this[key];
|
||||
if (typeof originalValue === 'object' && originalValue !== value) {
|
||||
const originalValue = parent[key];
|
||||
if (
|
||||
typeof originalValue === 'object' &&
|
||||
originalValue !== value &&
|
||||
!(originalValue instanceof Date)
|
||||
) {
|
||||
if (objectName(originalValue) !== 'Object') {
|
||||
console.error(
|
||||
'Only plain objects can be passed to Server Functions from the Client. ' +
|
||||
|
@ -266,6 +278,17 @@ export function processReply(
|
|||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// TODO: Maybe too clever. If we support URL there's no similar trick.
|
||||
if (value[value.length - 1] === 'Z') {
|
||||
// Possibly a Date, whose toJSON automatically calls toISOString
|
||||
// $FlowFixMe[incompatible-use]
|
||||
const originalValue = parent[key];
|
||||
// $FlowFixMe[method-unbinding]
|
||||
if (originalValue instanceof Date) {
|
||||
return serializeDateFromDateJSON(value);
|
||||
}
|
||||
}
|
||||
|
||||
return escapeStringValue(value);
|
||||
}
|
||||
|
||||
|
|
|
@ -306,6 +306,23 @@ describe('ReactFlight', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('can transport Date', async () => {
|
||||
function ComponentClient({prop}) {
|
||||
return `prop: ${prop.toISOString()}`;
|
||||
}
|
||||
const Component = clientReference(ComponentClient);
|
||||
|
||||
const model = <Component prop={new Date(1234567890123)} />;
|
||||
|
||||
const transport = ReactNoopFlightServer.render(model);
|
||||
|
||||
await act(async () => {
|
||||
ReactNoop.render(await ReactNoopFlightClient.read(transport));
|
||||
});
|
||||
|
||||
expect(ReactNoop).toMatchRenderedOutput('prop: 2009-02-13T23:31:30.123Z');
|
||||
});
|
||||
|
||||
it('can render a lazy component as a shared component on the server', async () => {
|
||||
function SharedComponent({text}) {
|
||||
return (
|
||||
|
@ -675,28 +692,39 @@ describe('ReactFlight', () => {
|
|||
});
|
||||
|
||||
it('should warn in DEV if a toJSON instance is passed to a host component', () => {
|
||||
const obj = {
|
||||
toJSON() {
|
||||
return 123;
|
||||
},
|
||||
};
|
||||
expect(() => {
|
||||
const transport = ReactNoopFlightServer.render(
|
||||
<input value={new Date()} />,
|
||||
);
|
||||
const transport = ReactNoopFlightServer.render(<input value={obj} />);
|
||||
ReactNoopFlightClient.read(transport);
|
||||
}).toErrorDev(
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Date objects are not supported.',
|
||||
'Objects with toJSON methods are not supported. ' +
|
||||
'Convert it manually to a simple value before passing it to props.\n' +
|
||||
' <input value={{toJSON: function}}>\n' +
|
||||
' ^^^^^^^^^^^^^^^^^^^^',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn in DEV if a toJSON instance is passed to a host component child', () => {
|
||||
class MyError extends Error {
|
||||
toJSON() {
|
||||
return 123;
|
||||
}
|
||||
}
|
||||
expect(() => {
|
||||
const transport = ReactNoopFlightServer.render(
|
||||
<div>Current date: {new Date()}</div>,
|
||||
<div>Womp womp: {new MyError('spaghetti')}</div>,
|
||||
);
|
||||
ReactNoopFlightClient.read(transport);
|
||||
}).toErrorDev(
|
||||
'Date objects cannot be rendered as text children. Try formatting it using toString().\n' +
|
||||
' <div>Current date: {Date}</div>\n' +
|
||||
' ^^^^^^',
|
||||
'Error objects cannot be rendered as text children. Try formatting it using toString().\n' +
|
||||
' <div>Womp womp: {Error}</div>\n' +
|
||||
' ^^^^^^^',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
@ -728,37 +756,46 @@ describe('ReactFlight', () => {
|
|||
});
|
||||
|
||||
it('should warn in DEV if a toJSON instance is passed to a Client Component', () => {
|
||||
const obj = {
|
||||
toJSON() {
|
||||
return 123;
|
||||
},
|
||||
};
|
||||
function ClientImpl({value}) {
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
const Client = clientReference(ClientImpl);
|
||||
expect(() => {
|
||||
const transport = ReactNoopFlightServer.render(
|
||||
<Client value={new Date()} />,
|
||||
);
|
||||
const transport = ReactNoopFlightServer.render(<Client value={obj} />);
|
||||
ReactNoopFlightClient.read(transport);
|
||||
}).toErrorDev(
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Date objects are not supported.',
|
||||
'Objects with toJSON methods are not supported.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn in DEV if a toJSON instance is passed to a Client Component child', () => {
|
||||
const obj = {
|
||||
toJSON() {
|
||||
return 123;
|
||||
},
|
||||
};
|
||||
function ClientImpl({children}) {
|
||||
return <div>{children}</div>;
|
||||
}
|
||||
const Client = clientReference(ClientImpl);
|
||||
expect(() => {
|
||||
const transport = ReactNoopFlightServer.render(
|
||||
<Client>Current date: {new Date()}</Client>,
|
||||
<Client>Current date: {obj}</Client>,
|
||||
);
|
||||
ReactNoopFlightClient.read(transport);
|
||||
}).toErrorDev(
|
||||
'Only plain objects can be passed to Client Components from Server Components. ' +
|
||||
'Date objects are not supported.\n' +
|
||||
' <>Current date: {Date}</>\n' +
|
||||
' ^^^^^^',
|
||||
'Objects with toJSON methods are not supported. ' +
|
||||
'Convert it manually to a simple value before passing it to props.\n' +
|
||||
' <>Current date: {{toJSON: function}}</>\n' +
|
||||
' ^^^^^^^^^^^^^^^^^^^^',
|
||||
{withoutStack: true},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -188,4 +188,13 @@ describe('ReactFlightDOMReply', () => {
|
|||
expect(formDataA2.get('greeting')).toBe('hello');
|
||||
expect(formDataB2.get('greeting')).toBe('hi');
|
||||
});
|
||||
|
||||
it('can pass a Date as a reply', async () => {
|
||||
const d = new Date(1234567890123);
|
||||
const body = await ReactServerDOMClient.encodeReply(d);
|
||||
const d2 = await ReactServerDOMServer.decodeReply(body, webpackServerMap);
|
||||
|
||||
expect(d).toEqual(d2);
|
||||
expect(d % 1000).toEqual(123); // double-check the milliseconds made it through
|
||||
});
|
||||
});
|
||||
|
|
|
@ -447,6 +447,10 @@ function parseModelString(
|
|||
// Special encoding for `undefined` which can't be serialized as JSON otherwise.
|
||||
return undefined;
|
||||
}
|
||||
case 'D': {
|
||||
// Date
|
||||
return new Date(Date.parse(value.substring(2)));
|
||||
}
|
||||
case 'n': {
|
||||
// BigInt
|
||||
return BigInt(value.substring(2));
|
||||
|
|
|
@ -571,6 +571,12 @@ function serializeUndefined(): string {
|
|||
return '$undefined';
|
||||
}
|
||||
|
||||
function serializeDateFromDateJSON(dateJSON: string): string {
|
||||
// JSON.stringify automatically calls Date.prototype.toJSON which calls toISOString.
|
||||
// We need only tack on a $D prefix.
|
||||
return '$D' + dateJSON;
|
||||
}
|
||||
|
||||
function serializeBigInt(n: bigint): string {
|
||||
return '$n' + n.toString(10);
|
||||
}
|
||||
|
@ -687,10 +693,15 @@ export function resolveModelToJSON(
|
|||
key: string,
|
||||
value: ReactClientValue,
|
||||
): ReactJSONValue {
|
||||
// Make sure that `parent[key]` wasn't JSONified before `value` was passed to us
|
||||
if (__DEV__) {
|
||||
// $FlowFixMe[incompatible-use]
|
||||
const originalValue = parent[key];
|
||||
if (typeof originalValue === 'object' && originalValue !== value) {
|
||||
if (
|
||||
typeof originalValue === 'object' &&
|
||||
originalValue !== value &&
|
||||
!(originalValue instanceof Date)
|
||||
) {
|
||||
if (objectName(originalValue) !== 'Object') {
|
||||
const jsxParentType = jsxChildrenParents.get(parent);
|
||||
if (typeof jsxParentType === 'string') {
|
||||
|
@ -892,6 +903,17 @@ export function resolveModelToJSON(
|
|||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
// TODO: Maybe too clever. If we support URL there's no similar trick.
|
||||
if (value[value.length - 1] === 'Z') {
|
||||
// Possibly a Date, whose toJSON automatically calls toISOString
|
||||
// $FlowFixMe[incompatible-use]
|
||||
const originalValue = parent[key];
|
||||
// $FlowFixMe[method-unbinding]
|
||||
if (originalValue instanceof Date) {
|
||||
return serializeDateFromDateJSON(value);
|
||||
}
|
||||
}
|
||||
|
||||
return escapeStringValue(value);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue