Remove nested JSON.stringify from props serialization (#7995)

This commit is contained in:
Jany Belluz 2023-08-09 13:06:21 +01:00 committed by GitHub
parent 895afd4447
commit 79376f842d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 41 additions and 29 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fix quadratic quote escaping in nested data in island props

View file

@ -18,25 +18,32 @@ declare const Astro: {
} }
const propTypes: PropTypeSelector = { const propTypes: PropTypeSelector = {
0: (value) => value, 0: (value) => reviveObject(value),
1: (value) => JSON.parse(value, reviver), 1: (value) => reviveArray(value),
2: (value) => new RegExp(value), 2: (value) => new RegExp(value),
3: (value) => new Date(value), 3: (value) => new Date(value),
4: (value) => new Map(JSON.parse(value, reviver)), 4: (value) => new Map(reviveArray(value)),
5: (value) => new Set(JSON.parse(value, reviver)), 5: (value) => new Set(reviveArray(value)),
6: (value) => BigInt(value), 6: (value) => BigInt(value),
7: (value) => new URL(value), 7: (value) => new URL(value),
8: (value) => new Uint8Array(JSON.parse(value)), 8: (value) => new Uint8Array(value),
9: (value) => new Uint16Array(JSON.parse(value)), 9: (value) => new Uint16Array(value),
10: (value) => new Uint32Array(JSON.parse(value)), 10: (value) => new Uint32Array(value),
}; };
const reviver = (propKey: string, raw: string): any => { // Not using JSON.parse reviver because it's bottom-up but we want top-down
if (propKey === '' || !Array.isArray(raw)) return raw; const reviveTuple = (raw: any): any => {
const [type, value] = raw; const [type, value] = raw;
return type in propTypes ? propTypes[type](value) : undefined; 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)]));
};
if (!customElements.get('astro-island')) { if (!customElements.get('astro-island')) {
customElements.define( customElements.define(
'astro-island', 'astro-island',
@ -132,7 +139,7 @@ declare const Astro: {
try { try {
props = this.hasAttribute('props') props = this.hasAttribute('props')
? JSON.parse(this.getAttribute('props')!, reviver) ? reviveObject(JSON.parse(this.getAttribute('props')!))
: {}; : {};
} catch (e) { } catch (e) {
let componentName: string = this.getAttribute('component-url') || '<unknown>'; let componentName: string = this.getAttribute('component-url') || '<unknown>';

View file

@ -4,7 +4,7 @@ type ValueOf<T> = T[keyof T];
const PROP_TYPE = { const PROP_TYPE = {
Value: 0, Value: 0,
JSON: 1, JSON: 1, // Actually means Array
RegExp: 2, RegExp: 2,
Date: 3, Date: 3,
Map: 4, Map: 4,
@ -68,16 +68,10 @@ function convertToSerializedForm(
return [PROP_TYPE.RegExp, (value as RegExp).source]; return [PROP_TYPE.RegExp, (value as RegExp).source];
} }
case '[object Map]': { case '[object Map]': {
return [ return [PROP_TYPE.Map, serializeArray(Array.from(value as Map<any, any>), metadata, parents)];
PROP_TYPE.Map,
JSON.stringify(serializeArray(Array.from(value as Map<any, any>), metadata, parents)),
];
} }
case '[object Set]': { case '[object Set]': {
return [ return [PROP_TYPE.Set, serializeArray(Array.from(value as Set<any>), metadata, parents)];
PROP_TYPE.Set,
JSON.stringify(serializeArray(Array.from(value as Set<any>), metadata, parents)),
];
} }
case '[object BigInt]': { case '[object BigInt]': {
return [PROP_TYPE.BigInt, (value as bigint).toString()]; return [PROP_TYPE.BigInt, (value as bigint).toString()];
@ -86,16 +80,16 @@ function convertToSerializedForm(
return [PROP_TYPE.URL, (value as URL).toString()]; return [PROP_TYPE.URL, (value as URL).toString()];
} }
case '[object Array]': { case '[object Array]': {
return [PROP_TYPE.JSON, JSON.stringify(serializeArray(value, metadata, parents))]; return [PROP_TYPE.JSON, serializeArray(value, metadata, parents)];
} }
case '[object Uint8Array]': { case '[object Uint8Array]': {
return [PROP_TYPE.Uint8Array, JSON.stringify(Array.from(value as Uint8Array))]; return [PROP_TYPE.Uint8Array, Array.from(value as Uint8Array)];
} }
case '[object Uint16Array]': { case '[object Uint16Array]': {
return [PROP_TYPE.Uint16Array, JSON.stringify(Array.from(value as Uint16Array))]; return [PROP_TYPE.Uint16Array, Array.from(value as Uint16Array)];
} }
case '[object Uint32Array]': { case '[object Uint32Array]': {
return [PROP_TYPE.Uint32Array, JSON.stringify(Array.from(value as Uint32Array))]; return [PROP_TYPE.Uint32Array, Array.from(value as Uint32Array)];
} }
default: { default: {
if (value !== null && typeof value === 'object') { if (value !== null && typeof value === 'object') {

View file

@ -34,7 +34,13 @@ describe('serialize', () => {
}); });
it('serializes an array', () => { it('serializes an array', () => {
const input = { a: [0] }; const input = { a: [0] };
const output = `{"a":[1,"[[0,0]]"]}`; const output = `{"a":[1,[[0,0]]]}`;
expect(serializeProps(input)).to.equal(output);
});
it('can serialize deeply nested data without quadratic quote escaping', () => {
const input = { a: [{ b: [{ c: [{ d: [{ e: [{ f: [{ g: ['leaf'] }] }] }] }] }] }] };
const output =
'{"a":[1,[[0,{"b":[1,[[0,{"c":[1,[[0,{"d":[1,[[0,{"e":[1,[[0,{"f":[1,[[0,{"g":[1,[[0,"leaf"]]]}]]]}]]]}]]]}]]]}]]]}]]]}';
expect(serializeProps(input)).to.equal(output); expect(serializeProps(input)).to.equal(output);
}); });
it('serializes a regular expression', () => { it('serializes a regular expression', () => {
@ -49,12 +55,12 @@ describe('serialize', () => {
}); });
it('serializes a Map', () => { it('serializes a Map', () => {
const input = { a: new Map([[0, 1]]) }; const input = { a: new Map([[0, 1]]) };
const output = `{"a":[4,"[[1,\\"[[0,0],[0,1]]\\"]]"]}`; const output = `{"a":[4,[[1,[[0,0],[0,1]]]]]}`;
expect(serializeProps(input)).to.equal(output); expect(serializeProps(input)).to.equal(output);
}); });
it('serializes a Set', () => { it('serializes a Set', () => {
const input = { a: new Set([0, 1, 2, 3]) }; const input = { a: new Set([0, 1, 2, 3]) };
const output = `{"a":[5,"[[0,0],[0,1],[0,2],[0,3]]"]}`; const output = `{"a":[5,[[0,0],[0,1],[0,2],[0,3]]]}`;
expect(serializeProps(input)).to.equal(output); expect(serializeProps(input)).to.equal(output);
}); });
it('serializes a BigInt', () => { it('serializes a BigInt', () => {
@ -69,17 +75,17 @@ describe('serialize', () => {
}); });
it('serializes a Uint8Array', () => { it('serializes a Uint8Array', () => {
const input = { a: new Uint8Array([1, 2, 3]) }; const input = { a: new Uint8Array([1, 2, 3]) };
const output = `{"a":[8,"[1,2,3]"]}`; const output = `{"a":[8,[1,2,3]]}`;
expect(serializeProps(input)).to.equal(output); expect(serializeProps(input)).to.equal(output);
}); });
it('serializes a Uint16Array', () => { it('serializes a Uint16Array', () => {
const input = { a: new Uint16Array([1, 2, 3]) }; const input = { a: new Uint16Array([1, 2, 3]) };
const output = `{"a":[9,"[1,2,3]"]}`; const output = `{"a":[9,[1,2,3]]}`;
expect(serializeProps(input)).to.equal(output); expect(serializeProps(input)).to.equal(output);
}); });
it('serializes a Uint32Array', () => { it('serializes a Uint32Array', () => {
const input = { a: new Uint32Array([1, 2, 3]) }; const input = { a: new Uint32Array([1, 2, 3]) };
const output = `{"a":[10,"[1,2,3]"]}`; const output = `{"a":[10,[1,2,3]]}`;
expect(serializeProps(input)).to.equal(output); expect(serializeProps(input)).to.equal(output);
}); });
it('cannot serialize a cyclic reference', () => { it('cannot serialize a cyclic reference', () => {