diff --git a/.changeset/smooth-nails-remain.md b/.changeset/smooth-nails-remain.md new file mode 100644 index 000000000000..185e12f7eea0 --- /dev/null +++ b/.changeset/smooth-nails-remain.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes race condition between directives being defined diff --git a/packages/astro/e2e/fixtures/hydration-race/astro.config.mjs b/packages/astro/e2e/fixtures/hydration-race/astro.config.mjs new file mode 100644 index 000000000000..c9662ed094ff --- /dev/null +++ b/packages/astro/e2e/fixtures/hydration-race/astro.config.mjs @@ -0,0 +1,5 @@ +import preact from '@astrojs/preact'; + +export default { + integrations: [preact()] +}; diff --git a/packages/astro/e2e/fixtures/hydration-race/package.json b/packages/astro/e2e/fixtures/hydration-race/package.json new file mode 100644 index 000000000000..ee28e25f93f5 --- /dev/null +++ b/packages/astro/e2e/fixtures/hydration-race/package.json @@ -0,0 +1,13 @@ +{ + "name": "@e2e/hydration-race", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "astro dev", + "build": "astro build" + }, + "dependencies": { + "astro": "workspace:*", + "@astrojs/preact": "workspace:*" + } +} diff --git a/packages/astro/e2e/fixtures/hydration-race/src/components/Deeper.astro b/packages/astro/e2e/fixtures/hydration-race/src/components/Deeper.astro new file mode 100644 index 000000000000..76945dff9527 --- /dev/null +++ b/packages/astro/e2e/fixtures/hydration-race/src/components/Deeper.astro @@ -0,0 +1,9 @@ +--- +import One from './One.jsx'; +--- + +
+ Before one + +
+ diff --git a/packages/astro/e2e/fixtures/hydration-race/src/components/Layout.astro b/packages/astro/e2e/fixtures/hydration-race/src/components/Layout.astro new file mode 100644 index 000000000000..8ee14c08f4a8 --- /dev/null +++ b/packages/astro/e2e/fixtures/hydration-race/src/components/Layout.astro @@ -0,0 +1,6 @@ +--- +import Wrapper from './Wrapper.astro'; +--- + + + diff --git a/packages/astro/e2e/fixtures/hydration-race/src/components/One.jsx b/packages/astro/e2e/fixtures/hydration-race/src/components/One.jsx new file mode 100644 index 000000000000..7b76c8f6ccc6 --- /dev/null +++ b/packages/astro/e2e/fixtures/hydration-race/src/components/One.jsx @@ -0,0 +1,8 @@ +import { h } from 'preact'; + +export default function({ name }) { + const inTheClient = import.meta.env.SSR ? '' : ' in the client' + return ( +
Hello {name}{inTheClient}
+ ); +} diff --git a/packages/astro/e2e/fixtures/hydration-race/src/components/Wrapper.astro b/packages/astro/e2e/fixtures/hydration-race/src/components/Wrapper.astro new file mode 100644 index 000000000000..a3de9b8ecc6b --- /dev/null +++ b/packages/astro/e2e/fixtures/hydration-race/src/components/Wrapper.astro @@ -0,0 +1,8 @@ +--- +import One from './One.jsx'; +import Deeper from './Deeper.astro'; +--- + + + + diff --git a/packages/astro/e2e/fixtures/hydration-race/src/pages/slot.astro b/packages/astro/e2e/fixtures/hydration-race/src/pages/slot.astro new file mode 100644 index 000000000000..f509a2bf7b75 --- /dev/null +++ b/packages/astro/e2e/fixtures/hydration-race/src/pages/slot.astro @@ -0,0 +1,15 @@ +--- +import Layout from '../components/Layout.astro'; +import One from '../components/One.jsx'; +--- + + + Testing + + + + + + + + diff --git a/packages/astro/e2e/hydration-race.test.js b/packages/astro/e2e/hydration-race.test.js new file mode 100644 index 000000000000..e516adfc8423 --- /dev/null +++ b/packages/astro/e2e/hydration-race.test.js @@ -0,0 +1,36 @@ +import { expect } from '@playwright/test'; +import { testFactory } from './test-utils.js'; + +const test = testFactory({ + root: './fixtures/hydration-race/', +}); + +let devServer; + +test.beforeEach(async ({ astro }) => { + devServer = await astro.startDevServer(); +}); + +test.afterEach(async () => { + await devServer.stop(); +}); + +test.describe('Hydration race', () => { + test('Islands inside of slots hydrate', async ({ page, astro }) => { + await page.goto('/slot'); + + const html = await page.content(); + + const one = page.locator('#one'); + await expect(one, 'updated text').toHaveText('Hello One in the client'); + + const two = page.locator('#two'); + await expect(two, 'updated text').toHaveText('Hello Two in the client'); + + const three = page.locator('#three'); + await expect(three, 'updated text').toHaveText('Hello Three in the client'); + + const four = page.locator('#four'); + await expect(four, 'updated text').toHaveText('Hello Four in the client'); + }); +}); diff --git a/packages/astro/src/runtime/client/idle.ts b/packages/astro/src/runtime/client/idle.ts index 90090731e09d..4af28bd46180 100644 --- a/packages/astro/src/runtime/client/idle.ts +++ b/packages/astro/src/runtime/client/idle.ts @@ -10,3 +10,4 @@ setTimeout(cb, 200); } }; +window.dispatchEvent(new Event('astro:idle')); diff --git a/packages/astro/src/runtime/client/load.ts b/packages/astro/src/runtime/client/load.ts index 9081799ed622..426c6c68aae9 100644 --- a/packages/astro/src/runtime/client/load.ts +++ b/packages/astro/src/runtime/client/load.ts @@ -4,3 +4,4 @@ await hydrate(); })(); }; +window.dispatchEvent(new Event('astro:load')); diff --git a/packages/astro/src/runtime/client/media.ts b/packages/astro/src/runtime/client/media.ts index 0bf11e448df5..c180d396a1e8 100644 --- a/packages/astro/src/runtime/client/media.ts +++ b/packages/astro/src/runtime/client/media.ts @@ -16,3 +16,4 @@ } } }; +window.dispatchEvent(new Event('astro:media')); diff --git a/packages/astro/src/runtime/client/only.ts b/packages/astro/src/runtime/client/only.ts index 78832e9dcbc8..e8272edbb429 100644 --- a/packages/astro/src/runtime/client/only.ts +++ b/packages/astro/src/runtime/client/only.ts @@ -7,3 +7,4 @@ await hydrate(); })(); }; +window.dispatchEvent(new Event('astro:only')); diff --git a/packages/astro/src/runtime/client/visible.ts b/packages/astro/src/runtime/client/visible.ts index eac73115ff3c..28975040cf0a 100644 --- a/packages/astro/src/runtime/client/visible.ts +++ b/packages/astro/src/runtime/client/visible.ts @@ -24,3 +24,4 @@ io.observe(child); } }; +window.dispatchEvent(new Event('astro:visible')); diff --git a/packages/astro/src/runtime/server/astro-island.ts b/packages/astro/src/runtime/server/astro-island.ts index 96b1b8b6e0b8..07ea982d3dc8 100644 --- a/packages/astro/src/runtime/server/astro-island.ts +++ b/packages/astro/src/runtime/server/astro-island.ts @@ -5,7 +5,7 @@ type directiveAstroKeys = 'load' | 'idle' | 'visible' | 'media' | 'only'; declare const Astro: { - [k in directiveAstroKeys]: ( + [k in directiveAstroKeys]?: ( fn: () => Promise<() => void>, opts: Record, root: HTMLElement @@ -57,8 +57,16 @@ declare const Astro: { async childrenConnectedCallback() { window.addEventListener('astro:hydrate', this.hydrate); await import(this.getAttribute('before-hydration-url')!); + this.start(); + } + start() { const opts = JSON.parse(this.getAttribute('opts')!) as Record; - Astro[this.getAttribute('client') as directiveAstroKeys]( + const directive = this.getAttribute('client') as directiveAstroKeys; + if(Astro[directive] === undefined) { + window.addEventListener(`astro:${directive}`, () => this.start(), { once: true }); + return; + } + Astro[directive]!( async () => { const rendererUrl = this.getAttribute('renderer-url'); const [componentModule, { default: hydrator }] = await Promise.all([ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de355297a331..9a449db84ae1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -660,6 +660,14 @@ importers: react: 18.2.0 react-dom: 18.2.0_react@18.2.0 + packages/astro/e2e/fixtures/hydration-race: + specifiers: + '@astrojs/preact': workspace:* + astro: workspace:* + dependencies: + '@astrojs/preact': link:../../../../integrations/preact + astro: link:../../.. + packages/astro/e2e/fixtures/invalidate-script-deps: specifiers: astro: workspace:*