summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar wackbyte <wackbyte@pm.me> 2023-06-30 17:24:29 -0400
committerGravatar GitHub <noreply@github.com> 2023-06-30 16:24:29 -0500
commit2172dd4f0dd8f87d1adbc5ae90f44724e66eb964 (patch)
treed7e69a8da981d1c2b36e964ca05e6330de5c3e2c
parent4dd8849be2250cc65a3b9264b2c7db2f3300cac7 (diff)
downloadastro-2172dd4f0dd8f87d1adbc5ae90f44724e66eb964.tar.gz
astro-2172dd4f0dd8f87d1adbc5ae90f44724e66eb964.tar.zst
astro-2172dd4f0dd8f87d1adbc5ae90f44724e66eb964.zip
fix: don't serialize `undefined` as `null` (#7531)
* fix: don't serialize `undefined` as `null` * test: include more types in the pass-js test
-rw-r--r--.changeset/moody-singers-develop.md5
-rw-r--r--packages/astro/e2e/fixtures/pass-js/src/components/React.tsx37
-rw-r--r--packages/astro/e2e/fixtures/pass-js/src/pages/index.astro18
-rw-r--r--packages/astro/e2e/pass-js.test.js91
-rw-r--r--packages/astro/src/runtime/server/serialize.ts4
-rw-r--r--packages/astro/test/serialize.test.js27
6 files changed, 145 insertions, 37 deletions
diff --git a/.changeset/moody-singers-develop.md b/.changeset/moody-singers-develop.md
new file mode 100644
index 000000000..4f791a73e
--- /dev/null
+++ b/.changeset/moody-singers-develop.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Fix serialization of `undefined` in framework component props
diff --git a/packages/astro/e2e/fixtures/pass-js/src/components/React.tsx b/packages/astro/e2e/fixtures/pass-js/src/components/React.tsx
index 02023fc9d..15314461c 100644
--- a/packages/astro/e2e/fixtures/pass-js/src/components/React.tsx
+++ b/packages/astro/e2e/fixtures/pass-js/src/components/React.tsx
@@ -1,10 +1,14 @@
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>;
}
@@ -12,7 +16,7 @@ interface Props {
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>
@@ -20,13 +24,22 @@ export default function Component({ obj, num, arr, map, set }: Props) {
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>
diff --git a/packages/astro/e2e/fixtures/pass-js/src/pages/index.astro b/packages/astro/e2e/fixtures/pass-js/src/pages/index.astro
index be40948d8..181f2bfba 100644
--- a/packages/astro/e2e/fixtures/pass-js/src/pages/index.astro
+++ b/packages/astro/e2e/fixtures/pass-js/src/pages/index.astro
@@ -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')
},
@@ -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>
diff --git a/packages/astro/e2e/pass-js.test.js b/packages/astro/e2e/pass-js.test.js
index 9ff6e6047..0db9895d1 100644
--- a/packages/astro/e2e/pass-js.test.js
+++ b/packages/astro/e2e/pass-js.test.js
@@ -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]');
@@ -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');
});
});
diff --git a/packages/astro/src/runtime/server/serialize.ts b/packages/astro/src/runtime/server/serialize.ts
index 140823600..7c0d46dee 100644
--- a/packages/astro/src/runtime/server/serialize.ts
+++ b/packages/astro/src/runtime/server/serialize.ts
@@ -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]': {
@@ -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];
}
diff --git a/packages/astro/test/serialize.test.js b/packages/astro/test/serialize.test.js
index cab016648..f6838be19 100644
--- a/packages/astro/test/serialize.test.js
+++ b/packages/astro/test/serialize.test.js
@@ -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]]"]}`;