Skip to content

Commit

Permalink
Improve nested and client:only hydration (#3455)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
natemoo-re and matthewp authored May 31, 2022
1 parent 4061459 commit e9a77d8
Show file tree
Hide file tree
Showing 71 changed files with 2,114 additions and 152 deletions.
9 changes: 9 additions & 0 deletions .changeset/polite-hounds-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@astrojs/preact': patch
'@astrojs/react': patch
'@astrojs/solid-js': patch
'@astrojs/svelte': patch
'@astrojs/vue': patch
---

Update client hydration to check for `ssr` attribute. Requires `astro@^1.0.0-beta.36`.
5 changes: 5 additions & 0 deletions .changeset/unlucky-gorillas-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Implements improved hydration event system, meaning hydration for client:only and nested frameworks should be see significant stability improvements
10 changes: 6 additions & 4 deletions .github/scripts/bundle-size.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { build } from 'esbuild';
import { existsSync } from 'fs';

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

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

for (let [filename, { oldSize, newSize, sourceFile }] of Object.entries(output)) {
filename = filename !== 'hmr' ? `client:${filename}` : filename;
filename = ['idle', 'load', 'media', 'only', 'visible'].includes(filename) ? `client:${filename}` : filename;
const prefix = (newSize - oldSize) === 0 ? '' : (newSize - oldSize) > 0 ? '+ ' : '- ';
const change = `${prefix}${formatBytes(newSize - oldSize)}`;
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} |`);
Expand All @@ -57,8 +58,9 @@ ${table.join('\n')}`,
}

async function bundle(files) {

const { metafile } = await build({
entryPoints: [...files.map(({ filename }) => filename), ...files.map(({ filename }) => `main/${filename}`)],
entryPoints: [...files.map(({ filename }) => filename), ...files.map(({ filename }) => `main/${filename}`).filter(f => existsSync(f))],
bundle: true,
minify: true,
sourcemap: false,
Expand All @@ -72,10 +74,10 @@ async function bundle(files) {
if (filename.startsWith('main/')) {
filename = filename.slice('main/'.length).replace(CLIENT_RUNTIME_PATH, '').replace('.js', '');
const oldSize = info.bytes;
return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? {}, { oldSize }) });
return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? { oldSize: 0, newSize: 0 }, { oldSize }) });
}
filename = filename.replace(CLIENT_RUNTIME_PATH, '').replace('.js', '');
const newSize = info.bytes;
return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? {}, { newSize, sourceFile: Object.keys(info.inputs).find(src => src.endsWith('.ts')) }) });
return Object.assign(acc, { [filename]: Object.assign(acc[filename] ?? { oldSize: 0, newSize: 0 }, { newSize, sourceFile: Object.keys(info.inputs).find(src => src.endsWith('.ts')) }) });
}, {});
}
111 changes: 111 additions & 0 deletions packages/astro/e2e/client-only.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { test as base, expect } from '@playwright/test';
import { loadFixture } from './test-utils.js';

const test = base.extend({
astro: async ({}, use) => {
const fixture = await loadFixture({ root: './fixtures/client-only/' });
await use(fixture);
},
});

let devServer;

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

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

test.describe('Client only', () => {
test('React counter', async ({ astro, page }) => {
await page.goto('/');

const counter = await page.locator('#react-counter');
await expect(counter, 'component is visible').toBeVisible();

const count = await counter.locator('pre');
await expect(count, 'initial count is 0').toHaveText('0');

const children = await counter.locator('.children');
await expect(children, 'children exist').toHaveText('react');

const increment = await counter.locator('.increment');
await increment.click();

await expect(count, 'count incremented by 1').toHaveText('1');
});

test('Preact counter', async ({ astro, page }) => {
await page.goto('/');

const counter = await page.locator('#preact-counter');
await expect(counter, 'component is visible').toBeVisible();

const count = await counter.locator('pre');
await expect(count, 'initial count is 0').toHaveText('0');

const children = await counter.locator('.children');
await expect(children, 'children exist').toHaveText('preact');

const increment = await counter.locator('.increment');
await increment.click();

await expect(count, 'count incremented by 1').toHaveText('1');
});

test('Solid counter', async ({ astro, page }) => {
await page.goto('/');

const counter = await page.locator('#solid-counter');
await expect(counter, 'component is visible').toBeVisible();

const count = await counter.locator('pre');
await expect(count, 'initial count is 0').toHaveText('0');

const children = await counter.locator('.children');
await expect(children, 'children exist').toHaveText('solid');

const increment = await counter.locator('.increment');
await increment.click();

await expect(count, 'count incremented by 1').toHaveText('1');
});

test('Vue counter', async ({ astro, page }) => {
await page.goto('/');

const counter = await page.locator('#vue-counter');
await expect(counter, 'component is visible').toBeVisible();

const count = await counter.locator('pre');
await expect(count, 'initial count is 0').toHaveText('0');

const children = await counter.locator('.children');
await expect(children, 'children exist').toHaveText('vue');

const increment = await counter.locator('.increment');
await increment.click();

await expect(count, 'count incremented by 1').toHaveText('1');
});

test('Svelte counter', async ({ astro, page }) => {
await page.goto('/');

const counter = await page.locator('#svelte-counter');
await expect(counter, 'component is visible').toBeVisible();

const count = await counter.locator('pre');
await expect(count, 'initial count is 0').toHaveText('0');

const children = await counter.locator('.children');
await expect(children, 'children exist').toHaveText('svelte');

const increment = await counter.locator('.increment');
await increment.click();

await expect(count, 'count incremented by 1').toHaveText('1');
});
});
12 changes: 12 additions & 0 deletions packages/astro/e2e/fixtures/client-only/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
import react from '@astrojs/react';
import svelte from '@astrojs/svelte';
import vue from '@astrojs/vue';
import solid from '@astrojs/solid-js';

// https://astro.build/config
export default defineConfig({
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue(), solid()],
});
21 changes: 21 additions & 0 deletions packages/astro/e2e/fixtures/client-only/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@e2e/client-only",
"version": "0.0.0",
"private": true,
"devDependencies": {
"@astrojs/preact": "^0.1.2",
"@astrojs/react": "^0.1.2",
"@astrojs/solid-js": "^0.1.2",
"@astrojs/svelte": "^0.1.3",
"@astrojs/vue": "^0.1.4",
"astro": "^1.0.0-beta.32"
},
"dependencies": {
"preact": "^10.7.2",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"solid-js": "^1.4.2",
"svelte": "^3.48.0",
"vue": "^3.2.36"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useState } from 'preact/hooks';

/** a counter written in Preact */
export function PreactCounter({ children, id }) {
const [count, setCount] = useState(0);
const add = () => setCount((i) => i + 1);
const subtract = () => setCount((i) => i - 1);

return (
<div id={id} class="counter">
<button class="decrement" onClick={subtract}>-</button>
<pre>{count}</pre>
<button class="increment" onClick={add}>+</button>
<div class="children">{children}</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useState } from 'react';

/** a counter written in React */
export function Counter({ children, id }) {
const [count, setCount] = useState(0);
const add = () => setCount((i) => i + 1);
const subtract = () => setCount((i) => i - 1);

return (
<div id={id} className="counter">
<button className="decrement" onClick={subtract}>-</button>
<pre>{count}</pre>
<button className="increment" onClick={add}>+</button>
<div className="children">{children}</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createSignal } from 'solid-js';

/** a counter written with Solid */
export default function SolidCounter({ children, id }) {
const [count, setCount] = createSignal(0);
const add = () => setCount(count() + 1);
const subtract = () => setCount(count() - 1);

return (
<div id={id} class="counter">
<button class="decrement" onClick={subtract}>-</button>
<pre>{count()}</pre>
<button class="increment" onClick={add}>+</button>
<div class="children">{children}</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

<script>
export let id;
let children;
let count = 0;
function add() {
count += 1;
}
function subtract() {
count -= 1;
}
</script>

<div {id} class="counter">
<button class="decrement" on:click={subtract}>-</button>
<pre>{ count }</pre>
<button class="increment" on:click={add}>+</button>
<div class="children">
<slot />
</div>
</div>

<style>
.counter {
background: white;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<template>
<div :id="id" class="counter">
<button class="decrement" @click="subtract()">-</button>
<pre>{{ count }}</pre>
<button class="increment" @click="add()">+</button>
<div class="children">
<slot />
</div>
</div>
</template>

<script>
import { ref } from 'vue';
export default {
props: {
id: {
type: String,
required: true
}
},
setup(props) {
const count = ref(0);
const add = () => (count.value = count.value + 1);
const subtract = () => (count.value = count.value - 1);
return {
id: props.id,
count,
add,
subtract,
};
},
};
</script>
41 changes: 41 additions & 0 deletions packages/astro/e2e/fixtures/client-only/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
import * as react from '../components/ReactCounter.jsx';
import { PreactCounter } from '../components/PreactCounter.tsx';
import SolidCounter from '../components/SolidCounter.tsx';
import VueCounter from '../components/VueCounter.vue';
import SvelteCounter from '../components/SvelteCounter.svelte';
// Full Astro Component Syntax:
// https://docs.astro.build/core-concepts/astro-components/
---

<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
</head>
<body>
<main>
<react.Counter id="react-counter" client:only="react">
<h1>react</h1>
</react.Counter>

<PreactCounter id="preact-counter" client:only="preact">
<h1>preact</h1>
</PreactCounter>

<SolidCounter id="solid-counter" client:only="solid-js">
<h1>solid</h1>
</SolidCounter>

<VueCounter id="vue-counter" client:only="vue">
<h1>vue</h1>
</VueCounter>

<SvelteCounter id="svelte-counter" client:only="svelte">
<h1>svelte</h1>
</SvelteCounter>
</main>
</body>
</html>
12 changes: 12 additions & 0 deletions packages/astro/e2e/fixtures/nested-in-preact/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
import react from '@astrojs/react';
import svelte from '@astrojs/svelte';
import vue from '@astrojs/vue';
import solid from '@astrojs/solid-js';

// https://astro.build/config
export default defineConfig({
// Enable many frameworks to support all different kinds of components.
integrations: [preact(), react(), svelte(), vue(), solid()],
});
21 changes: 21 additions & 0 deletions packages/astro/e2e/fixtures/nested-in-preact/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@e2e/nested-in-preact",
"version": "0.0.0",
"private": true,
"devDependencies": {
"@astrojs/preact": "^0.1.2",
"@astrojs/react": "^0.1.2",
"@astrojs/solid-js": "^0.1.2",
"@astrojs/svelte": "^0.1.3",
"@astrojs/vue": "^0.1.4",
"astro": "^1.0.0-beta.32"
},
"dependencies": {
"preact": "^10.7.2",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"solid-js": "^1.4.2",
"svelte": "^3.48.0",
"vue": "^3.2.36"
}
}
Loading

0 comments on commit e9a77d8

Please sign in to comment.