diff --git a/.changeset/dry-pandas-flash.md b/.changeset/dry-pandas-flash.md new file mode 100644 index 0000000000..fb18de65db --- /dev/null +++ b/.changeset/dry-pandas-flash.md @@ -0,0 +1,5 @@ +--- +'create-astro': patch +--- + +Add support for more Starlight templates diff --git a/.changeset/funny-glasses-bathe.md b/.changeset/funny-glasses-bathe.md new file mode 100644 index 0000000000..28db2f746c --- /dev/null +++ b/.changeset/funny-glasses-bathe.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixed `EndpointOutput` types with `{ encoding: 'binary' }` diff --git a/.changeset/great-icons-turn.md b/.changeset/great-icons-turn.md new file mode 100644 index 0000000000..c3d937f91b --- /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/@types/astro.ts b/packages/astro/src/@types/astro.ts index 4491dc48cc..f7b07dbfec 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1870,10 +1870,15 @@ export interface APIContext = Record; + } + | { + body: Uint8Array; + encoding: 'binary'; + }; export type APIRoute = Record> = ( context: APIContext diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 4496f00980..43f7a3926f 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -194,7 +194,6 @@ export class App { } return response.response; } else { - const body = response.body; const headers = new Headers(); const mimeType = mime.getType(url.pathname); if (mimeType) { @@ -202,7 +201,8 @@ export class App { } else { headers.set('Content-Type', 'text/plain;charset=utf-8'); } - const bytes = this.#encoder.encode(body); + const bytes = + response.encoding !== 'binary' ? this.#encoder.encode(response.body) : response.body; headers.set('Content-Length', bytes.byteLength.toString()); const newResponse = new Response(bytes, { diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index b35895197d..97b26e3800 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -18,12 +18,10 @@ const clientAddressSymbol = Symbol.for('astro.clientAddress'); const clientLocalsSymbol = Symbol.for('astro.locals'); export type EndpointCallResult = - | { + | (EndpointOutput & { type: 'simple'; - body: string; - encoding?: BufferEncoding; cookies: AstroCookies; - } + }) | { type: 'response'; response: Response; @@ -153,9 +151,8 @@ export async function callEndpoint } return { + ...response, type: 'simple', - body: response.body, - encoding: response.encoding, cookies: context.cookies, }; } diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index a108044acf..7be630d068 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 7c0d46deee..4795522605 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/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index ff926bca2e..f58d248a35 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -246,12 +246,15 @@ export async function handleRoute({ if (computedMimeType) { contentType = computedMimeType; } - const response = new Response(Buffer.from(result.body, result.encoding), { - status: 200, - headers: { - 'Content-Type': `${contentType};charset=utf-8`, - }, - }); + const response = new Response( + result.encoding !== 'binary' ? Buffer.from(result.body, result.encoding) : result.body, + { + status: 200, + headers: { + 'Content-Type': `${contentType};charset=utf-8`, + }, + } + ); attachToResponse(response, result.cookies); await writeWebResponse(incomingResponse, response); } diff --git a/packages/astro/test/serialize.test.js b/packages/astro/test/serialize.test.js index f6838be19c..3a370b8af3 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', () => { diff --git a/packages/create-astro/src/actions/template.ts b/packages/create-astro/src/actions/template.ts index f762b264f7..ca041642bf 100644 --- a/packages/create-astro/src/actions/template.ts +++ b/packages/create-astro/src/actions/template.ts @@ -67,9 +67,12 @@ const FILES_TO_UPDATE = { }; function getTemplateTarget(tmpl: string, ref = 'latest') { + if (tmpl.startsWith('starlight')) { + const [, starter = 'basics'] = tmpl.split('/'); + return `withastro/starlight/examples/${starter}`; + } const isThirdParty = tmpl.includes('/'); if (isThirdParty) return tmpl; - if (tmpl === 'starlight') return `withastro/starlight/examples/basics`; return `github:withastro/astro/examples/${tmpl}#${ref}`; } diff --git a/packages/integrations/markdoc/README.md b/packages/integrations/markdoc/README.md index 4b9772bc7e..780b8de9a6 100644 --- a/packages/integrations/markdoc/README.md +++ b/packages/integrations/markdoc/README.md @@ -278,7 +278,7 @@ export default defineMarkdocConfig({ ### Use client-side UI components -Tags and nodes are restricted to `.astro` files. To embed client-side UI components in Markdoc, [use a wrapper `.astro` component that renders a framework component](/en/core-concepts/framework-components/#nesting-framework-components) with your desired `client:` directive. +Tags and nodes are restricted to `.astro` files. To embed client-side UI components in Markdoc, [use a wrapper `.astro` component that renders a framework component](https://docs.astro.build/en/core-concepts/framework-components/#nesting-framework-components) with your desired `client:` directive. This example wraps a React `Aside.tsx` component with a `ClientAside.astro` component: