Skip to content

Commit

Permalink
Fix solid recursion bug (#4215)
Browse files Browse the repository at this point in the history
* Fix solid recursion bug

* Fix types

* Remove debug code

* Remove logging from e2e test
  • Loading branch information
matthewp authored Aug 10, 2022
1 parent 58941e9 commit 0af5aa7
Show file tree
Hide file tree
Showing 17 changed files with 227 additions and 43 deletions.
7 changes: 7 additions & 0 deletions packages/astro/e2e/fixtures/solid-recurse/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'astro/config';
import solid from '@astrojs/solid-js';

// https://astro.build/config
export default defineConfig({
integrations: [solid()],
});
12 changes: 12 additions & 0 deletions packages/astro/e2e/fixtures/solid-recurse/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@e2e/solid-recurse",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/solid-js": "workspace:*",
"astro": "workspace:*"
},
"devDependencies": {
"solid-js": "^1.4.3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createSignal } from 'solid-js';

export default function Counter(props) {
const [count, setCount] = createSignal(0);
const type = props.type;

return (
<button
id={props.id + '-' + type}
data-type={type}
ref={(el) =>
console.log(
` ${type} ${type == el.dataset.type ? '==' : '!='} ${el.dataset.type}`
)
}
onClick={() => {
console.log('click');
setCount((p) => ++p);
}}
>
{type}: {count()}
</button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Counter from './Counter';

export default function WrapperA(props) {
return (
// removing the div avoids the error
<div data-wrapper-a-root>
<Counter id={props.id} type="A"></Counter>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Counter from './Counter';

export default function WrapperB(props) {
return (
<div id={props.id}>
{/* Reversing the order of these avoids the error: */}
<div data-wrapper-children>{props.children}</div>
<Counter id={props.id} type="B"></Counter>
</div>
);
}
20 changes: 20 additions & 0 deletions packages/astro/e2e/fixtures/solid-recurse/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
import WrapperB from "../components/WrapperB.jsx";
import WrapperA from "../components/WrapperA.jsx";
---

<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
</head>
<body>
<main>
Case 1
<WrapperB id="case1" client:load>
<WrapperA id="case1" />
</WrapperB>
<br/>
</main>
</body>
</html>
29 changes: 29 additions & 0 deletions packages/astro/e2e/solid-recurse.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect } from '@playwright/test';
import { testFactory } from './test-utils.js';

const test = testFactory({ root: './fixtures/solid-recurse/' });

let devServer;

test.beforeEach(async ({ astro }) => {
devServer = await astro.startDevServer();
});

test.afterEach(async () => {
await devServer.stop();
});

test.describe('Recursive elements with Solid', () => {
test('Counter', async ({ astro, page }) => {
await page.goto(astro.resolveUrl('/'));

const wrapper = page.locator('#case1');
await expect(wrapper, 'component is visible').toBeVisible();

const increment = page.locator('#case1-B');
await expect(increment, 'initial count is 0').toHaveText('B: 0');

await increment.click();
await expect(increment, 'count is incremented').toHaveText('B: 1');
});
});
1 change: 1 addition & 0 deletions packages/astro/src/@types/astro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,7 @@ export interface SSRLoadedRenderer extends AstroRenderer {
check: AsyncRendererComponentFn<boolean>;
renderToStaticMarkup: AsyncRendererComponentFn<{
html: string;
attrs?: Record<string, string>;
}>;
};
}
Expand Down
10 changes: 9 additions & 1 deletion packages/astro/src/runtime/server/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,15 @@ interface HydrateScriptOptions {
result: SSRResult;
astroId: string;
props: Record<string | number, any>;
attrs: Record<string, string> | undefined;
}

/** For hydrated components, generate a <script type="module"> to load the component */
export async function generateHydrateScript(
scriptOptions: HydrateScriptOptions,
metadata: Required<AstroComponentMetadata>
): Promise<SSRElement> {
const { renderer, result, astroId, props } = scriptOptions;
const { renderer, result, astroId, props, attrs } = scriptOptions;
const { hydrate, componentUrl, componentExport } = metadata;

if (!componentExport) {
Expand All @@ -131,6 +132,13 @@ export async function generateHydrateScript(
},
};

// Attach renderer-provided attributes
if(attrs) {
for(const [key, value] of Object.entries(attrs)) {
island.props[key] = value;
}
}

// Add component url
island.props['component-url'] = await result.resolve(componentUrl);

Expand Down
7 changes: 4 additions & 3 deletions packages/astro/src/runtime/server/render/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export async function renderComponent(

const { hydration, isPage, props } = extractDirectives(_props);
let html = '';
let attrs: Record<string, string> | undefined = undefined;

if (hydration) {
metadata.hydrate = hydration.directive as AstroComponentMetadata['hydrate'];
Expand Down Expand Up @@ -222,7 +223,7 @@ Did you mean to enable ${formatList(probableRendererNames.map((r) => '`' + r + '
// We already know that renderer.ssr.check() has failed
// but this will throw a much more descriptive error!
renderer = matchingRenderers[0];
({ html } = await renderer.ssr.renderToStaticMarkup.call(
({ html, attrs } = await renderer.ssr.renderToStaticMarkup.call(
{ result },
Component,
props,
Expand All @@ -247,7 +248,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
if (metadata.hydrate === 'only') {
html = await renderSlot(result, slots?.fallback);
} else {
({ html } = await renderer.ssr.renderToStaticMarkup.call(
({ html, attrs } = await renderer.ssr.renderToStaticMarkup.call(
{ result },
Component,
props,
Expand Down Expand Up @@ -302,7 +303,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
);

const island = await generateHydrateScript(
{ renderer: renderer!, result, astroId, props },
{ renderer: renderer!, result, astroId, props, attrs },
metadata as Required<AstroComponentMetadata>
);

Expand Down
4 changes: 2 additions & 2 deletions packages/integrations/solid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
"exports": {
".": "./dist/index.js",
"./*": "./*",
"./client.js": "./client.js",
"./server.js": "./server.js",
"./client.js": "./dist/client.js",
"./server.js": "./dist/server.js",
"./package.json": "./package.json"
},
"scripts": {
Expand Down
31 changes: 0 additions & 31 deletions packages/integrations/solid/server.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { sharedConfig } from 'solid-js';
import { hydrate, render, createComponent } from 'solid-js/web';

export default (element) =>
(Component, props, slotted, { client }) => {
export default (element: HTMLElement) =>
(Component: any, props: any, slotted: any, { client }: { client: string }) => {
// Prepare global object expected by Solid's hydration logic
if (!window._$HY) {
window._$HY = { events: [], completed: new WeakSet(), r: {} };
if (!(window as any)._$HY) {
(window as any)._$HY = { events: [], completed: new WeakSet(), r: {} };
}
if (!element.hasAttribute('ssr')) return;

const fn = client === 'only' ? render : hydrate;

let _slots = {};
let _slots: Record<string, any> = {};
if (Object.keys(slotted).length > 0) {
// hydrating
if (sharedConfig.context) {
Expand All @@ -28,6 +28,7 @@ export default (element) =>
}

const { default: children, ...slots } = _slots;
const renderId = element.dataset.solidRenderId;

fn(
() =>
Expand All @@ -36,6 +37,9 @@ export default (element) =>
...slots,
children,
}),
element
element,
{
renderId
}
);
};
28 changes: 28 additions & 0 deletions packages/integrations/solid/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { RendererContext } from './types';

type Context = {
id: string;
c: number;
}

const contexts = new WeakMap<RendererContext['result'], Context>();

export function getContext(result: RendererContext['result']): Context {
if(contexts.has(result)) {
return contexts.get(result)!;
}
let ctx = {
c: 0,
get id() {
return 's' + this.c.toString();
}
};
contexts.set(result, ctx);
return ctx;
}

export function incrementId(ctx: Context): string {
let id = ctx.id;
ctx.c++;
return id;
}
45 changes: 45 additions & 0 deletions packages/integrations/solid/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { RendererContext } from './types';
import { renderToString, ssr, createComponent } from 'solid-js/web';
import { getContext, incrementId } from './context.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) {
if (typeof Component !== 'function') return false;
const { html } = renderToStaticMarkup.call(this, Component, props, children);
return typeof html === 'string';
}

function renderToStaticMarkup(this: RendererContext, Component: any, props: Record<string, any>, { default: children, ...slotted }: any, metadata?: undefined | Record<string, any>) {
const renderId = metadata?.hydrate ? incrementId(getContext(this.result)) : '';

const html = renderToString(() => {
const slots: Record<string, any> = {};
for (const [key, value] of Object.entries(slotted)) {
const name = slotName(key);
slots[name] = ssr(`<astro-slot name="${name}">${value}</astro-slot>`);
}
// 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(`<astro-slot>${children}</astro-slot>`) : children,
};

return createComponent(Component, newProps);
}, {
renderId
});
return {
attrs: {
'data-solid-render-id': renderId
},
html
};
}

export default {
check,
renderToStaticMarkup,
};
4 changes: 4 additions & 0 deletions packages/integrations/solid/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { SSRResult } from 'astro';
export type RendererContext = {
result: SSRResult;
};
11 changes: 11 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 0af5aa7

Please sign in to comment.