Skip to content
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

fix: don't serialize undefined as null #7531

Merged
merged 2 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/moody-singers-develop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fix serialization of `undefined` in framework component props
37 changes: 25 additions & 12 deletions packages/astro/e2e/fixtures/pass-js/src/components/React.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
import type { BigNestedObject } from '../types';
import { useState } from 'react';

interface Props {
obj: BigNestedObject;
num: bigint;
arr: any[];
undefined: undefined;
null: null;
boolean: boolean;
number: number;
string: string;
bigint: bigint;
object: BigNestedObject;
array: any[];
map: Map<string, string>;
set: Set<string>;
}

const isNode = typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]';

/** a counter written in React */
export default function Component({ obj, num, arr, map, set }: Props) {
export default function Component({ undefined: undefinedProp, null: nullProp, boolean, number, string, bigint, object, array, map, set }: Props) {
// We are testing hydration, so don't return anything in the server.
if(isNode) {
return <div></div>
}

return (
<div>
<span id="nested-date">{obj.nested.date.toUTCString()}</span>
<span id="regexp-type">{Object.prototype.toString.call(obj.more.another.exp)}</span>
<span id="regexp-value">{obj.more.another.exp.source}</span>
<span id="bigint-type">{Object.prototype.toString.call(num)}</span>
<span id="bigint-value">{num.toString()}</span>
<span id="arr-type">{Object.prototype.toString.call(arr)}</span>
<span id="arr-value">{arr.join(',')}</span>
<span id="undefined-type">{Object.prototype.toString.call(undefinedProp)}</span>
<span id="null-type">{Object.prototype.toString.call(nullProp)}</span>
<span id="boolean-type">{Object.prototype.toString.call(boolean)}</span>
<span id="boolean-value">{boolean.toString()}</span>
<span id="number-type">{Object.prototype.toString.call(number)}</span>
<span id="number-value">{number.toString()}</span>
<span id="string-type">{Object.prototype.toString.call(string)}</span>
<span id="string-value">{string}</span>
<span id="bigint-type">{Object.prototype.toString.call(bigint)}</span>
<span id="bigint-value">{bigint.toString()}</span>
<span id="date-type">{Object.prototype.toString.call(object.nested.date)}</span>
<span id="date-value">{object.nested.date.toUTCString()}</span>
<span id="regexp-type">{Object.prototype.toString.call(object.more.another.exp)}</span>
<span id="regexp-value">{object.more.another.exp.source}</span>
<span id="array-type">{Object.prototype.toString.call(array)}</span>
<span id="array-value">{array.join(',')}</span>
<span id="map-type">{Object.prototype.toString.call(map)}</span>
<ul id="map-items">{Array.from(map).map(([key, value]) => (
<li>{key}: {value}</li>
Expand Down
18 changes: 15 additions & 3 deletions packages/astro/e2e/fixtures/pass-js/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
import type { BigNestedObject } from '../types';
import Component from '../components/React';
import { BigNestedObject } from '../types';

const obj: BigNestedObject = {
const object: BigNestedObject = {
nested: {
date: new Date('Thu, 09 Jun 2022 14:18:27 GMT')
},
Expand Down Expand Up @@ -30,7 +30,19 @@ set.add('test2');
</head>
<body>
<main>
<Component client:load obj={obj} num={11n} arr={[0, "foo"]} map={map} set={set} />
<Component
client:load
undefined={undefined}
null={null}
boolean={true}
number={16}
string={"abc"}
bigint={11n}
object={object}
array={[0, "foo"]}
map={map}
set={set}
/>
</main>
</body>
</html>
91 changes: 71 additions & 20 deletions packages/astro/e2e/pass-js.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,49 +16,97 @@ test.afterAll(async () => {
});

test.describe('Passing JS into client components', () => {
test('Complex nested objects', async ({ astro, page }) => {
test('Primitive values', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

const nestedDate = await page.locator('#nested-date');
await expect(nestedDate, 'component is visible').toBeVisible();
await expect(nestedDate).toHaveText('Thu, 09 Jun 2022 14:18:27 GMT');

const regeExpType = await page.locator('#regexp-type');
await expect(regeExpType, 'is visible').toBeVisible();
await expect(regeExpType).toHaveText('[object RegExp]');

const regExpValue = await page.locator('#regexp-value');
await expect(regExpValue, 'is visible').toBeVisible();
await expect(regExpValue).toHaveText('ok');
// undefined
const undefinedType = page.locator('#undefined-type');
await expect(undefinedType, 'is visible').toBeVisible();
await expect(undefinedType).toHaveText('[object Undefined]');

// null
const nullType = page.locator('#null-type');
await expect(nullType, 'is visible').toBeVisible();
await expect(nullType).toHaveText('[object Null]');

// boolean
const booleanType = page.locator('#boolean-type');
await expect(booleanType, 'is visible').toBeVisible();
await expect(booleanType).toHaveText('[object Boolean]');

const booleanValue = page.locator('#boolean-value');
await expect(booleanValue, 'is visible').toBeVisible();
await expect(booleanValue).toHaveText('true');

// number
const numberType = page.locator('#number-type');
await expect(numberType, 'is visible').toBeVisible();
await expect(numberType).toHaveText('[object Number]');

const numberValue = page.locator('#number-value');
await expect(numberValue, 'is visible').toBeVisible();
await expect(numberValue).toHaveText('16');

// string
const stringType = page.locator('#string-type');
await expect(stringType, 'is visible').toBeVisible();
await expect(stringType).toHaveText('[object String]');

const stringValue = page.locator('#string-value');
await expect(stringValue, 'is visible').toBeVisible();
await expect(stringValue).toHaveText('abc');
});

test('BigInts', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

const bigIntType = await page.locator('#bigint-type');
const bigIntType = page.locator('#bigint-type');
await expect(bigIntType, 'is visible').toBeVisible();
await expect(bigIntType).toHaveText('[object BigInt]');

const bigIntValue = await page.locator('#bigint-value');
const bigIntValue = page.locator('#bigint-value');
await expect(bigIntValue, 'is visible').toBeVisible();
await expect(bigIntValue).toHaveText('11');
});

test('Complex nested objects', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

// Date
const dateType = page.locator('#date-type');
await expect(dateType, 'is visible').toBeVisible();
await expect(dateType).toHaveText('[object Date]');

const dateValue = page.locator('#date-value');
await expect(dateValue, 'is visible').toBeVisible();
await expect(dateValue).toHaveText('Thu, 09 Jun 2022 14:18:27 GMT');

// RegExp
const regExpType = page.locator('#regexp-type');
await expect(regExpType, 'is visible').toBeVisible();
await expect(regExpType).toHaveText('[object RegExp]');

const regExpValue = page.locator('#regexp-value');
await expect(regExpValue, 'is visible').toBeVisible();
await expect(regExpValue).toHaveText('ok');
});

test('Arrays that look like the serialization format', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

const arrType = await page.locator('#arr-type');
await expect(arrType, 'is visible').toBeVisible();
await expect(arrType).toHaveText('[object Array]');
const arrayType = page.locator('#array-type');
await expect(arrayType, 'is visible').toBeVisible();
await expect(arrayType).toHaveText('[object Array]');

const arrValue = await page.locator('#arr-value');
await expect(arrValue, 'is visible').toBeVisible();
await expect(arrValue).toHaveText('0,foo');
const arrayValue = page.locator('#array-value');
await expect(arrayValue, 'is visible').toBeVisible();
await expect(arrayValue).toHaveText('0,foo');
});

test('Maps and Sets', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

// Map
const mapType = page.locator('#map-type');
await expect(mapType, 'is visible').toBeVisible();
await expect(mapType).toHaveText('[object Map]');
Expand All @@ -69,10 +117,13 @@ test.describe('Passing JS into client components', () => {
const texts = await mapValues.allTextContents();
expect(texts).toEqual(['test1: test2', 'test3: test4']);

// Set
const setType = page.locator('#set-type');
await expect(setType, 'is visible').toBeVisible();
await expect(setType).toHaveText('[object Set]');

const setValue = page.locator('#set-value');
await expect(setValue, 'is visible').toBeVisible();
await expect(setValue).toHaveText('test1,test2');
});
});
4 changes: 3 additions & 1 deletion packages/astro/src/runtime/server/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ function convertToSerializedForm(
value: any,
metadata: AstroComponentMetadata | Record<string, any> = {},
parents = new WeakSet<any>()
): [ValueOf<typeof PROP_TYPE>, any] {
): [ValueOf<typeof PROP_TYPE>, any] | [ValueOf<typeof PROP_TYPE>] {
const tag = Object.prototype.toString.call(value);
switch (tag) {
case '[object Date]': {
Expand Down Expand Up @@ -100,6 +100,8 @@ function convertToSerializedForm(
default: {
if (value !== null && typeof value === 'object') {
return [PROP_TYPE.Value, serializeObject(value, metadata, parents)];
} else if (value === undefined) {
return [PROP_TYPE.Value];
} else {
return [PROP_TYPE.Value, value];
}
Expand Down
27 changes: 26 additions & 1 deletion packages/astro/test/serialize.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,36 @@ import { expect } from 'chai';
import { serializeProps } from '../dist/runtime/server/serialize.js';

describe('serialize', () => {
it('serializes a plain value', () => {
it('serializes undefined', () => {
const input = { a: undefined };
const output = `{"a":[0]}`;
expect(serializeProps(input)).to.equal(output);
});
it('serializes null', () => {
const input = { a: null };
const output = `{"a":[0,null]}`;
expect(serializeProps(input)).to.equal(output);
});
it('serializes a boolean', () => {
const input = { a: false };
const output = `{"a":[0,false]}`;
expect(serializeProps(input)).to.equal(output);
});
it('serializes a number', () => {
const input = { a: 1 };
const output = `{"a":[0,1]}`;
expect(serializeProps(input)).to.equal(output);
});
it('serializes a string', () => {
const input = { a: 'b' };
const output = `{"a":[0,"b"]}`;
expect(serializeProps(input)).to.equal(output);
});
it('serializes an object', () => {
const input = { a: { b: 'c' } };
const output = `{"a":[0,{"b":[0,"c"]}]}`;
expect(serializeProps(input)).to.equal(output);
});
it('serializes an array', () => {
const input = { a: [0] };
const output = `{"a":[1,"[[0,0]]"]}`;
Expand Down