diff --git a/packages/astro/e2e/fixtures/server-islands/astro.config.mjs b/packages/astro/e2e/fixtures/server-islands/astro.config.mjs new file mode 100644 index 000000000000..34e1ec5c6811 --- /dev/null +++ b/packages/astro/e2e/fixtures/server-islands/astro.config.mjs @@ -0,0 +1,11 @@ +import mdx from '@astrojs/mdx'; +import react from '@astrojs/react'; +import { defineConfig } from 'astro/config'; +import nodejs from '@astrojs/node'; + +// https://astro.build/config +export default defineConfig({ + output: 'hybrid', + adapter: nodejs({ mode: 'standalone' }), + integrations: [react(), mdx()], +}); diff --git a/packages/astro/e2e/fixtures/server-islands/package.json b/packages/astro/e2e/fixtures/server-islands/package.json new file mode 100644 index 000000000000..9958ee287857 --- /dev/null +++ b/packages/astro/e2e/fixtures/server-islands/package.json @@ -0,0 +1,16 @@ +{ + "name": "@e2e/server-islands", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "astro dev" + }, + "dependencies": { + "@astrojs/react": "workspace:*", + "astro": "workspace:*", + "@astrojs/mdx": "workspace:*", + "@astrojs/node": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.1" + } +} diff --git a/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro b/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro new file mode 100644 index 000000000000..a6d5fa3dc7f0 --- /dev/null +++ b/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro @@ -0,0 +1,3 @@ +--- +--- +

I am an island

diff --git a/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro b/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro new file mode 100644 index 000000000000..a620e7169cb2 --- /dev/null +++ b/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +import Island from '../components/Island.astro'; +--- + + + + + + + + + diff --git a/packages/astro/e2e/fixtures/server-islands/src/pages/mdx.mdx b/packages/astro/e2e/fixtures/server-islands/src/pages/mdx.mdx new file mode 100644 index 000000000000..1a0a0ac6f3f9 --- /dev/null +++ b/packages/astro/e2e/fixtures/server-islands/src/pages/mdx.mdx @@ -0,0 +1,3 @@ +import Island from '../components/Island.astro'; + + diff --git a/packages/astro/e2e/server-islands.test.js b/packages/astro/e2e/server-islands.test.js new file mode 100644 index 000000000000..0255b9f0563b --- /dev/null +++ b/packages/astro/e2e/server-islands.test.js @@ -0,0 +1,59 @@ +import { expect } from '@playwright/test'; +import { testFactory } from './test-utils.js'; + +const test = testFactory({ root: './fixtures/server-islands/' }); + +test.describe('Server islands', () => { + test.describe('Development', () => { + let devServer; + + test.beforeAll(async ({ astro }) => { + devServer = await astro.startDevServer(); + }); + + test.afterAll(async () => { + await devServer.stop(); + }); + + test('Load content from the server', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/')); + let el = page.locator('#island'); + + await expect(el, 'element rendered').toBeVisible(); + await expect(el, 'should have content').toHaveText('I am an island'); + }); + + test('Can be in an MDX file', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/mdx')); + let el = page.locator('#island'); + + await expect(el, 'element rendered').toBeVisible(); + await expect(el, 'should have content').toHaveText('I am an island'); + }); + }); + + test.describe('Production', () => { + let previewServer; + + test.beforeAll(async ({ astro }) => { + // Playwright's Node version doesn't have these functions, so stub them. + process.stdout.clearLine = () => {}; + process.stdout.cursorTo = () => {}; + await astro.build(); + previewServer = await astro.preview(); + }); + + test.afterAll(async () => { + await previewServer.stop(); + }); + + test('Only one component in prod', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/')); + + let el = page.locator('#island'); + + await expect(el, 'element rendered').toBeVisible(); + await expect(el, 'should have content').toHaveText('I am an island'); + }); + }); +}); diff --git a/packages/astro/src/core/server-islands/endpoint.ts b/packages/astro/src/core/server-islands/endpoint.ts index c9d3f5866d30..35063b0822b1 100644 --- a/packages/astro/src/core/server-islands/endpoint.ts +++ b/packages/astro/src/core/server-islands/endpoint.ts @@ -69,6 +69,7 @@ export function createEndpoint(manifest: SSRManifest) { const instance: ComponentInstance = { default: page, + partial: true, }; return instance; diff --git a/packages/astro/test/fixtures/server-islands/hybrid/astro.config.mjs b/packages/astro/test/fixtures/server-islands/hybrid/astro.config.mjs new file mode 100644 index 000000000000..8a47dbb0292e --- /dev/null +++ b/packages/astro/test/fixtures/server-islands/hybrid/astro.config.mjs @@ -0,0 +1,10 @@ +import svelte from '@astrojs/svelte'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + output: 'hybrid', + integrations: [ + svelte() + ] +}); + diff --git a/packages/astro/test/fixtures/server-islands/hybrid/package.json b/packages/astro/test/fixtures/server-islands/hybrid/package.json new file mode 100644 index 000000000000..fdb447b0e071 --- /dev/null +++ b/packages/astro/test/fixtures/server-islands/hybrid/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/server-islands-hybrid", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/svelte": "workspace:*", + "astro": "workspace:*", + "svelte": "^4.2.18" + } +} diff --git a/packages/astro/test/fixtures/server-islands/hybrid/src/components/Island.astro b/packages/astro/test/fixtures/server-islands/hybrid/src/components/Island.astro new file mode 100644 index 000000000000..49a5a87ae0d5 --- /dev/null +++ b/packages/astro/test/fixtures/server-islands/hybrid/src/components/Island.astro @@ -0,0 +1,4 @@ +--- + +--- +

I'm an island

diff --git a/packages/astro/test/fixtures/server-islands/hybrid/src/pages/index.astro b/packages/astro/test/fixtures/server-islands/hybrid/src/pages/index.astro new file mode 100644 index 000000000000..d42973294e6d --- /dev/null +++ b/packages/astro/test/fixtures/server-islands/hybrid/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +import Island from '../components/Island.astro'; +--- + + + Testing + + +

Testing

+ + + diff --git a/packages/astro/test/fixtures/server-islands/ssr/astro.config.mjs b/packages/astro/test/fixtures/server-islands/ssr/astro.config.mjs new file mode 100644 index 000000000000..9d52f7a5fd09 --- /dev/null +++ b/packages/astro/test/fixtures/server-islands/ssr/astro.config.mjs @@ -0,0 +1,10 @@ +import svelte from '@astrojs/svelte'; +import { defineConfig } from 'astro/config'; + +export default defineConfig({ + output: 'server', + integrations: [ + svelte() + ] +}); + diff --git a/packages/astro/test/fixtures/server-islands/ssr/package.json b/packages/astro/test/fixtures/server-islands/ssr/package.json new file mode 100644 index 000000000000..fa6e000dda49 --- /dev/null +++ b/packages/astro/test/fixtures/server-islands/ssr/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/server-islands-ssr", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/svelte": "workspace:*", + "astro": "workspace:*", + "svelte": "^4.2.18" + } +} diff --git a/packages/astro/test/fixtures/server-islands/ssr/src/components/Island.astro b/packages/astro/test/fixtures/server-islands/ssr/src/components/Island.astro new file mode 100644 index 000000000000..49a5a87ae0d5 --- /dev/null +++ b/packages/astro/test/fixtures/server-islands/ssr/src/components/Island.astro @@ -0,0 +1,4 @@ +--- + +--- +

I'm an island

diff --git a/packages/astro/test/fixtures/server-islands/ssr/src/pages/index.astro b/packages/astro/test/fixtures/server-islands/ssr/src/pages/index.astro new file mode 100644 index 000000000000..d42973294e6d --- /dev/null +++ b/packages/astro/test/fixtures/server-islands/ssr/src/pages/index.astro @@ -0,0 +1,12 @@ +--- +import Island from '../components/Island.astro'; +--- + + + Testing + + +

Testing

+ + + diff --git a/packages/astro/test/server-islands.test.js b/packages/astro/test/server-islands.test.js new file mode 100644 index 000000000000..60fece1e46d4 --- /dev/null +++ b/packages/astro/test/server-islands.test.js @@ -0,0 +1,120 @@ + +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import * as cheerio from 'cheerio'; +import testAdapter from './test-adapter.js'; +import { loadFixture } from './test-utils.js'; + +describe('Server islands', () => { + describe('SSR', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + before(async () => { + fixture = await loadFixture({ + root: './fixtures/server-islands/ssr', + adapter: testAdapter(), + }); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await fixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('omits the islands HTML', async () => { + const res = await fixture.fetch('/'); + assert.equal(res.status, 200); + const html = await res.text(); + const $ = cheerio.load(html); + const serverIslandEl = $('h2#island'); + assert.equal(serverIslandEl.length, 0); + }); + }); + + describe('prod', () => { + before(async () => { + await fixture.build(); + }); + + it('omits the islands HTML', async () => { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/'); + const response = await app.render(request); + const html = await response.text(); + + const $ = cheerio.load(html); + const serverIslandEl = $('h2#island'); + assert.equal(serverIslandEl.length, 0); + + const serverIslandScript = $('script[data-island-id]'); + assert.equal(serverIslandScript.length, 1, 'has the island script'); + }); + }); + }); + + describe('Hybrid mode', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + before(async () => { + fixture = await loadFixture({ + root: './fixtures/server-islands/hybrid', + adapter: testAdapter(), + }); + }); + + describe('build', () => { + before(async () => { + await fixture.build(); + }); + + it('Omits the island HTML from the static HTML', async () => { + let html = await fixture.readFile('/client/index.html'); + + const $ = cheerio.load(html); + const serverIslandEl = $('h2#island'); + assert.equal(serverIslandEl.length, 0); + + const serverIslandScript = $('script[data-island-id]'); + assert.equal(serverIslandScript.length, 1, 'has the island script'); + }); + + describe('prod', () => { + async function fetchIsland() { + const app = await fixture.loadTestAdapterApp(); + const request = new Request('http://example.com/_server-islands/Island', { + method: 'POST', + body: JSON.stringify({ + componentExport: 'default', + props: {}, + slots: {}, + }) + }); + return app.render(request); + } + + it('Island returns its HTML', async () => { + const response = await fetchIsland(); + const html = await response.text(); + const $ = cheerio.load(html); + + const serverIslandEl = $('h2#island'); + assert.equal(serverIslandEl.length, 1); + }); + + it('Island does not include the doctype', async () => { + const response = await fetchIsland(); + const html = await response.text(); + console.log(html); + + assert.ok(!/doctype/i.test(html), 'html does not include doctype'); + }); + }); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b3d759ba89a..8769f7a8f5c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1596,6 +1596,27 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + packages/astro/e2e/fixtures/server-islands: + dependencies: + '@astrojs/mdx': + specifier: workspace:* + version: link:../../../../integrations/mdx + '@astrojs/node': + specifier: workspace:* + version: link:../../../../integrations/node + '@astrojs/react': + specifier: workspace:* + version: link:../../../../integrations/react + astro: + specifier: workspace:* + version: link:../../.. + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + packages/astro/e2e/fixtures/solid-circular: dependencies: '@astrojs/solid-js': @@ -3616,6 +3637,30 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/server-islands/hybrid: + dependencies: + '@astrojs/svelte': + specifier: workspace:* + version: link:../../../../../integrations/svelte + astro: + specifier: workspace:* + version: link:../../../.. + svelte: + specifier: ^4.2.18 + version: 4.2.18 + + packages/astro/test/fixtures/server-islands/ssr: + dependencies: + '@astrojs/svelte': + specifier: workspace:* + version: link:../../../../../integrations/svelte + astro: + specifier: workspace:* + version: link:../../../.. + svelte: + specifier: ^4.2.18 + version: 4.2.18 + packages/astro/test/fixtures/set-html: dependencies: astro: