Skip to content

Commit

Permalink
Use devalue for island props serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
bluwy committed Sep 30, 2024
1 parent 21b5e80 commit 9816cb8
Show file tree
Hide file tree
Showing 14 changed files with 22 additions and 327 deletions.
26 changes: 0 additions & 26 deletions packages/astro/e2e/error-cyclic.test.js

This file was deleted.

7 changes: 0 additions & 7 deletions packages/astro/e2e/fixtures/error-cyclic/astro.config.mjs

This file was deleted.

10 changes: 0 additions & 10 deletions packages/astro/e2e/fixtures/error-cyclic/package.json

This file was deleted.

This file was deleted.

This file was deleted.

4 changes: 3 additions & 1 deletion packages/astro/e2e/fixtures/pass-js/src/components/React.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ interface Props {
array: any[];
map: Map<string, string>;
set: Set<string>;
infinity: number;
}

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

/** a counter written in React */
export default function Component({ undefined: undefinedProp, null: nullProp, boolean, number, string, bigint, object, array, map, set }: Props) {
export default function Component({ undefined: undefinedProp, null: nullProp, boolean, number, string, bigint, object, array, map, set, infinity }: Props) {
// We are testing hydration, so don't return anything in the server.
if(isNode) {
return <div></div>
Expand Down Expand Up @@ -47,6 +48,7 @@ export default function Component({ undefined: undefinedProp, null: nullProp, bo
</ul>
<span id="set-type">{Object.prototype.toString.call(set)}</span>
<span id="set-value">{Array.from(set).join(',')}</span>
<span id="infinity-value">{infinity.toString()}</span>
</div>
);
}
1 change: 1 addition & 0 deletions packages/astro/e2e/fixtures/pass-js/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ set.add('test2');
array={[0, "foo"]}
map={map}
set={set}
infinity={Infinity}
/>
</main>
</body>
Expand Down
4 changes: 4 additions & 0 deletions packages/astro/e2e/pass-js.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ test.describe('Passing JS into client components', () => {
await expect(numberValue, 'is visible').toBeVisible();
await expect(numberValue).toHaveText('16');

const infinityValue = page.locator('#infinity-value');
await expect(infinityValue, 'is visible').toBeVisible();
await expect(infinityValue).toHaveText('Infinity');

// string
const stringType = page.locator('#string-type');
await expect(stringType, 'is visible').toBeVisible();
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/runtime/client/dev-toolbar/apps/xray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export default {
tooltip.sections = [];

const islandProps = island.getAttribute('props')
? JSON.parse(island.getAttribute('props')!)
? (0, eval)('(' + island.getAttribute('props') + ')')
: {};
const islandClientDirective = island.getAttribute('client');

Expand All @@ -147,7 +147,7 @@ export default {
);
if (islandPropsEntries.length > 0) {
const stringifiedProps = JSON.stringify(
Object.fromEntries(islandPropsEntries.map((prop: any) => [prop[0], prop[1][1]])),
Object.fromEntries(islandPropsEntries),
undefined,
2,
);
Expand Down
37 changes: 3 additions & 34 deletions packages/astro/src/runtime/server/astro-island.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,6 @@ declare const Astro: {
};

{
interface PropTypeSelector {
[k: string]: (value: any) => any;
}

const propTypes: PropTypeSelector = {
0: (value) => reviveObject(value),
1: (value) => reviveArray(value),
2: (value) => new RegExp(value),
3: (value) => new Date(value),
4: (value) => new Map(reviveArray(value)),
5: (value) => new Set(reviveArray(value)),
6: (value) => BigInt(value),
7: (value) => new URL(value),
8: (value) => new Uint8Array(value),
9: (value) => new Uint16Array(value),
10: (value) => new Uint32Array(value),
};

// Not using JSON.parse reviver because it's bottom-up but we want top-down
const reviveTuple = (raw: any): any => {
const [type, value] = raw;
return type in propTypes ? propTypes[type](value) : undefined;
};

const reviveArray = (raw: any): any => (raw as Array<any>).map(reviveTuple);

const reviveObject = (raw: any): any => {
if (typeof raw !== 'object' || raw === null) return raw;
return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, reviveTuple(value)]));
};

// 🌊🏝️🌴
class AstroIsland extends HTMLElement {
public Component: any;
Expand Down Expand Up @@ -167,9 +136,9 @@ declare const Astro: {
let props: Record<string, unknown>;

try {
props = this.hasAttribute('props')
? reviveObject(JSON.parse(this.getAttribute('props')!))
: {};
// eval should be safe as only Astro serializes the props. This uses `(0, eval)` to cause an
// indirect eval, which causes the eval to run in the global scope rather than this current scope
props = this.hasAttribute('props') ? (0, eval)('(' + this.getAttribute('props') + ')') : {};
} catch (e) {
let componentName: string = this.getAttribute('component-url') || '<unknown>';
const componentExport = this.getAttribute('component-export');
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/runtime/server/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export async function generateHydrateScript(
if (renderer.clientEntrypoint) {
island.props['component-export'] = componentExport.value;
island.props['renderer-url'] = await result.resolve(decodeURI(renderer.clientEntrypoint));
island.props['props'] = escapeHTML(serializeProps(props, metadata));
island.props['props'] = escapeHTML(serializeProps(props));
}

island.props['ssr'] = '';
Expand Down
3 changes: 1 addition & 2 deletions packages/astro/src/runtime/server/render/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { clsx } from 'clsx';
import { AstroError, AstroErrorData } from '../../../core/errors/index.js';
import { markHTMLString } from '../escape.js';
import { extractDirectives, generateHydrateScript } from '../hydration.js';
import { serializeProps } from '../serialize.js';
import { shorthash } from '../shorthash.js';
import { isPromise } from '../util.js';
import { type AstroComponentFactory, isAstroComponentFactory } from './astro/factory.js';
Expand All @@ -29,6 +28,7 @@ import { maybeRenderHead } from './head.js';
import { containsServerDirective, renderServerIsland } from './server-islands.js';
import { type ComponentSlots, renderSlotToString, renderSlots } from './slot.js';
import { formatList, internalSpreadAttributes, renderElement, voidElementNames } from './util.js';
import { serializeProps } from '../serialize.js';

const needsHeadRenderingSymbol = Symbol.for('astro.needsHeadRendering');
const rendererAliases = new Map([['solid', 'solid-js']]);
Expand Down Expand Up @@ -331,7 +331,6 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
const astroId = shorthash(
`<!--${metadata.componentExport!.value}:${metadata.componentUrl}-->\n${html}\n${serializeProps(
props,
metadata,
)}`,
);

Expand Down
112 changes: 7 additions & 105 deletions packages/astro/src/runtime/server/serialize.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,10 @@
import type { ValueOf } from '../../type-utils.js';
import type { AstroComponentMetadata } from '../../types/public/internal.js';
import { uneval } from 'devalue';

const PROP_TYPE = {
Value: 0,
JSON: 1, // Actually means Array
RegExp: 2,
Date: 3,
Map: 4,
Set: 5,
BigInt: 6,
URL: 7,
Uint8Array: 8,
Uint16Array: 9,
Uint32Array: 10,
};

function serializeArray(
value: any[],
metadata: AstroComponentMetadata | Record<string, any> = {},
parents = new WeakSet<any>(),
): any[] {
if (parents.has(value)) {
throw new Error(`Cyclic reference detected while serializing props for <${metadata.displayName} client:${metadata.hydrate}>!
Cyclic references cannot be safely serialized for client-side usage. Please remove the cyclic reference.`);
}
parents.add(value);
const serialized = value.map((v) => {
return convertToSerializedForm(v, metadata, parents);
});
parents.delete(value);
return serialized;
}

function serializeObject(
value: Record<any, any>,
metadata: AstroComponentMetadata | Record<string, any> = {},
parents = new WeakSet<any>(),
): Record<any, any> {
if (parents.has(value)) {
throw new Error(`Cyclic reference detected while serializing props for <${metadata.displayName} client:${metadata.hydrate}>!
Cyclic references cannot be safely serialized for client-side usage. Please remove the cyclic reference.`);
export function serializeProps(props: Record<string, unknown>) {
// Remove symbolic keys as devalue can't handle them and they don't really make sense to be
// serialized as symbolic keys aren't really equal between the client and server realms
if (Object.getOwnPropertySymbols(props).length) {
props = Object.fromEntries(Object.entries(props).filter(([key]) => typeof key !== 'symbol'));
}
parents.add(value);
const serialized = Object.fromEntries(
Object.entries(value).map(([k, v]) => {
return [k, convertToSerializedForm(v, metadata, parents)];
}),
);
parents.delete(value);
return serialized;
}

function convertToSerializedForm(
value: any,
metadata: AstroComponentMetadata | Record<string, any> = {},
parents = new WeakSet<any>(),
): [ValueOf<typeof PROP_TYPE>, any] | [ValueOf<typeof PROP_TYPE>] {
const tag = Object.prototype.toString.call(value);
switch (tag) {
case '[object Date]': {
return [PROP_TYPE.Date, (value as Date).toISOString()];
}
case '[object RegExp]': {
return [PROP_TYPE.RegExp, (value as RegExp).source];
}
case '[object Map]': {
return [PROP_TYPE.Map, serializeArray(Array.from(value as Map<any, any>), metadata, parents)];
}
case '[object Set]': {
return [PROP_TYPE.Set, serializeArray(Array.from(value as Set<any>), metadata, parents)];
}
case '[object BigInt]': {
return [PROP_TYPE.BigInt, (value as bigint).toString()];
}
case '[object URL]': {
return [PROP_TYPE.URL, (value as URL).toString()];
}
case '[object Array]': {
return [PROP_TYPE.JSON, serializeArray(value, metadata, parents)];
}
case '[object Uint8Array]': {
return [PROP_TYPE.Uint8Array, Array.from(value as Uint8Array)];
}
case '[object Uint16Array]': {
return [PROP_TYPE.Uint16Array, Array.from(value as Uint16Array)];
}
case '[object Uint32Array]': {
return [PROP_TYPE.Uint32Array, Array.from(value as Uint32Array)];
}
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];
}
}
}
}

export function serializeProps(props: any, metadata: AstroComponentMetadata) {
const serialized = JSON.stringify(serializeObject(props, metadata));
return serialized;
return uneval(props);
}
Loading

0 comments on commit 9816cb8

Please sign in to comment.