Render async SolidJS components
This commit is contained in:
parent
fca6892f8d
commit
f12ce6c947
18 changed files with 511 additions and 40 deletions
21
.changeset/chilly-badgers-push.md
Normal file
21
.changeset/chilly-badgers-push.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
'@astrojs/solid-js': major
|
||||
---
|
||||
|
||||
Render SolidJS components using [renderToStringAsync](https://www.solidjs.com/docs/latest#rendertostringasync).
|
||||
|
||||
This changes the renderer of SolidJS components from renderToString to renderToStringAsync.
|
||||
|
||||
The server render phase will now wait for Suspense boundaries to resolve instead of immediately rendering the [Suspense](https://www.solidjs.com/docs/latest#suspense) fallback.
|
||||
|
||||
If your SolidJS component uses APIs such as [lazy](https://www.solidjs.com/docs/latest#lazy) or [createResource](https://www.solidjs.com/docs/latest#createresource), these functions may now be called on the server side.
|
||||
|
||||
This increases the power of the SolidJS integration. For example, server-only SolidJS components could now call async functions directly using the `createResource` API, like loading data from another API or using the async Astro Image function `getImage()`. It is unlikely that a server only component would make use of Suspense until now, so this should not be a breaking change for server-only components.
|
||||
|
||||
This could be a breaking change if:
|
||||
|
||||
- the component has a hydrating directive like `client:load`, and
|
||||
- the component uses Suspense APIs like `lazy` or `createResource`, and
|
||||
- the component uses Suspense fallback with the intention for it to be a server-side fallback
|
||||
|
||||
In this case, instead of relying on Suspense as a server-side fallback, use APIs like [isServer](https://www.solidjs.com/docs/latest/api#isserver) or `onMount()` to detect server mode and render the server fallback without using Suspense.
|
|
@ -1,7 +1,7 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { scrollToElement, testFactory, waitForHydrate } from './test-utils.js';
|
||||
|
||||
export function prepareTestFactory(opts) {
|
||||
export function prepareTestFactory(opts, { canReplayClicks = false } = {}) {
|
||||
const test = testFactory(opts);
|
||||
|
||||
let devServer;
|
||||
|
@ -104,7 +104,16 @@ export function prepareTestFactory(opts) {
|
|||
await waitForHydrate(page, counter);
|
||||
|
||||
await inc.click();
|
||||
await expect(count, 'count incremented by 1').toHaveText('1');
|
||||
|
||||
if (canReplayClicks) {
|
||||
// SolidJS has a hydration script that automatically captures
|
||||
// and replays click and input events on Hydration:
|
||||
// https://www.solidjs.com/docs/latest#hydrationscript
|
||||
// so in total there are two click events.
|
||||
await expect(count, 'count incremented by 2').toHaveText('2');
|
||||
} else {
|
||||
await expect(count, 'count incremented by 1').toHaveText('1');
|
||||
}
|
||||
});
|
||||
|
||||
test('client:only', async ({ page, astro }) => {
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { prepareTestFactory } from './shared-component-tests.js';
|
||||
|
||||
const { test, createTests } = prepareTestFactory({ root: './fixtures/solid-component/' });
|
||||
const { test, createTests } = prepareTestFactory(
|
||||
{ root: './fixtures/solid-component/' },
|
||||
{
|
||||
canReplayClicks: true,
|
||||
}
|
||||
);
|
||||
|
||||
const config = {
|
||||
componentFilePath: './src/components/SolidComponent.jsx',
|
||||
|
|
34
packages/astro/test/fixtures/solid-component/src/components/Counter.jsx
vendored
Normal file
34
packages/astro/test/fixtures/solid-component/src/components/Counter.jsx
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
// Based on reproduction from https://github.com/withastro/astro/issues/6912
|
||||
|
||||
import { For, Match, Switch } from 'solid-js';
|
||||
|
||||
export default function Counter(props) {
|
||||
return (
|
||||
<For each={[1, 2, 3, 4]}>
|
||||
{(page) => {
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={page % 2 === 0}>
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log(page);
|
||||
}}
|
||||
>
|
||||
even {page}
|
||||
</button>
|
||||
</Match>
|
||||
<Match when={page % 2 === 1}>
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log(page);
|
||||
}}
|
||||
>
|
||||
odd {page}
|
||||
</button>
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
);
|
||||
}
|
5
packages/astro/test/fixtures/solid-component/src/components/LazyCounter.jsx
vendored
Normal file
5
packages/astro/test/fixtures/solid-component/src/components/LazyCounter.jsx
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
// Based on reproduction from https://github.com/withastro/astro/issues/6912
|
||||
|
||||
import { lazy } from 'solid-js';
|
||||
|
||||
export const LazyCounter = lazy(() => import('./Counter'));
|
55
packages/astro/test/fixtures/solid-component/src/components/async-components.jsx
vendored
Normal file
55
packages/astro/test/fixtures/solid-component/src/components/async-components.jsx
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { createResource, ErrorBoundary } from 'solid-js';
|
||||
|
||||
// It may be good to try short and long sleep times.
|
||||
// But short is faster for testing.
|
||||
const SLEEP_MS = 10;
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
export function AsyncComponent(props) {
|
||||
const [data] = createResource(async () => {
|
||||
// console.log("Start rendering async component " + props.title);
|
||||
await sleep(props.delay ?? SLEEP_MS);
|
||||
// console.log("Finish rendering async component " + props.title);
|
||||
return 'async_result_from_async_component';
|
||||
});
|
||||
|
||||
return (
|
||||
<div data-name="AsyncComponent" onClick={() => actions.refetch()}>
|
||||
{data()}
|
||||
{/* NOTE: The props.children are intentionally commented out
|
||||
to simulate a situation where hydration script might not
|
||||
be injected in the right spot. */}
|
||||
{/* {props.children} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AsyncErrorComponent() {
|
||||
const [data] = createResource(async () => {
|
||||
await sleep(SLEEP_MS);
|
||||
throw new Error('Async error thrown!');
|
||||
});
|
||||
|
||||
return <div>{data()}</div>;
|
||||
}
|
||||
|
||||
export function AsyncErrorInErrorBoundary() {
|
||||
return (
|
||||
<ErrorBoundary fallback={<div>Async error boundary fallback</div>}>
|
||||
<AsyncErrorComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
export function SyncErrorComponent() {
|
||||
throw new Error('Sync error thrown!');
|
||||
}
|
||||
|
||||
export function SyncErrorInErrorBoundary() {
|
||||
return (
|
||||
<ErrorBoundary fallback={<div>Sync error boundary fallback</div>}>
|
||||
<SyncErrorComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
18
packages/astro/test/fixtures/solid-component/src/pages/nested.astro
vendored
Normal file
18
packages/astro/test/fixtures/solid-component/src/pages/nested.astro
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
import { AsyncComponent } from '../components/async-components.jsx';
|
||||
---
|
||||
|
||||
<html>
|
||||
<head><title>Nested Test</title></head>
|
||||
<body>
|
||||
<div>
|
||||
<AsyncComponent client:load title="level-a">
|
||||
<AsyncComponent client:load title="level-a-a" ></AsyncComponent>
|
||||
<AsyncComponent client:load title="level-a-b">
|
||||
<AsyncComponent client:load title="level-a-b-a"></AsyncComponent>
|
||||
</AsyncComponent>
|
||||
<AsyncComponent client:load title="level-a-2" ></AsyncComponent>
|
||||
</AsyncComponent>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
21
packages/astro/test/fixtures/solid-component/src/pages/ssr-client-load-throwing.astro
vendored
Normal file
21
packages/astro/test/fixtures/solid-component/src/pages/ssr-client-load-throwing.astro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
import {
|
||||
AsyncErrorInErrorBoundary,
|
||||
SyncErrorInErrorBoundary,
|
||||
} from '../components/async-components.jsx';
|
||||
---
|
||||
|
||||
<html>
|
||||
<head><title>Solid</title></head>
|
||||
<body>
|
||||
<div>
|
||||
<!--
|
||||
Error boundary in hydrating component may generate scripts script:
|
||||
https://github.com/ryansolid/dom-expressions/blob/6746f048c4adf4d4797276f074dd2d487654796a/packages/dom-expressions/src/server.js#L24
|
||||
So make sure that the hydration script is generated on this page.
|
||||
-->
|
||||
<AsyncErrorInErrorBoundary client:load />
|
||||
<SyncErrorInErrorBoundary client:load />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
17
packages/astro/test/fixtures/solid-component/src/pages/ssr-client-load.astro
vendored
Normal file
17
packages/astro/test/fixtures/solid-component/src/pages/ssr-client-load.astro
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
import { LazyCounter } from '../components/LazyCounter.jsx';
|
||||
import { AsyncComponent } from '../components/async-components.jsx';
|
||||
---
|
||||
|
||||
<html>
|
||||
<head><title>Solid</title></head>
|
||||
<body>
|
||||
<div>
|
||||
<!-- client:load should generate exactly one hydration script per page -->
|
||||
<AsyncComponent client:load />
|
||||
<AsyncComponent client:load />
|
||||
<!-- Lazy copmonents should render consistently, even on first render. -->
|
||||
<LazyCounter client:load />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
24
packages/astro/test/fixtures/solid-component/src/pages/ssr-client-none-throwing.astro
vendored
Normal file
24
packages/astro/test/fixtures/solid-component/src/pages/ssr-client-none-throwing.astro
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import {
|
||||
AsyncErrorInErrorBoundary,
|
||||
SyncErrorInErrorBoundary,
|
||||
// AsyncErrorComponent,
|
||||
// SyncErrorComponent,
|
||||
} from '../components/async-components.jsx';
|
||||
---
|
||||
|
||||
<html>
|
||||
<head><title>Solid</title></head>
|
||||
<body>
|
||||
<div>
|
||||
<!-- Async errors should be caught by ErrorBoundary -->
|
||||
<AsyncErrorInErrorBoundary />
|
||||
<!-- Sync errors should be caught by ErrorBoundary -->
|
||||
<SyncErrorInErrorBoundary />
|
||||
|
||||
<!-- Error not wrapped in ErrorBoundary should bubble up to Astro renderToStaticMarkup() function. -->
|
||||
<!-- <AsyncErrorComponent /> -->
|
||||
<!-- <SyncErrorComponent /> -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
16
packages/astro/test/fixtures/solid-component/src/pages/ssr-client-none.astro
vendored
Normal file
16
packages/astro/test/fixtures/solid-component/src/pages/ssr-client-none.astro
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
import { AsyncComponent } from '../components/async-components.jsx';
|
||||
import { LazyCounter } from '../components/LazyCounter.jsx';
|
||||
---
|
||||
|
||||
<html>
|
||||
<head><title>Solid</title></head>
|
||||
<body>
|
||||
<div>
|
||||
<!-- Static component should not create any hydration scripts -->
|
||||
<AsyncComponent />
|
||||
<!-- Lazy copmonents should render consistently, even on first render. -->
|
||||
<LazyCounter />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
13
packages/astro/test/fixtures/solid-component/src/pages/ssr-client-only.astro
vendored
Normal file
13
packages/astro/test/fixtures/solid-component/src/pages/ssr-client-only.astro
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
import { AsyncComponent } from '../components/async-components.jsx';
|
||||
---
|
||||
|
||||
<html>
|
||||
<head><title>Solid</title></head>
|
||||
<body>
|
||||
<div>
|
||||
<!-- client only component should not generate hydration -->
|
||||
<AsyncComponent client:only />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -26,6 +26,72 @@ describe('Solid component', () => {
|
|||
// test 2: Support rendering proxy components
|
||||
expect($('#proxy-component').text()).to.be.equal('Hello world');
|
||||
});
|
||||
|
||||
// ssr-client-none.astro
|
||||
it('Supports server only components', async () => {
|
||||
const html = await fixture.readFile('ssr-client-none/index.html');
|
||||
const hydrationScriptCount = countHydrationScripts(html);
|
||||
expect(hydrationScriptCount).to.be.equal(0);
|
||||
const hydrationEventsCount = countHydrationEvents(html);
|
||||
expect(hydrationEventsCount).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('Supports lazy server only components', async () => {
|
||||
const html = await fixture.readFile('ssr-client-none/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
expect($('button')).to.have.lengthOf(4);
|
||||
});
|
||||
|
||||
// ssr-client-none-throwing.astro
|
||||
it('Supports server only components with error boundaries', async () => {
|
||||
const html = await fixture.readFile('ssr-client-none-throwing/index.html');
|
||||
const hydrationScriptCount = countHydrationScripts(html);
|
||||
expect(hydrationScriptCount).to.be.equal(0);
|
||||
expect(html).to.include('Async error boundary fallback');
|
||||
expect(html).to.include('Sync error boundary fallback');
|
||||
const hydrationEventsCount = countHydrationEvents(html);
|
||||
expect(hydrationEventsCount).to.be.equal(0);
|
||||
});
|
||||
|
||||
// ssr-client-load.astro
|
||||
it('Supports hydrating components', async () => {
|
||||
const html = await fixture.readFile('ssr-client-load/index.html');
|
||||
const hydrationScriptCount = countHydrationScripts(html);
|
||||
expect(hydrationScriptCount).to.be.equal(1);
|
||||
});
|
||||
|
||||
it('Supports lazy hydrating components', async () => {
|
||||
const html = await fixture.readFile('ssr-client-load/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
expect($('button')).to.have.lengthOf(4);
|
||||
});
|
||||
|
||||
// ssr-client-load-throwing.astro
|
||||
it('Supports hydrating components with error boundaries', async () => {
|
||||
const html = await fixture.readFile('ssr-client-load-throwing/index.html');
|
||||
const hydrationScriptCount = countHydrationScripts(html);
|
||||
expect(hydrationScriptCount).to.be.equal(1);
|
||||
expect(html).to.include('Async error boundary fallback');
|
||||
expect(html).to.include('Sync error boundary fallback');
|
||||
const hydrationEventsCount = countHydrationEvents(html);
|
||||
expect(hydrationEventsCount).to.be.greaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
// ssr-client-only.astro
|
||||
it('Supports client only components', async () => {
|
||||
const html = await fixture.readFile('ssr-client-only/index.html');
|
||||
const hydrationScriptCount = countHydrationScripts(html);
|
||||
expect(hydrationScriptCount).to.be.equal(0);
|
||||
});
|
||||
|
||||
// nested.astro
|
||||
|
||||
it('Injects hydration script before any SolidJS components in the HTML, even if heavily nested', async () => {
|
||||
const html = await fixture.readFile('nested/index.html');
|
||||
const firstHydrationScriptAt = String(html).indexOf('_$HY=');
|
||||
const firstHydrationEventAt = String(html).indexOf('_$HY.set');
|
||||
expect(firstHydrationScriptAt).to.be.lessThan(firstHydrationEventAt);
|
||||
});
|
||||
});
|
||||
|
||||
if (isWindows) return;
|
||||
|
@ -64,3 +130,18 @@ describe('Solid component', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
function countHydrationScripts(/** @type {string} */ html) {
|
||||
// Based on this hydration script
|
||||
// https://github.com/ryansolid/dom-expressions/blob/2ea8a70518710941da6a59b0fe9c2a8d94d1baed/packages/dom-expressions/assets/hydrationScripts.js
|
||||
// we look for the hint "_$HY="
|
||||
|
||||
return html.match(/_\$HY=/g)?.length ?? 0;
|
||||
}
|
||||
|
||||
function countHydrationEvents(/** @type {string} */ html) {
|
||||
// number of times a component was hydrated during rendering
|
||||
// we look for the hint "_$HY.set("
|
||||
|
||||
return html.match(/_\$HY.set\(/g)?.length ?? 0;
|
||||
}
|
||||
|
|
|
@ -96,6 +96,60 @@ export default defineConfig({
|
|||
});
|
||||
```
|
||||
|
||||
## Rendering strategy
|
||||
|
||||
Hydrating SolidJS components are automatically wrapped in Suspense boundaries and rendered on the server using the [renderToStringAsync](https://www.solidjs.com/docs/latest/api#rendertostringasync) function. This means that lazy components, such as:
|
||||
|
||||
```tsx
|
||||
// HelloAstro.tsx
|
||||
export default function HelloAstro() {
|
||||
return <div>Hello Astro</div>;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// LazyHelloAstro.tsx
|
||||
import { lazy } from 'solid-js';
|
||||
export const LazyHelloAstro = lazy(() => import('./HelloAstro'));
|
||||
```
|
||||
|
||||
```astro
|
||||
// hello.astro import {LazyHelloAstro} from "./LazyHelloAstro.tsx"
|
||||
<LazyHelloAstro />
|
||||
```
|
||||
|
||||
Will be rendered into the server's HTML output as
|
||||
|
||||
```html
|
||||
<div>Hello Astro</div>
|
||||
```
|
||||
|
||||
Resources will also be resolved. For example:
|
||||
|
||||
```tsx
|
||||
// CharacterName.tsx
|
||||
function CharacterName() {
|
||||
const [name] = createResource(() =>
|
||||
fetch('https://swapi.dev/api/people/1')
|
||||
.then((result) => result.json())
|
||||
.then((data) => data.name)
|
||||
);
|
||||
|
||||
return <div>Name: {name()}</div>;
|
||||
}
|
||||
|
||||
// character.astro
|
||||
<CharacterName />;
|
||||
```
|
||||
|
||||
Will cause the server to fetch the resource and generate the following HTML output:
|
||||
|
||||
```html
|
||||
<div>Name: Luke Skywalker</div>
|
||||
```
|
||||
|
||||
Non-hydrating [`client:only` components](https://docs.astro.build/en/reference/directives-reference/#clientonly) are not automatically wrapped in a Suspense boundaries. You mnay wrap these components in Suspense boundaries yourself if this behavior is desired.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For help, check out the `#support` channel on [Discord](https://astro.build/chat). Our friendly Support Squad members are here to help!
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
"solid-js": "^1.7.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.4.3"
|
||||
"solid-js": "^1.7.11"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.14.1"
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import { Suspense } from 'solid-js';
|
||||
import { createComponent, hydrate, render } from 'solid-js/web';
|
||||
|
||||
export default (element: HTMLElement) =>
|
||||
(Component: any, props: any, slotted: any, { client }: { client: string }) => {
|
||||
// Prepare global object expected by Solid's hydration logic
|
||||
if (!(window as any)._$HY) {
|
||||
(window as any)._$HY = { events: [], completed: new WeakSet(), r: {} };
|
||||
}
|
||||
if (!element.hasAttribute('ssr')) return;
|
||||
|
||||
const boostrap = client === 'only' ? render : hydrate;
|
||||
const isHydrate = client !== 'only';
|
||||
const bootstrap = isHydrate ? hydrate : render;
|
||||
|
||||
let slot: HTMLElement | null;
|
||||
let _slots: Record<string, any> = {};
|
||||
|
@ -35,13 +33,25 @@ export default (element: HTMLElement) =>
|
|||
const { default: children, ...slots } = _slots;
|
||||
const renderId = element.dataset.solidRenderId;
|
||||
|
||||
const dispose = boostrap(
|
||||
() =>
|
||||
createComponent(Component, {
|
||||
...props,
|
||||
...slots,
|
||||
children,
|
||||
}),
|
||||
const dispose = bootstrap(
|
||||
() => {
|
||||
const inner = () =>
|
||||
createComponent(Component, {
|
||||
...props,
|
||||
...slots,
|
||||
children,
|
||||
});
|
||||
|
||||
if (isHydrate) {
|
||||
return createComponent(Suspense, {
|
||||
get children() {
|
||||
return inner();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
return inner();
|
||||
}
|
||||
},
|
||||
element,
|
||||
{
|
||||
renderId,
|
||||
|
|
|
@ -3,6 +3,7 @@ import type { RendererContext } from './types.js';
|
|||
type Context = {
|
||||
id: string;
|
||||
c: number;
|
||||
hasHydrationScript: boolean;
|
||||
};
|
||||
|
||||
const contexts = new WeakMap<RendererContext['result'], Context>();
|
||||
|
@ -11,11 +12,12 @@ export function getContext(result: RendererContext['result']): Context {
|
|||
if (contexts.has(result)) {
|
||||
return contexts.get(result)!;
|
||||
}
|
||||
let ctx = {
|
||||
let ctx: Context = {
|
||||
c: 0,
|
||||
get id() {
|
||||
return 's' + this.c.toString();
|
||||
},
|
||||
hasHydrationScript: false,
|
||||
};
|
||||
contexts.set(result, ctx);
|
||||
return ctx;
|
||||
|
|
|
@ -1,16 +1,38 @@
|
|||
import { createComponent, renderToString, ssr } from 'solid-js/web';
|
||||
import {
|
||||
createComponent,
|
||||
generateHydrationScript,
|
||||
NoHydration,
|
||||
renderToString,
|
||||
renderToStringAsync,
|
||||
ssr,
|
||||
Suspense,
|
||||
} from 'solid-js/web';
|
||||
import { getContext, incrementId } from './context.js';
|
||||
import type { RendererContext } from './types.js';
|
||||
|
||||
const slotName = (str: string) => str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
|
||||
|
||||
function check(this: RendererContext, Component: any, props: Record<string, any>, children: any) {
|
||||
type RenderStrategy = 'sync' | 'async';
|
||||
|
||||
async function check(
|
||||
this: RendererContext,
|
||||
Component: any,
|
||||
props: Record<string, any>,
|
||||
children: any
|
||||
) {
|
||||
if (typeof Component !== 'function') return false;
|
||||
const { html } = renderToStaticMarkup.call(this, Component, props, children);
|
||||
const { html } = await renderToStaticMarkup.call(this, Component, props, children, {
|
||||
// The check() function appears to just be checking if it is
|
||||
// a valid Solid component. This should be lightweight so prefer
|
||||
// sync render strategy, which should simplify render Suspense fallbacks
|
||||
// not try to load any resources.
|
||||
renderStrategy: 'sync' as RenderStrategy,
|
||||
});
|
||||
return typeof html === 'string';
|
||||
}
|
||||
|
||||
function renderToStaticMarkup(
|
||||
// AsyncRendererComponentFn
|
||||
async function renderToStaticMarkup(
|
||||
this: RendererContext,
|
||||
Component: any,
|
||||
props: Record<string, any>,
|
||||
|
@ -21,27 +43,91 @@ function renderToStaticMarkup(
|
|||
const needsHydrate = metadata?.astroStaticSlot ? !!metadata.hydrate : true;
|
||||
const tagName = needsHydrate ? 'astro-slot' : 'astro-static-slot';
|
||||
|
||||
const html = renderToString(
|
||||
() => {
|
||||
const slots: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(slotted)) {
|
||||
const name = slotName(key);
|
||||
slots[name] = ssr(`<${tagName} name="${name}">${value}</${tagName}>`);
|
||||
}
|
||||
// Note: create newProps to avoid mutating `props` before they are serialized
|
||||
const newProps = {
|
||||
...props,
|
||||
...slots,
|
||||
// In Solid SSR mode, `ssr` creates the expected structure for `children`.
|
||||
children: children != null ? ssr(`<${tagName}>${children}</${tagName}>`) : children,
|
||||
};
|
||||
const ctx = getContext(this.result);
|
||||
|
||||
return createComponent(Component, newProps);
|
||||
},
|
||||
{
|
||||
renderId,
|
||||
const renderStrategy = (metadata?.renderStrategy ?? 'async') as RenderStrategy;
|
||||
|
||||
const renderFn = () => {
|
||||
const slots: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(slotted)) {
|
||||
const name = slotName(key);
|
||||
slots[name] = ssr(`<${tagName} name="${name}">${value}</${tagName}>`);
|
||||
}
|
||||
);
|
||||
// Note: create newProps to avoid mutating `props` before they are serialized
|
||||
const newProps = {
|
||||
...props,
|
||||
...slots,
|
||||
// In Solid SSR mode, `ssr` creates the expected structure for `children`.
|
||||
children: children != null ? ssr(`<${tagName}>${children}</${tagName}>`) : children,
|
||||
};
|
||||
|
||||
if (renderStrategy === 'sync') {
|
||||
// Sync Render:
|
||||
// <Component />
|
||||
// This render mode is not exposed directly to the consumer, only
|
||||
// used in the check() function.
|
||||
return createComponent(Component, newProps);
|
||||
} else {
|
||||
if (needsHydrate) {
|
||||
// Hydrate + Async Render:
|
||||
// <Suspense>
|
||||
// <Component />
|
||||
// </Suspense>
|
||||
return createComponent(Suspense, {
|
||||
get children() {
|
||||
return createComponent(Component, newProps);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Static + Async Render
|
||||
// <NoHydration>
|
||||
// <Suspense>
|
||||
// <Component />
|
||||
// </Suspense>
|
||||
// </NoHydration>
|
||||
return createComponent(NoHydration, {
|
||||
get children() {
|
||||
return createComponent(Suspense, {
|
||||
get children() {
|
||||
return createComponent(Component, newProps);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let html = '';
|
||||
|
||||
if (needsHydrate && renderStrategy === 'async') {
|
||||
if (!ctx.hasHydrationScript) {
|
||||
// The hydration script needs to come before to the first hydrating component of the page.
|
||||
// One way to this would be to prepend the rendered output, eg:
|
||||
//
|
||||
// html += generateHydrationScript();
|
||||
//
|
||||
// However, in certain situations, nested components may be rendered depth-first, causing SolidJS
|
||||
// to put the hydration script in the wrong spot.
|
||||
//
|
||||
// Therefore we render the hydration script to the extraHead so it can work anytime.
|
||||
|
||||
// NOTE: It seems that components on a page may be rendered in parallel.
|
||||
// To avoid a race condition, this code block is intentionally written
|
||||
// *before* the first `await` in the function, so the hydration script will
|
||||
// be prefixed to the first hydratable component on the page, regardless of
|
||||
// the order in which the components finish rendering.
|
||||
|
||||
this.result._metadata.extraHead.push(generateHydrationScript());
|
||||
ctx.hasHydrationScript = true;
|
||||
}
|
||||
}
|
||||
|
||||
html +=
|
||||
renderStrategy === 'async'
|
||||
? await renderToStringAsync(renderFn, { renderId })
|
||||
: renderToString(renderFn, { renderId });
|
||||
|
||||
return {
|
||||
attrs: {
|
||||
'data-solid-render-id': renderId,
|
||||
|
|
Loading…
Reference in a new issue