diff --git a/.changeset/polite-hounds-lick.md b/.changeset/polite-hounds-lick.md
new file mode 100644
index 000000000000..8b2191648117
--- /dev/null
+++ b/.changeset/polite-hounds-lick.md
@@ -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`.
diff --git a/.changeset/unlucky-gorillas-beg.md b/.changeset/unlucky-gorillas-beg.md
new file mode 100644
index 000000000000..5e667a06233f
--- /dev/null
+++ b/.changeset/unlucky-gorillas-beg.md
@@ -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
diff --git a/.github/scripts/bundle-size.mjs b/.github/scripts/bundle-size.mjs
index 4166735a9f27..618d6e8cb546 100644
--- a/.github/scripts/bundle-size.mjs
+++ b/.github/scripts/bundle-size.mjs
@@ -1,4 +1,5 @@
import { build } from 'esbuild';
+import { existsSync } from 'fs';
const CLIENT_RUNTIME_PATH = 'packages/astro/src/runtime/client/';
@@ -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} |`);
@@ -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,
@@ -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')) }) });
}, {});
}
diff --git a/packages/astro/e2e/client-only.test.js b/packages/astro/e2e/client-only.test.js
new file mode 100644
index 000000000000..78a06fdbe31b
--- /dev/null
+++ b/packages/astro/e2e/client-only.test.js
@@ -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');
+ });
+});
diff --git a/packages/astro/e2e/fixtures/client-only/astro.config.mjs b/packages/astro/e2e/fixtures/client-only/astro.config.mjs
new file mode 100644
index 000000000000..4b50887cd70c
--- /dev/null
+++ b/packages/astro/e2e/fixtures/client-only/astro.config.mjs
@@ -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()],
+});
diff --git a/packages/astro/e2e/fixtures/client-only/package.json b/packages/astro/e2e/fixtures/client-only/package.json
new file mode 100644
index 000000000000..2ff99efb18e9
--- /dev/null
+++ b/packages/astro/e2e/fixtures/client-only/package.json
@@ -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"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/client-only/src/components/PreactCounter.tsx b/packages/astro/e2e/fixtures/client-only/src/components/PreactCounter.tsx
new file mode 100644
index 000000000000..b0570046c4f8
--- /dev/null
+++ b/packages/astro/e2e/fixtures/client-only/src/components/PreactCounter.tsx
@@ -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 (
+
+
+
{count}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/client-only/src/components/ReactCounter.jsx b/packages/astro/e2e/fixtures/client-only/src/components/ReactCounter.jsx
new file mode 100644
index 000000000000..30d804199bbd
--- /dev/null
+++ b/packages/astro/e2e/fixtures/client-only/src/components/ReactCounter.jsx
@@ -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 (
+
+
+
{count}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/client-only/src/components/SolidCounter.tsx b/packages/astro/e2e/fixtures/client-only/src/components/SolidCounter.tsx
new file mode 100644
index 000000000000..fbbb9850b5e4
--- /dev/null
+++ b/packages/astro/e2e/fixtures/client-only/src/components/SolidCounter.tsx
@@ -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 (
+
+
+
{count()}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/client-only/src/components/SvelteCounter.svelte b/packages/astro/e2e/fixtures/client-only/src/components/SvelteCounter.svelte
new file mode 100644
index 000000000000..cc9fe8c93c60
--- /dev/null
+++ b/packages/astro/e2e/fixtures/client-only/src/components/SvelteCounter.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
+
{ count }
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/client-only/src/components/VueCounter.vue b/packages/astro/e2e/fixtures/client-only/src/components/VueCounter.vue
new file mode 100644
index 000000000000..906da19447c9
--- /dev/null
+++ b/packages/astro/e2e/fixtures/client-only/src/components/VueCounter.vue
@@ -0,0 +1,34 @@
+
+
+
+
{{ count }}
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/client-only/src/pages/index.astro b/packages/astro/e2e/fixtures/client-only/src/pages/index.astro
new file mode 100644
index 000000000000..708ba1582dc9
--- /dev/null
+++ b/packages/astro/e2e/fixtures/client-only/src/pages/index.astro
@@ -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/
+---
+
+
+
+
+
+
+
+
+
+
+ react
+
+
+
+ preact
+
+
+
+ solid
+
+
+
+ vue
+
+
+
+ svelte
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-preact/astro.config.mjs b/packages/astro/e2e/fixtures/nested-in-preact/astro.config.mjs
new file mode 100644
index 000000000000..4b50887cd70c
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-preact/astro.config.mjs
@@ -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()],
+});
diff --git a/packages/astro/e2e/fixtures/nested-in-preact/package.json b/packages/astro/e2e/fixtures/nested-in-preact/package.json
new file mode 100644
index 000000000000..c09c7838cddd
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-preact/package.json
@@ -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"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-preact/src/components/PreactCounter.tsx b/packages/astro/e2e/fixtures/nested-in-preact/src/components/PreactCounter.tsx
new file mode 100644
index 000000000000..5f20f560d1f6
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-preact/src/components/PreactCounter.tsx
@@ -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 (
+
+
+
{count}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-preact/src/components/ReactCounter.jsx b/packages/astro/e2e/fixtures/nested-in-preact/src/components/ReactCounter.jsx
new file mode 100644
index 000000000000..0dc0deb47b38
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-preact/src/components/ReactCounter.jsx
@@ -0,0 +1,17 @@
+import { useState } from 'react';
+
+/** a counter written in React */
+export default function Counter({ children, id }) {
+ const [count, setCount] = useState(0);
+ const add = () => setCount((i) => i + 1);
+ const subtract = () => setCount((i) => i - 1);
+
+ return (
+
+
+
{count}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-preact/src/components/SolidCounter.tsx b/packages/astro/e2e/fixtures/nested-in-preact/src/components/SolidCounter.tsx
new file mode 100644
index 000000000000..afabe43b9e81
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-preact/src/components/SolidCounter.tsx
@@ -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 (
+
+
+
{count()}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-preact/src/components/SvelteCounter.svelte b/packages/astro/e2e/fixtures/nested-in-preact/src/components/SvelteCounter.svelte
new file mode 100644
index 000000000000..733f58076a24
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-preact/src/components/SvelteCounter.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
+
{ count }
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-preact/src/components/VueCounter.vue b/packages/astro/e2e/fixtures/nested-in-preact/src/components/VueCounter.vue
new file mode 100644
index 000000000000..d404cc965c52
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-preact/src/components/VueCounter.vue
@@ -0,0 +1,34 @@
+
+
+
+
{{ count }}
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-preact/src/pages/index.astro b/packages/astro/e2e/fixtures/nested-in-preact/src/pages/index.astro
new file mode 100644
index 000000000000..619e8cccd0ae
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-preact/src/pages/index.astro
@@ -0,0 +1,28 @@
+---
+import ReactCounter 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/
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-react/astro.config.mjs b/packages/astro/e2e/fixtures/nested-in-react/astro.config.mjs
new file mode 100644
index 000000000000..4b50887cd70c
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-react/astro.config.mjs
@@ -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()],
+});
diff --git a/packages/astro/e2e/fixtures/nested-in-react/package.json b/packages/astro/e2e/fixtures/nested-in-react/package.json
new file mode 100644
index 000000000000..fae7b3e2b8c1
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-react/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@e2e/nested-in-react",
+ "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"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-react/src/components/PreactCounter.tsx b/packages/astro/e2e/fixtures/nested-in-react/src/components/PreactCounter.tsx
new file mode 100644
index 000000000000..5f20f560d1f6
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-react/src/components/PreactCounter.tsx
@@ -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 (
+
+
+
{count}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-react/src/components/ReactCounter.jsx b/packages/astro/e2e/fixtures/nested-in-react/src/components/ReactCounter.jsx
new file mode 100644
index 000000000000..0dc0deb47b38
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-react/src/components/ReactCounter.jsx
@@ -0,0 +1,17 @@
+import { useState } from 'react';
+
+/** a counter written in React */
+export default function Counter({ children, id }) {
+ const [count, setCount] = useState(0);
+ const add = () => setCount((i) => i + 1);
+ const subtract = () => setCount((i) => i - 1);
+
+ return (
+
+
+
{count}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-react/src/components/SolidCounter.tsx b/packages/astro/e2e/fixtures/nested-in-react/src/components/SolidCounter.tsx
new file mode 100644
index 000000000000..afabe43b9e81
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-react/src/components/SolidCounter.tsx
@@ -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 (
+
+
+
{count()}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-react/src/components/SvelteCounter.svelte b/packages/astro/e2e/fixtures/nested-in-react/src/components/SvelteCounter.svelte
new file mode 100644
index 000000000000..733f58076a24
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-react/src/components/SvelteCounter.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
+
{ count }
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-react/src/components/VueCounter.vue b/packages/astro/e2e/fixtures/nested-in-react/src/components/VueCounter.vue
new file mode 100644
index 000000000000..d404cc965c52
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-react/src/components/VueCounter.vue
@@ -0,0 +1,34 @@
+
+
+
+
{{ count }}
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-react/src/pages/index.astro b/packages/astro/e2e/fixtures/nested-in-react/src/pages/index.astro
new file mode 100644
index 000000000000..0b3b23d9d8e8
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-react/src/pages/index.astro
@@ -0,0 +1,28 @@
+---
+import ReactCounter 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/
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-solid/astro.config.mjs b/packages/astro/e2e/fixtures/nested-in-solid/astro.config.mjs
new file mode 100644
index 000000000000..4b50887cd70c
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-solid/astro.config.mjs
@@ -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()],
+});
diff --git a/packages/astro/e2e/fixtures/nested-in-solid/package.json b/packages/astro/e2e/fixtures/nested-in-solid/package.json
new file mode 100644
index 000000000000..412561a6de94
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-solid/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@e2e/nested-in-solid",
+ "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"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-solid/src/components/PreactCounter.tsx b/packages/astro/e2e/fixtures/nested-in-solid/src/components/PreactCounter.tsx
new file mode 100644
index 000000000000..5f20f560d1f6
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-solid/src/components/PreactCounter.tsx
@@ -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 (
+
+
+
{count}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-solid/src/components/ReactCounter.jsx b/packages/astro/e2e/fixtures/nested-in-solid/src/components/ReactCounter.jsx
new file mode 100644
index 000000000000..c7197a072b85
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-solid/src/components/ReactCounter.jsx
@@ -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 (
+
+
+
{count}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-solid/src/components/SolidCounter.tsx b/packages/astro/e2e/fixtures/nested-in-solid/src/components/SolidCounter.tsx
new file mode 100644
index 000000000000..afabe43b9e81
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-solid/src/components/SolidCounter.tsx
@@ -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 (
+
+
+
{count()}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-solid/src/components/SvelteCounter.svelte b/packages/astro/e2e/fixtures/nested-in-solid/src/components/SvelteCounter.svelte
new file mode 100644
index 000000000000..733f58076a24
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-solid/src/components/SvelteCounter.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
+
{ count }
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-solid/src/components/VueCounter.vue b/packages/astro/e2e/fixtures/nested-in-solid/src/components/VueCounter.vue
new file mode 100644
index 000000000000..d404cc965c52
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-solid/src/components/VueCounter.vue
@@ -0,0 +1,34 @@
+
+
+
+
{{ count }}
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-solid/src/pages/index.astro b/packages/astro/e2e/fixtures/nested-in-solid/src/pages/index.astro
new file mode 100644
index 000000000000..0feb5ba60035
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-solid/src/pages/index.astro
@@ -0,0 +1,28 @@
+---
+import { Counter as ReactCounter } 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/
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-svelte/astro.config.mjs b/packages/astro/e2e/fixtures/nested-in-svelte/astro.config.mjs
new file mode 100644
index 000000000000..4b50887cd70c
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-svelte/astro.config.mjs
@@ -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()],
+});
diff --git a/packages/astro/e2e/fixtures/nested-in-svelte/package.json b/packages/astro/e2e/fixtures/nested-in-svelte/package.json
new file mode 100644
index 000000000000..44c3018cf0ed
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-svelte/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@e2e/nested-in-svelte",
+ "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"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-svelte/src/components/PreactCounter.tsx b/packages/astro/e2e/fixtures/nested-in-svelte/src/components/PreactCounter.tsx
new file mode 100644
index 000000000000..5f20f560d1f6
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-svelte/src/components/PreactCounter.tsx
@@ -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 (
+
+
+
{count}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-svelte/src/components/ReactCounter.jsx b/packages/astro/e2e/fixtures/nested-in-svelte/src/components/ReactCounter.jsx
new file mode 100644
index 000000000000..c7197a072b85
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-svelte/src/components/ReactCounter.jsx
@@ -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 (
+
+
+
{count}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-svelte/src/components/SolidCounter.tsx b/packages/astro/e2e/fixtures/nested-in-svelte/src/components/SolidCounter.tsx
new file mode 100644
index 000000000000..afabe43b9e81
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-svelte/src/components/SolidCounter.tsx
@@ -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 (
+
+
+
{count()}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-svelte/src/components/SvelteCounter.svelte b/packages/astro/e2e/fixtures/nested-in-svelte/src/components/SvelteCounter.svelte
new file mode 100644
index 000000000000..733f58076a24
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-svelte/src/components/SvelteCounter.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
+
{ count }
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-svelte/src/components/VueCounter.vue b/packages/astro/e2e/fixtures/nested-in-svelte/src/components/VueCounter.vue
new file mode 100644
index 000000000000..d404cc965c52
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-svelte/src/components/VueCounter.vue
@@ -0,0 +1,34 @@
+
+
+
+
{{ count }}
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-svelte/src/pages/index.astro b/packages/astro/e2e/fixtures/nested-in-svelte/src/pages/index.astro
new file mode 100644
index 000000000000..764ebfaf702e
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-svelte/src/pages/index.astro
@@ -0,0 +1,28 @@
+---
+import { Counter as ReactCounter } 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/
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-vue/astro.config.mjs b/packages/astro/e2e/fixtures/nested-in-vue/astro.config.mjs
new file mode 100644
index 000000000000..4b50887cd70c
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-vue/astro.config.mjs
@@ -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()],
+});
diff --git a/packages/astro/e2e/fixtures/nested-in-vue/package.json b/packages/astro/e2e/fixtures/nested-in-vue/package.json
new file mode 100644
index 000000000000..b25b3e0b1dc7
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-vue/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@e2e/nested-in-vue",
+ "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"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-vue/src/components/PreactCounter.tsx b/packages/astro/e2e/fixtures/nested-in-vue/src/components/PreactCounter.tsx
new file mode 100644
index 000000000000..5f20f560d1f6
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-vue/src/components/PreactCounter.tsx
@@ -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 (
+
+
+
{count}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-vue/src/components/ReactCounter.jsx b/packages/astro/e2e/fixtures/nested-in-vue/src/components/ReactCounter.jsx
new file mode 100644
index 000000000000..c7197a072b85
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-vue/src/components/ReactCounter.jsx
@@ -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 (
+
+
+
{count}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-vue/src/components/SolidCounter.tsx b/packages/astro/e2e/fixtures/nested-in-vue/src/components/SolidCounter.tsx
new file mode 100644
index 000000000000..afabe43b9e81
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-vue/src/components/SolidCounter.tsx
@@ -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 (
+
+
+
{count()}
+
+
{children}
+
+ );
+}
diff --git a/packages/astro/e2e/fixtures/nested-in-vue/src/components/SvelteCounter.svelte b/packages/astro/e2e/fixtures/nested-in-vue/src/components/SvelteCounter.svelte
new file mode 100644
index 000000000000..733f58076a24
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-vue/src/components/SvelteCounter.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+
+
{ count }
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-vue/src/components/VueCounter.vue b/packages/astro/e2e/fixtures/nested-in-vue/src/components/VueCounter.vue
new file mode 100644
index 000000000000..d404cc965c52
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-vue/src/components/VueCounter.vue
@@ -0,0 +1,34 @@
+
+
+
+
{{ count }}
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/nested-in-vue/src/pages/index.astro b/packages/astro/e2e/fixtures/nested-in-vue/src/pages/index.astro
new file mode 100644
index 000000000000..5e4d47d768b3
--- /dev/null
+++ b/packages/astro/e2e/fixtures/nested-in-vue/src/pages/index.astro
@@ -0,0 +1,28 @@
+---
+import { Counter as ReactCounter } 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/
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/nested-in-preact.test.js b/packages/astro/e2e/nested-in-preact.test.js
new file mode 100644
index 000000000000..ab4d3c6ba298
--- /dev/null
+++ b/packages/astro/e2e/nested-in-preact.test.js
@@ -0,0 +1,96 @@
+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/nested-in-preact/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+test.describe('Nested Frameworks in Preact', () => {
+ 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('#react-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#react-counter-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('#preact-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#preact-counter-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('#solid-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#solid-counter-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('#vue-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#vue-counter-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('#svelte-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#svelte-counter-increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+});
diff --git a/packages/astro/e2e/nested-in-react.test.js b/packages/astro/e2e/nested-in-react.test.js
new file mode 100644
index 000000000000..5b7a0d18b01e
--- /dev/null
+++ b/packages/astro/e2e/nested-in-react.test.js
@@ -0,0 +1,96 @@
+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/nested-in-react/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+test.describe('Nested Frameworks in React', () => {
+ 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('#react-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#react-counter-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('#preact-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#preact-counter-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('#solid-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#solid-counter-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('#vue-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#vue-counter-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('#svelte-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#svelte-counter-increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+});
diff --git a/packages/astro/e2e/nested-in-solid.test.js b/packages/astro/e2e/nested-in-solid.test.js
new file mode 100644
index 000000000000..11f97f9d8f0c
--- /dev/null
+++ b/packages/astro/e2e/nested-in-solid.test.js
@@ -0,0 +1,97 @@
+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/nested-in-solid/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+test.describe('Nested Frameworks in Solid', () => {
+ 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('#react-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#react-counter-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('#preact-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#preact-counter-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('#solid-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#solid-counter-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('#vue-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#vue-counter-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('#svelte-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#svelte-counter-increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+});
+
diff --git a/packages/astro/e2e/nested-in-svelte.test.js b/packages/astro/e2e/nested-in-svelte.test.js
new file mode 100644
index 000000000000..f951854c7f43
--- /dev/null
+++ b/packages/astro/e2e/nested-in-svelte.test.js
@@ -0,0 +1,96 @@
+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/nested-in-svelte/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+test.describe('Nested Frameworks in Svelte', () => {
+ 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('#react-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#react-counter-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('#preact-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#preact-counter-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('#solid-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#solid-counter-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('#vue-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#vue-counter-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('#svelte-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#svelte-counter-increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+});
diff --git a/packages/astro/e2e/nested-in-vue.test.js b/packages/astro/e2e/nested-in-vue.test.js
new file mode 100644
index 000000000000..c3fa9a20379c
--- /dev/null
+++ b/packages/astro/e2e/nested-in-vue.test.js
@@ -0,0 +1,96 @@
+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/nested-in-vue/' });
+ await use(fixture);
+ },
+});
+
+let devServer;
+
+test.beforeEach(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+});
+
+test.afterEach(async () => {
+ await devServer.stop();
+});
+
+test.describe('Nested Frameworks in Vue', () => {
+ 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('#react-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#react-counter-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('#preact-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#preact-counter-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('#solid-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#solid-counter-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('#vue-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#vue-counter-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('#svelte-counter-count');
+ await expect(count, 'initial count is 0').toHaveText('0');
+
+ const increment = await counter.locator('#svelte-counter-increment');
+ await increment.click();
+
+ await expect(count, 'count incremented by 1').toHaveText('1');
+ });
+});
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 31f9e98f3c39..c48f2e2a7903 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -763,7 +763,7 @@ export interface MarkdownInstance> {
}
export type GetHydrateCallback = () => Promise<
- (element: Element, innerHTML: string | null) => void
+ (element: Element, innerHTML: string | null) => void | Promise
>;
/**
diff --git a/packages/astro/src/runtime/client/events.ts b/packages/astro/src/runtime/client/events.ts
new file mode 100644
index 000000000000..aa8e92736350
--- /dev/null
+++ b/packages/astro/src/runtime/client/events.ts
@@ -0,0 +1,24 @@
+const HYDRATE_KEY = `astro:hydrate`;
+function debounce any>(cb: T, wait = 20) {
+ let h = 0;
+ let callable = (...args: any) => {
+ clearTimeout(h);
+ h = setTimeout(() => cb(...args), wait) as unknown as number;
+ };
+ return callable as T;
+}
+
+export const notify = debounce(() => {
+ if (document.querySelector('astro-root[ssr]')) {
+ window.dispatchEvent(new CustomEvent(HYDRATE_KEY));
+ }
+});
+
+export const listen = (cb: (...args: any[]) => any) => window.addEventListener(HYDRATE_KEY, cb, { once: true });
+
+if (!(window as any)[HYDRATE_KEY]) {
+ if ('MutationObserver' in window) {
+ new MutationObserver(notify).observe(document.body, { subtree: true, childList: true });
+ }
+ (window as any)[HYDRATE_KEY] = true;
+}
diff --git a/packages/astro/src/runtime/client/idle.ts b/packages/astro/src/runtime/client/idle.ts
index e1e1c5b2f7ab..ff37585e6aa9 100644
--- a/packages/astro/src/runtime/client/idle.ts
+++ b/packages/astro/src/runtime/client/idle.ts
@@ -1,7 +1,8 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
+import { notify, listen } from './events';
/**
- * Hydrate this component as soon as the main thread is free!
+ * Hydrate this component as soon as the main thread is free
* (or after a short delay, if `requestIdleCallback`) isn't supported
*/
export default async function onIdle(
@@ -9,35 +10,44 @@ export default async function onIdle(
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
- const cb = async () => {
- const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
- if (roots.length === 0) {
- throw new Error(`Unable to find the root for the component ${options.name}`);
- }
+ let innerHTML: string | null = null;
+ let hydrate: Awaited>;
- let innerHTML: string | null = null;
- let fragment = roots[0].querySelector(`astro-fragment`);
- if (fragment == null && roots[0].hasAttribute('tmpl')) {
- // If there is no child fragment, check to see if there is a template.
- // This happens if children were passed but the client component did not render any.
- let template = roots[0].querySelector(`template[data-astro-template]`);
- if (template) {
- innerHTML = template.innerHTML;
- template.remove();
+ async function idle() {
+ listen(idle)
+ const cb = async () => {
+ const roots = document.querySelectorAll(`astro-root[ssr][uid="${astroId}"]`);
+ if (roots.length === 0) return;
+ if (typeof innerHTML !== 'string') {
+ let fragment = roots[0].querySelector(`astro-fragment`);
+ if (fragment == null && roots[0].hasAttribute('tmpl')) {
+ // If there is no child fragment, check to see if there is a template.
+ // This happens if children were passed but the client component did not render any.
+ let template = roots[0].querySelector(`template[data-astro-template]`);
+ if (template) {
+ innerHTML = template.innerHTML;
+ template.remove();
+ }
+ } else if (fragment) {
+ innerHTML = fragment.innerHTML;
+ }
}
- } else if (fragment) {
- innerHTML = fragment.innerHTML;
- }
- const hydrate = await getHydrateCallback();
+ if (!hydrate) {
+ hydrate = await getHydrateCallback();
+ }
+ for (const root of roots) {
+ if (root.parentElement?.closest('astro-root[ssr]')) continue;
+ await hydrate(root, innerHTML);
+ root.removeAttribute('ssr');
+ }
+ notify();
+ };
- for (const root of roots) {
- hydrate(root, innerHTML);
+ if ('requestIdleCallback' in window) {
+ (window as any).requestIdleCallback(cb);
+ } else {
+ setTimeout(cb, 200);
}
- };
-
- if ('requestIdleCallback' in window) {
- (window as any).requestIdleCallback(cb);
- } else {
- setTimeout(cb, 200);
}
+ idle();
}
diff --git a/packages/astro/src/runtime/client/load.ts b/packages/astro/src/runtime/client/load.ts
index d969b4061b6e..80a1f4d51534 100644
--- a/packages/astro/src/runtime/client/load.ts
+++ b/packages/astro/src/runtime/client/load.ts
@@ -1,36 +1,44 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
+import { notify, listen } from './events';
/**
- * Hydrate this component immediately!
+ * Hydrate this component immediately
*/
export default async function onLoad(
astroId: string,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
- const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
- if (roots.length === 0) {
- throw new Error(`Unable to find the root for the component ${options.name}`);
- }
-
let innerHTML: string | null = null;
- let fragment = roots[0].querySelector(`astro-fragment`);
- if (fragment == null && roots[0].hasAttribute('tmpl')) {
- // If there is no child fragment, check to see if there is a template.
- // This happens if children were passed but the client component did not render any.
- let template = roots[0].querySelector(`template[data-astro-template]`);
- if (template) {
- innerHTML = template.innerHTML;
- template.remove();
- }
- } else if (fragment) {
- innerHTML = fragment.innerHTML;
- }
+ let hydrate: Awaited>;
- //const innerHTML = roots[0].querySelector(`astro-fragment`)?.innerHTML ?? null;
- const hydrate = await getHydrateCallback();
-
- for (const root of roots) {
- hydrate(root, innerHTML);
+ async function load() {
+ listen(load);
+ const roots = document.querySelectorAll(`astro-root[ssr][uid="${astroId}"]`);
+ if (roots.length === 0) return;
+ if (typeof innerHTML !== 'string') {
+ let fragment = roots[0].querySelector(`astro-fragment`);
+ if (fragment == null && roots[0].hasAttribute('tmpl')) {
+ // If there is no child fragment, check to see if there is a template.
+ // This happens if children were passed but the client component did not render any.
+ let template = roots[0].querySelector(`template[data-astro-template]`);
+ if (template) {
+ innerHTML = template.innerHTML;
+ template.remove();
+ }
+ } else if (fragment) {
+ innerHTML = fragment.innerHTML;
+ }
+ }
+ if (!hydrate) {
+ hydrate = await getHydrateCallback();
+ }
+ for (const root of roots) {
+ if (root.parentElement?.closest('astro-root[ssr]')) continue;
+ await hydrate(root, innerHTML);
+ root.removeAttribute('ssr');
+ }
+ notify();
}
+ load();
}
diff --git a/packages/astro/src/runtime/client/media.ts b/packages/astro/src/runtime/client/media.ts
index edaa9a433864..cd44158790ab 100644
--- a/packages/astro/src/runtime/client/media.ts
+++ b/packages/astro/src/runtime/client/media.ts
@@ -1,45 +1,55 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
+import { notify, listen } from './events';
/**
- * Hydrate this component when a matching media query is found!
+ * Hydrate this component when a matching media query is found
*/
export default async function onMedia(
astroId: string,
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
- const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
- if (roots.length === 0) {
- throw new Error(`Unable to find the root for the component ${options.name}`);
- }
-
let innerHTML: string | null = null;
- let fragment = roots[0].querySelector(`astro-fragment`);
- if (fragment == null && roots[0].hasAttribute('tmpl')) {
- // If there is no child fragment, check to see if there is a template.
- // This happens if children were passed but the client component did not render any.
- let template = roots[0].querySelector(`template[data-astro-template]`);
- if (template) {
- innerHTML = template.innerHTML;
- template.remove();
- }
- } else if (fragment) {
- innerHTML = fragment.innerHTML;
- }
+ let hydrate: Awaited>;
- const cb = async () => {
- const hydrate = await getHydrateCallback();
- for (const root of roots) {
- hydrate(root, innerHTML);
- }
- };
+ async function media() {
+ listen(media)
+ const cb = async () => {
+ const roots = document.querySelectorAll(`astro-root[ssr][uid="${astroId}"]`);
+ if (roots.length === 0) return;
+ if (typeof innerHTML !== 'string') {
+ let fragment = roots[0].querySelector(`astro-fragment`);
+ if (fragment == null && roots[0].hasAttribute('tmpl')) {
+ // If there is no child fragment, check to see if there is a template.
+ // This happens if children were passed but the client component did not render any.
+ let template = roots[0].querySelector(`template[data-astro-template]`);
+ if (template) {
+ innerHTML = template.innerHTML;
+ template.remove();
+ }
+ } else if (fragment) {
+ innerHTML = fragment.innerHTML;
+ }
+ }
+ if (!hydrate) {
+ hydrate = await getHydrateCallback();
+ }
+ for (const root of roots) {
+ if (root.parentElement?.closest('astro-root[ssr]')) continue;
+ await hydrate(root, innerHTML);
+ root.removeAttribute('ssr');
+ }
+ notify();
+ };
- if (options.value) {
- const mql = matchMedia(options.value);
- if (mql.matches) {
- cb();
- } else {
- mql.addEventListener('change', cb, { once: true });
+ if (options.value) {
+ const mql = matchMedia(options.value);
+ if (mql.matches) {
+ cb();
+ } else {
+ mql.addEventListener('change', cb, { once: true });
+ }
}
}
+ media();
}
diff --git a/packages/astro/src/runtime/client/only.ts b/packages/astro/src/runtime/client/only.ts
index 04937c6084fd..65ea02bd7649 100644
--- a/packages/astro/src/runtime/client/only.ts
+++ b/packages/astro/src/runtime/client/only.ts
@@ -1,4 +1,5 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
+import { listen, notify } from './events';
/**
* Hydrate this component only on the client
@@ -8,27 +9,36 @@ export default async function onOnly(
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
- const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
- if (roots.length === 0) {
- throw new Error(`Unable to find the root for the component ${options.name}`);
- }
-
let innerHTML: string | null = null;
- let fragment = roots[0].querySelector(`astro-fragment`);
- if (fragment == null && roots[0].hasAttribute('tmpl')) {
- // If there is no child fragment, check to see if there is a template.
- // This happens if children were passed but the client component did not render any.
- let template = roots[0].querySelector(`template[data-astro-template]`);
- if (template) {
- innerHTML = template.innerHTML;
- template.remove();
- }
- } else if (fragment) {
- innerHTML = fragment.innerHTML;
- }
- const hydrate = await getHydrateCallback();
+ let hydrate: Awaited>;
- for (const root of roots) {
- hydrate(root, innerHTML);
+ async function only() {
+ listen(only);
+ const roots = document.querySelectorAll(`astro-root[ssr][uid="${astroId}"]`);
+ if (roots.length === 0) return;
+ if (typeof innerHTML !== 'string') {
+ let fragment = roots[0].querySelector(`astro-fragment`);
+ if (fragment == null && roots[0].hasAttribute('tmpl')) {
+ // If there is no child fragment, check to see if there is a template.
+ // This happens if children were passed but the client component did not render any.
+ let template = roots[0].querySelector(`template[data-astro-template]`);
+ if (template) {
+ innerHTML = template.innerHTML;
+ template.remove();
+ }
+ } else if (fragment) {
+ innerHTML = fragment.innerHTML;
+ }
+ }
+ if (!hydrate) {
+ hydrate = await getHydrateCallback();
+ }
+ for (const root of roots) {
+ if (root.parentElement?.closest('astro-root[ssr]')) continue;
+ await hydrate(root, innerHTML);
+ root.removeAttribute('ssr');
+ }
+ notify();
}
+ only();
}
diff --git a/packages/astro/src/runtime/client/visible.ts b/packages/astro/src/runtime/client/visible.ts
index e9c3e3310963..9202d8c7260f 100644
--- a/packages/astro/src/runtime/client/visible.ts
+++ b/packages/astro/src/runtime/client/visible.ts
@@ -1,7 +1,8 @@
import type { GetHydrateCallback, HydrateOptions } from '../../@types/astro';
+import { notify, listen } from './events';
/**
- * Hydrate this component when one of it's children becomes visible!
+ * Hydrate this component when one of it's children becomes visible
* We target the children because `astro-root` is set to `display: contents`
* which doesn't work with IntersectionObserver
*/
@@ -10,46 +11,61 @@ export default async function onVisible(
options: HydrateOptions,
getHydrateCallback: GetHydrateCallback
) {
- const roots = document.querySelectorAll(`astro-root[uid="${astroId}"]`);
- if (roots.length === 0) {
- throw new Error(`Unable to find the root for the component ${options.name}`);
- }
-
+ let io: IntersectionObserver;
let innerHTML: string | null = null;
- let fragment = roots[0].querySelector(`astro-fragment`);
- if (fragment == null && roots[0].hasAttribute('tmpl')) {
- // If there is no child fragment, check to see if there is a template.
- // This happens if children were passed but the client component did not render any.
- let template = roots[0].querySelector(`template[data-astro-template]`);
- if (template) {
- innerHTML = template.innerHTML;
- template.remove();
- }
- } else if (fragment) {
- innerHTML = fragment.innerHTML;
- }
+ let hydrate: Awaited>;
- const cb = async () => {
- const hydrate = await getHydrateCallback();
- for (const root of roots) {
- hydrate(root, innerHTML);
- }
- };
+ async function visible() {
+ listen(visible)
+ const roots = document.querySelectorAll(`astro-root[ssr][uid="${astroId}"]`);
+ const cb = async () => {
+ if (roots.length === 0) return;
+ if (typeof innerHTML !== 'string') {
+ let fragment = roots[0].querySelector(`astro-fragment`);
+ if (fragment == null && roots[0].hasAttribute('tmpl')) {
+ // If there is no child fragment, check to see if there is a template.
+ // This happens if children were passed but the client component did not render any.
+ let template = roots[0].querySelector(`template[data-astro-template]`);
+ if (template) {
+ innerHTML = template.innerHTML;
+ template.remove();
+ }
+ } else if (fragment) {
+ innerHTML = fragment.innerHTML;
+ }
+ }
+ if (!hydrate) {
+ hydrate = await getHydrateCallback();
+ }
+ for (const root of roots) {
+ if (root.parentElement?.closest('astro-root[ssr]')) continue;
+ await hydrate(root, innerHTML);
+ root.removeAttribute('ssr');
+ }
+ notify();
+ };
- const io = new IntersectionObserver((entries) => {
- for (const entry of entries) {
- if (!entry.isIntersecting) continue;
- // As soon as we hydrate, disconnect this IntersectionObserver for every `astro-root`
+ if (io) {
io.disconnect();
- cb();
- break; // break loop on first match
}
- });
- for (const root of roots) {
- for (let i = 0; i < root.children.length; i++) {
- const child = root.children[i];
- io.observe(child);
+ io = new IntersectionObserver((entries) => {
+ for (const entry of entries) {
+ if (!entry.isIntersecting) continue;
+ // As soon as we hydrate, disconnect this IntersectionObserver for every `astro-root`
+ io.disconnect();
+ cb();
+ break; // break loop on first match
+ }
+ });
+
+ for (const root of roots) {
+ for (let i = 0; i < root.children.length; i++) {
+ const child = root.children[i];
+ io.observe(child);
+ }
}
}
+
+ visible();
}
diff --git a/packages/astro/src/runtime/server/index.ts b/packages/astro/src/runtime/server/index.ts
index 762e764ce7de..e3c2806422dc 100644
--- a/packages/astro/src/runtime/server/index.ts
+++ b/packages/astro/src/runtime/server/index.ts
@@ -330,7 +330,7 @@ If you're still stuck, please open an issue on GitHub or join us at https://astr
const template = needsAstroTemplate ? `${children}` : '';
return markHTMLString(
- `${
+ `${
html ?? ''
}${template}`
);
diff --git a/packages/integrations/preact/client.js b/packages/integrations/preact/client.js
index 5ece5ddb2b81..6ff40d2ae1c5 100644
--- a/packages/integrations/preact/client.js
+++ b/packages/integrations/preact/client.js
@@ -1,8 +1,10 @@
import { h, render } from 'preact';
import StaticHtml from './static-html.js';
-export default (element) => (Component, props, children) =>
+export default (element) => (Component, props, children) => {
+ if (!element.hasAttribute('ssr')) return;
render(
h(Component, props, children != null ? h(StaticHtml, { value: children }) : children),
element
);
+}
diff --git a/packages/integrations/react/client.js b/packages/integrations/react/client.js
index fc952522be98..2828d2cbe3ac 100644
--- a/packages/integrations/react/client.js
+++ b/packages/integrations/react/client.js
@@ -2,13 +2,27 @@ import { createElement } from 'react';
import { createRoot, hydrateRoot } from 'react-dom/client';
import StaticHtml from './static-html.js';
+function isAlreadyHydrated(element) {
+ for (const key in element) {
+ if (key.startsWith('__reactContainer')) {
+ return key;
+ }
+ }
+}
+
export default (element) =>
(Component, props, children, { client }) => {
+ if (!element.hasAttribute('ssr')) return;
const componentEl = createElement(
Component,
props,
children != null ? createElement(StaticHtml, { value: children }) : children
);
+ const rootKey = isAlreadyHydrated(element);
+ // HACK: delete internal react marker for nested components to suppress agressive warnings
+ if (rootKey) {
+ delete element[rootKey];
+ }
if (client === 'only') {
return createRoot(element).render(componentEl);
}
diff --git a/packages/integrations/solid/client.js b/packages/integrations/solid/client.js
index d31c5cecd1ec..005f3c980376 100644
--- a/packages/integrations/solid/client.js
+++ b/packages/integrations/solid/client.js
@@ -1,21 +1,27 @@
import { sharedConfig } from 'solid-js';
-import { hydrate, createComponent } from 'solid-js/web';
+import { hydrate, render, createComponent } from 'solid-js/web';
-export default (element) => (Component, props, childHTML) => {
+export default (element) => (Component, props, childHTML, { client }) => {
// Prepare global object expected by Solid's hydration logic
if (!window._$HY) {
window._$HY = { events: [], completed: new WeakSet(), r: {} };
}
+ if (!element.hasAttribute('ssr')) return;
+
+ const fn = client === 'only' ? render : hydrate;
+
// Perform actual hydration
let children;
- hydrate(
+ fn(
() =>
createComponent(Component, {
...props,
get children() {
if (childHTML != null) {
// hydrating
- if (sharedConfig.context) children = element.querySelector('astro-fragment');
+ if (sharedConfig.context) {
+ children = element.querySelector('astro-fragment');
+ }
if (children == null) {
children = document.createElement('astro-fragment');
diff --git a/packages/integrations/svelte/client.js b/packages/integrations/svelte/client.js
index c10c7afa01b0..3f401b544f94 100644
--- a/packages/integrations/svelte/client.js
+++ b/packages/integrations/svelte/client.js
@@ -1,13 +1,14 @@
import SvelteWrapper from './Wrapper.svelte';
export default (target) => {
- return (component, props, children) => {
+ return (component, props, children, { client }) => {
+ if (!target.hasAttribute('ssr')) return;
delete props['class'];
try {
new SvelteWrapper({
target,
props: { __astro_component: component, __astro_children: children, ...props },
- hydrate: true,
+ hydrate: client !== 'only',
});
} catch (e) {}
};
diff --git a/packages/integrations/vue/client.js b/packages/integrations/vue/client.js
index 0ba4e81063ab..4832a984760b 100644
--- a/packages/integrations/vue/client.js
+++ b/packages/integrations/vue/client.js
@@ -1,14 +1,21 @@
-import { h, createSSRApp } from 'vue';
+import { h, createSSRApp, createApp } from 'vue';
import StaticHtml from './static-html.js';
-export default (element) => (Component, props, children) => {
+export default (element) => (Component, props, children, { client }) => {
delete props['class'];
+ if (!element.hasAttribute('ssr')) return;
+
// Expose name on host component for Vue devtools
const name = Component.name ? `${Component.name} Host` : undefined;
const slots = {};
if (children != null) {
slots.default = () => h(StaticHtml, { value: children });
}
- const app = createSSRApp({ name, render: () => h(Component, props, slots) });
- app.mount(element, true);
+ if (client === 'only') {
+ const app = createApp({ name, render: () => h(Component, props, slots) });
+ app.mount(element, false);
+ } else {
+ const app = createSSRApp({ name, render: () => h(Component, props, slots) });
+ app.mount(element, true);
+ }
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0091f0eca7c3..3909975671b9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -667,6 +667,35 @@ importers:
dependencies:
astro: link:../../..
+ packages/astro/e2e/fixtures/client-only:
+ specifiers:
+ '@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
+ 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
+ dependencies:
+ preact: 10.7.2
+ react: 18.1.0
+ react-dom: 18.1.0_react@18.1.0
+ solid-js: 1.4.2
+ svelte: 3.48.0
+ vue: 3.2.36
+ devDependencies:
+ '@astrojs/preact': link:../../../../integrations/preact
+ '@astrojs/react': link:../../../../integrations/react
+ '@astrojs/solid-js': link:../../../../integrations/solid
+ '@astrojs/svelte': link:../../../../integrations/svelte
+ '@astrojs/vue': link:../../../../integrations/vue
+ astro: link:../../..
+
packages/astro/e2e/fixtures/lit-component:
specifiers:
'@astrojs/lit': workspace:*
@@ -714,6 +743,151 @@ importers:
'@astrojs/vue': link:../../../../integrations/vue
astro: link:../../..
+ packages/astro/e2e/fixtures/nested-in-preact:
+ specifiers:
+ '@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
+ 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
+ dependencies:
+ preact: 10.7.2
+ react: 18.1.0
+ react-dom: 18.1.0_react@18.1.0
+ solid-js: 1.4.2
+ svelte: 3.48.0
+ vue: 3.2.36
+ devDependencies:
+ '@astrojs/preact': link:../../../../integrations/preact
+ '@astrojs/react': link:../../../../integrations/react
+ '@astrojs/solid-js': link:../../../../integrations/solid
+ '@astrojs/svelte': link:../../../../integrations/svelte
+ '@astrojs/vue': link:../../../../integrations/vue
+ astro: link:../../..
+
+ packages/astro/e2e/fixtures/nested-in-react:
+ specifiers:
+ '@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
+ 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
+ dependencies:
+ preact: 10.7.2
+ react: 18.1.0
+ react-dom: 18.1.0_react@18.1.0
+ solid-js: 1.4.2
+ svelte: 3.48.0
+ vue: 3.2.36
+ devDependencies:
+ '@astrojs/preact': link:../../../../integrations/preact
+ '@astrojs/react': link:../../../../integrations/react
+ '@astrojs/solid-js': link:../../../../integrations/solid
+ '@astrojs/svelte': link:../../../../integrations/svelte
+ '@astrojs/vue': link:../../../../integrations/vue
+ astro: link:../../..
+
+ packages/astro/e2e/fixtures/nested-in-solid:
+ specifiers:
+ '@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
+ 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
+ dependencies:
+ preact: 10.7.2
+ react: 18.1.0
+ react-dom: 18.1.0_react@18.1.0
+ solid-js: 1.4.2
+ svelte: 3.48.0
+ vue: 3.2.36
+ devDependencies:
+ '@astrojs/preact': link:../../../../integrations/preact
+ '@astrojs/react': link:../../../../integrations/react
+ '@astrojs/solid-js': link:../../../../integrations/solid
+ '@astrojs/svelte': link:../../../../integrations/svelte
+ '@astrojs/vue': link:../../../../integrations/vue
+ astro: link:../../..
+
+ packages/astro/e2e/fixtures/nested-in-svelte:
+ specifiers:
+ '@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
+ 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
+ dependencies:
+ preact: 10.7.2
+ react: 18.1.0
+ react-dom: 18.1.0_react@18.1.0
+ solid-js: 1.4.2
+ svelte: 3.48.0
+ vue: 3.2.36
+ devDependencies:
+ '@astrojs/preact': link:../../../../integrations/preact
+ '@astrojs/react': link:../../../../integrations/react
+ '@astrojs/solid-js': link:../../../../integrations/solid
+ '@astrojs/svelte': link:../../../../integrations/svelte
+ '@astrojs/vue': link:../../../../integrations/vue
+ astro: link:../../..
+
+ packages/astro/e2e/fixtures/nested-in-vue:
+ specifiers:
+ '@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
+ 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
+ dependencies:
+ preact: 10.7.2
+ react: 18.1.0
+ react-dom: 18.1.0_react@18.1.0
+ solid-js: 1.4.2
+ svelte: 3.48.0
+ vue: 3.2.36
+ devDependencies:
+ '@astrojs/preact': link:../../../../integrations/preact
+ '@astrojs/react': link:../../../../integrations/react
+ '@astrojs/solid-js': link:../../../../integrations/solid
+ '@astrojs/svelte': link:../../../../integrations/svelte
+ '@astrojs/vue': link:../../../../integrations/vue
+ astro: link:../../..
+
packages/astro/e2e/fixtures/nested-styles:
specifiers:
astro: workspace:*
@@ -7882,6 +8056,11 @@ packages:
/debug/3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
dependencies:
ms: 2.1.3
dev: false
@@ -10784,6 +10963,8 @@ packages:
debug: 3.2.7
iconv-lite: 0.4.24
sax: 1.2.4
+ transitivePeerDependencies:
+ - supports-color
dev: false
/netmask/2.0.2:
@@ -10867,6 +11048,8 @@ packages:
rimraf: 2.7.1
semver: 5.7.1
tar: 4.4.19
+ transitivePeerDependencies:
+ - supports-color
dev: false
/node-releases/2.0.5: