diff --git a/.changeset/great-icons-turn.md b/.changeset/great-icons-turn.md new file mode 100644 index 000000000..c3d937f91 --- /dev/null +++ b/.changeset/great-icons-turn.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix quadratic quote escaping in nested data in island props diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index a108044ac..7be630d06 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -18,25 +18,32 @@ declare const Astro: { } const propTypes: PropTypeSelector = { - 0: (value) => value, - 1: (value) => JSON.parse(value, reviver), + 0: (value) => reviveObject(value), + 1: (value) => reviveArray(value), 2: (value) => new RegExp(value), 3: (value) => new Date(value), - 4: (value) => new Map(JSON.parse(value, reviver)), - 5: (value) => new Set(JSON.parse(value, reviver)), + 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(JSON.parse(value)), - 9: (value) => new Uint16Array(JSON.parse(value)), - 10: (value) => new Uint32Array(JSON.parse(value)), + 8: (value) => new Uint8Array(value), + 9: (value) => new Uint16Array(value), + 10: (value) => new Uint32Array(value), }; - const reviver = (propKey: string, raw: string): any => { - if (propKey === '' || !Array.isArray(raw)) return raw; + // 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).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')) { customElements.define( 'astro-island', @@ -132,7 +139,7 @@ declare const Astro: { try { props = this.hasAttribute('props') - ? JSON.parse(this.getAttribute('props')!, reviver) + ? reviveObject(JSON.parse(this.getAttribute('props')!)) : {}; } catch (e) { let componentName: string = this.getAttribute('component-url') || ''; diff --git a/packages/astro/src/runtime/server/serialize.ts b/packages/astro/src/runtime/server/serialize.ts index 7c0d46dee..479552260 100644 --- a/packages/astro/src/runtime/server/serialize.ts +++ b/packages/astro/src/runtime/server/serialize.ts @@ -4,7 +4,7 @@ type ValueOf = T[keyof T]; const PROP_TYPE = { Value: 0, - JSON: 1, + JSON: 1, // Actually means Array RegExp: 2, Date: 3, Map: 4, @@ -68,16 +68,10 @@ function convertToSerializedForm( return [PROP_TYPE.RegExp, (value as RegExp).source]; } case '[object Map]': { - return [ - PROP_TYPE.Map, - JSON.stringify(serializeArray(Array.from(value as Map), metadata, parents)), - ]; + return [PROP_TYPE.Map, serializeArray(Array.from(value as Map), metadata, parents)]; } case '[object Set]': { - return [ - PROP_TYPE.Set, - JSON.stringify(serializeArray(Array.from(value as Set), metadata, parents)), - ]; + return [PROP_TYPE.Set, serializeArray(Array.from(value as Set), metadata, parents)]; } case '[object BigInt]': { return [PROP_TYPE.BigInt, (value as bigint).toString()]; @@ -86,16 +80,16 @@ function convertToSerializedForm( return [PROP_TYPE.URL, (value as URL).toString()]; } case '[object Array]': { - return [PROP_TYPE.JSON, JSON.stringify(serializeArray(value, metadata, parents))]; + return [PROP_TYPE.JSON, serializeArray(value, metadata, parents)]; } 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]': { - return [PROP_TYPE.Uint16Array, JSON.stringify(Array.from(value as Uint16Array))]; + return [PROP_TYPE.Uint16Array, Array.from(value as Uint16Array)]; } case '[object Uint32Array]': { - return [PROP_TYPE.Uint32Array, JSON.stringify(Array.from(value as Uint32Array))]; + return [PROP_TYPE.Uint32Array, Array.from(value as Uint32Array)]; } default: { if (value !== null && typeof value === 'object') { diff --git a/packages/astro/test/serialize.test.js b/packages/astro/test/serialize.test.js index f6838be19..3a370b8af 100644 --- a/packages/astro/test/serialize.test.js +++ b/packages/astro/test/serialize.test.js @@ -34,7 +34,13 @@ describe('serialize', () => { }); it('serializes an array', () => { 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); }); it('serializes a regular expression', () => { @@ -49,12 +55,12 @@ describe('serialize', () => { }); it('serializes a Map', () => { 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); }); it('serializes a Set', () => { 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); }); it('serializes a BigInt', () => { @@ -69,17 +75,17 @@ describe('serialize', () => { }); it('serializes a Uint8Array', () => { 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); }); it('serializes a Uint16Array', () => { 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); }); it('serializes a Uint32Array', () => { 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); }); it('cannot serialize a cyclic reference', () => {