Skip to content

Commit e9a77d8

Browse files
natemoo-rematthewp
andauthored
Improve nested and client:only hydration (#3455)
* wip: fix nested islands * fix: improve hydration for dynamic content * chore: fix bundle-size script for new files * chore: allow-list client:* directive files * fix(#3362): fix client:only behavior for React, Vue, Solid * test: add client-only e2e test * chore: update lockfile * test: fix e2e tests * test: add framework nesting e2e tests * Update packages/astro/src/runtime/client/events.ts Co-authored-by: Matthew Phillips <matthew@skypack.dev> * chore: add changeset * fix(preact): ignore hydrate roots * chore: remove `ssr` check in integrations * Revert "chore: remove `ssr` check in integrations" This reverts commit ba27eaa. * chore: add changeset Co-authored-by: Matthew Phillips <matthew@skypack.dev>
1 parent 4061459 commit e9a77d8

71 files changed

Lines changed: 2114 additions & 152 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/polite-hounds-lick.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@astrojs/preact': patch
3+
'@astrojs/react': patch
4+
'@astrojs/solid-js': patch
5+
'@astrojs/svelte': patch
6+
'@astrojs/vue': patch
7+
---
8+
9+
Update client hydration to check for `ssr` attribute. Requires `astro@^1.0.0-beta.36`.

.changeset/unlucky-gorillas-beg.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'astro': patch
3+
---
4+
5+
Implements improved hydration event system, meaning hydration for client:only and nested frameworks should be see significant stability improvements

.github/scripts/bundle-size.mjs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { build } from 'esbuild';
2+
import { existsSync } from 'fs';
23

34
const CLIENT_RUNTIME_PATH = 'packages/astro/src/runtime/client/';
45

@@ -32,7 +33,7 @@ export default async function checkBundleSize({ github, context }) {
3233
const output = await bundle(clientRuntimeFiles);
3334

3435
for (let [filename, { oldSize, newSize, sourceFile }] of Object.entries(output)) {
35-
filename = filename !== 'hmr' ? `client:${filename}` : filename;
36+
filename = ['idle', 'load', 'media', 'only', 'visible'].includes(filename) ? `client:${filename}` : filename;
3637
const prefix = (newSize - oldSize) === 0 ? '' : (newSize - oldSize) > 0 ? '+ ' : '- ';
3738
const change = `${prefix}${formatBytes(newSize - oldSize)}`;
3839
table.push(`| [\`${filename}\`](https://github.com/${context.repo.owner}/${context.repo.repo}/tree/${context.payload.pull_request.head.ref}/${sourceFile}) | ${formatBytes(oldSize)} | ${formatBytes(newSize)} | ${change} |`);
@@ -57,8 +58,9 @@ ${table.join('\n')}`,
5758
}
5859

5960
async function bundle(files) {
61+
6062
const { metafile } = await build({
61-
entryPoints: [...files.map(({ filename }) => filename), ...files.map(({ filename }) => `main/${filename}`)],
63+
entryPoints: [...files.map(({ filename }) => filename), ...files.map(({ filename }) => `main/${filename}`).filter(f => existsSync(f))],
6264
bundle: true,
6365
minify: true,
6466
sourcemap: false,
@@ -72,10 +74,10 @@ async function bundle(files) {
7274
if (filename.startsWith('main/')) {
7375
filename = filename.slice('main/'.length).replace(CLIENT_RUNTIME_PATH, '').replace('.js', '');
7476
const oldSize = info.bytes;
75-
return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? {}, { oldSize }) });
77+
return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? { oldSize: 0, newSize: 0 }, { oldSize }) });
7678
}
7779
filename = filename.replace(CLIENT_RUNTIME_PATH, '').replace('.js', '');
7880
const newSize = info.bytes;
79-
return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? {}, { newSize, sourceFile: Object.keys(info.inputs).find(src => src.endsWith('.ts')) }) });
81+
return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? { oldSize: 0, newSize: 0 }, { newSize, sourceFile: Object.keys(info.inputs).find(src => src.endsWith('.ts')) }) });
8082
}, {});
8183
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { test as base, expect } from '@playwright/test';
2+
import { loadFixture } from './test-utils.js';
3+
4+
const test = base.extend({
5+
astro: async ({}, use) => {
6+
const fixture = await loadFixture({ root: './fixtures/client-only/' });
7+
await use(fixture);
8+
},
9+
});
10+
11+
let devServer;
12+
13+
test.beforeEach(async ({ astro }) => {
14+
devServer = await astro.startDevServer();
15+
});
16+
17+
test.afterEach(async () => {
18+
await devServer.stop();
19+
});
20+
21+
test.describe('Client only', () => {
22+
test('React counter', async ({ astro, page }) => {
23+
await page.goto('/');
24+
25+
const counter = await page.locator('#react-counter');
26+
await expect(counter, 'component is visible').toBeVisible();
27+
28+
const count = await counter.locator('pre');
29+
await expect(count, 'initial count is 0').toHaveText('0');
30+
31+
const children = await counter.locator('.children');
32+
await expect(children, 'children exist').toHaveText('react');
33+
34+
const increment = await counter.locator('.increment');
35+
await increment.click();
36+
37+
await expect(count, 'count incremented by 1').toHaveText('1');
38+
});
39+
40+
test('Preact counter', async ({ astro, page }) => {
41+
await page.goto('/');
42+
43+
const counter = await page.locator('#preact-counter');
44+
await expect(counter, 'component is visible').toBeVisible();
45+
46+
const count = await counter.locator('pre');
47+
await expect(count, 'initial count is 0').toHaveText('0');
48+
49+
const children = await counter.locator('.children');
50+
await expect(children, 'children exist').toHaveText('preact');
51+
52+
const increment = await counter.locator('.increment');
53+
await increment.click();
54+
55+
await expect(count, 'count incremented by 1').toHaveText('1');
56+
});
57+
58+
test('Solid counter', async ({ astro, page }) => {
59+
await page.goto('/');
60+
61+
const counter = await page.locator('#solid-counter');
62+
await expect(counter, 'component is visible').toBeVisible();
63+
64+
const count = await counter.locator('pre');
65+
await expect(count, 'initial count is 0').toHaveText('0');
66+
67+
const children = await counter.locator('.children');
68+
await expect(children, 'children exist').toHaveText('solid');
69+
70+
const increment = await counter.locator('.increment');
71+
await increment.click();
72+
73+
await expect(count, 'count incremented by 1').toHaveText('1');
74+
});
75+
76+
test('Vue counter', async ({ astro, page }) => {
77+
await page.goto('/');
78+
79+
const counter = await page.locator('#vue-counter');
80+
await expect(counter, 'component is visible').toBeVisible();
81+
82+
const count = await counter.locator('pre');
83+
await expect(count, 'initial count is 0').toHaveText('0');
84+
85+
const children = await counter.locator('.children');
86+
await expect(children, 'children exist').toHaveText('vue');
87+
88+
const increment = await counter.locator('.increment');
89+
await increment.click();
90+
91+
await expect(count, 'count incremented by 1').toHaveText('1');
92+
});
93+
94+
test('Svelte counter', async ({ astro, page }) => {
95+
await page.goto('/');
96+
97+
const counter = await page.locator('#svelte-counter');
98+
await expect(counter, 'component is visible').toBeVisible();
99+
100+
const count = await counter.locator('pre');
101+
await expect(count, 'initial count is 0').toHaveText('0');
102+
103+
const children = await counter.locator('.children');
104+
await expect(children, 'children exist').toHaveText('svelte');
105+
106+
const increment = await counter.locator('.increment');
107+
await increment.click();
108+
109+
await expect(count, 'count incremented by 1').toHaveText('1');
110+
});
111+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from 'astro/config';
2+
import preact from '@astrojs/preact';
3+
import react from '@astrojs/react';
4+
import svelte from '@astrojs/svelte';
5+
import vue from '@astrojs/vue';
6+
import solid from '@astrojs/solid-js';
7+
8+
// https://astro.build/config
9+
export default defineConfig({
10+
// Enable many frameworks to support all different kinds of components.
11+
integrations: [preact(), react(), svelte(), vue(), solid()],
12+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@e2e/client-only",
3+
"version": "0.0.0",
4+
"private": true,
5+
"devDependencies": {
6+
"@astrojs/preact": "^0.1.2",
7+
"@astrojs/react": "^0.1.2",
8+
"@astrojs/solid-js": "^0.1.2",
9+
"@astrojs/svelte": "^0.1.3",
10+
"@astrojs/vue": "^0.1.4",
11+
"astro": "^1.0.0-beta.32"
12+
},
13+
"dependencies": {
14+
"preact": "^10.7.2",
15+
"react": "^18.1.0",
16+
"react-dom": "^18.1.0",
17+
"solid-js": "^1.4.2",
18+
"svelte": "^3.48.0",
19+
"vue": "^3.2.36"
20+
}
21+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useState } from 'preact/hooks';
2+
3+
/** a counter written in Preact */
4+
export function PreactCounter({ children, id }) {
5+
const [count, setCount] = useState(0);
6+
const add = () => setCount((i) => i + 1);
7+
const subtract = () => setCount((i) => i - 1);
8+
9+
return (
10+
<div id={id} class="counter">
11+
<button class="decrement" onClick={subtract}>-</button>
12+
<pre>{count}</pre>
13+
<button class="increment" onClick={add}>+</button>
14+
<div class="children">{children}</div>
15+
</div>
16+
);
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { useState } from 'react';
2+
3+
/** a counter written in React */
4+
export function Counter({ children, id }) {
5+
const [count, setCount] = useState(0);
6+
const add = () => setCount((i) => i + 1);
7+
const subtract = () => setCount((i) => i - 1);
8+
9+
return (
10+
<div id={id} className="counter">
11+
<button className="decrement" onClick={subtract}>-</button>
12+
<pre>{count}</pre>
13+
<button className="increment" onClick={add}>+</button>
14+
<div className="children">{children}</div>
15+
</div>
16+
);
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createSignal } from 'solid-js';
2+
3+
/** a counter written with Solid */
4+
export default function SolidCounter({ children, id }) {
5+
const [count, setCount] = createSignal(0);
6+
const add = () => setCount(count() + 1);
7+
const subtract = () => setCount(count() - 1);
8+
9+
return (
10+
<div id={id} class="counter">
11+
<button class="decrement" onClick={subtract}>-</button>
12+
<pre>{count()}</pre>
13+
<button class="increment" onClick={add}>+</button>
14+
<div class="children">{children}</div>
15+
</div>
16+
);
17+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
<script>
3+
export let id;
4+
let children;
5+
let count = 0;
6+
7+
function add() {
8+
count += 1;
9+
}
10+
11+
function subtract() {
12+
count -= 1;
13+
}
14+
</script>
15+
16+
<div {id} class="counter">
17+
<button class="decrement" on:click={subtract}>-</button>
18+
<pre>{ count }</pre>
19+
<button class="increment" on:click={add}>+</button>
20+
<div class="children">
21+
<slot />
22+
</div>
23+
</div>
24+
25+
<style>
26+
.counter {
27+
background: white;
28+
}
29+
</style>

0 commit comments

Comments
 (0)