diff --git a/.changeset/astro-island-route-container-options.md b/.changeset/astro-island-route-container-options.md new file mode 100644 index 0000000000..ac31edbd50 --- /dev/null +++ b/.changeset/astro-island-route-container-options.md @@ -0,0 +1,5 @@ +--- +'@tinacms/astro': minor +--- + +`experimental_createIslandRoute` now accepts an optional `containerOptions` argument that is forwarded to `AstroContainer.create`. This lets islands that use a UI framework (React, Vue, Svelte, ...) be rendered in visual editing — the container is created empty and, unlike the page pipeline, does not inherit renderers from the Astro config, so framework islands previously failed to render. Pass `{ containerOptions: { renderers: [...] } }` to register them. Existing callers are unaffected (the argument is optional). diff --git a/packages/@tinacms/astro/src/__tests__/island-route.test.ts b/packages/@tinacms/astro/src/__tests__/island-route.test.ts index f52af27731..5aac0b816e 100644 --- a/packages/@tinacms/astro/src/__tests__/island-route.test.ts +++ b/packages/@tinacms/astro/src/__tests__/island-route.test.ts @@ -1,6 +1,7 @@ import { PREVIEW_CONTENT_TYPE, PRIME_HEADER } from '@tinacms/bridge/preview'; import type { APIContext } from 'astro'; -import { describe, expect, it } from 'vitest'; +import { experimental_AstroContainer as AstroContainer } from 'astro/container'; +import { describe, expect, it, vi } from 'vitest'; import { requestWithMetadata } from '../data'; import { experimental_createIslandRoute } from '../island-route'; import IslandStub from './IslandStub.astro'; @@ -93,6 +94,46 @@ describe('experimental_createIslandRoute', () => { expect(globalHtml).not.toContain('data-tina-primary'); }); + it('forwards containerOptions to AstroContainer.create', async () => { + const spy = vi.spyOn(AstroContainer, 'create'); + // Renderers is the motivating use: framework islands (React, etc.) need + // renderers registered on the container, which is otherwise created empty. + const containerOptions = { renderers: [] }; + const routeWithOptions = experimental_createIslandRoute( + { + post: { + fetch: () => + requestWithMetadata( + Promise.resolve({ + data: { post: { title: 'Hi' } }, + query: 'query Post', + variables: { relativePath: 'hello.md' }, + }) + ), + component: IslandStub, + wrapper: { tag: 'article' }, + propsFromData: (data) => ({ value: data }), + }, + }, + { containerOptions } + ); + + const url = new URL('http://localhost/tina-island/post'); + const res = (await routeWithOptions({ + params: { name: 'post' }, + request: new Request(url, { + method: 'POST', + headers: { 'content-type': PREVIEW_CONTENT_TYPE }, + body: '{}', + }), + url, + } as unknown as APIContext)) as Response; + + expect(res.status).toBe(200); + expect(spy).toHaveBeenCalledWith(containerOptions); + spy.mockRestore(); + }); + it('rejects non-preview requests', async () => { const url = new URL('http://localhost/tina-island/post'); const res = (await route({ diff --git a/packages/@tinacms/astro/src/experimental.ts b/packages/@tinacms/astro/src/experimental.ts index 2a43b13b11..c9b191ff1c 100644 --- a/packages/@tinacms/astro/src/experimental.ts +++ b/packages/@tinacms/astro/src/experimental.ts @@ -11,4 +11,5 @@ export { experimental_createIslandRoute, type IslandConfig, type IslandRegistry, + type IslandRouteOptions, } from './island-route'; diff --git a/packages/@tinacms/astro/src/island-route.ts b/packages/@tinacms/astro/src/island-route.ts index 102e094904..eb17075009 100644 --- a/packages/@tinacms/astro/src/island-route.ts +++ b/packages/@tinacms/astro/src/island-route.ts @@ -36,8 +36,25 @@ export interface IslandConfig { export type IslandRegistry = Record; +export interface IslandRouteOptions { + /** + * Options forwarded to `AstroContainer.create`. Pass `renderers` here so + * islands that use a UI framework (React, Vue, Svelte, ...) can be rendered — + * the container is created empty and, unlike the page pipeline, does not + * inherit renderers from the Astro config. + * + * @example + * import reactRenderer from '@astrojs/react/server.js'; + * experimental_createIslandRoute(islands, { + * containerOptions: { renderers: [{ name: '@astrojs/react', ssr: reactRenderer }] }, + * }); + */ + containerOptions?: Parameters[0]; +} + export function experimental_createIslandRoute( - islands: IslandRegistry + islands: IslandRegistry, + options?: IslandRouteOptions ): APIRoute { return async ({ params, request, url }) => { const rejection = rejectIfUnsafe(request); @@ -55,7 +72,7 @@ export function experimental_createIslandRoute( const html = await requestStore.run(request, () => formsStore.run(forms, async () => { const data = await island.fetch(request, url.searchParams); - const container = await AstroContainer.create(); + const container = await AstroContainer.create(options?.containerOptions); return container.renderToString(island.component, { props: island.propsFromData(data, url.searchParams), });