diff --git a/packages/react/src/plugin-pages.ts b/packages/react/src/plugin-pages.ts index dcfd36b31..f449a117e 100644 --- a/packages/react/src/plugin-pages.ts +++ b/packages/react/src/plugin-pages.ts @@ -38,7 +38,7 @@ type PagesPluginOptions = { prefetch?: PrefetchConfig; }; -/** Check if the pages directory has any non-island page files (.tsx/.jsx without .island.). */ +/** Check if the pages directory has any page components that need the full client runtime. */ function hasNonIslandPages(dir: string): boolean { try { for (const entry of readdirSync(dir, { withFileTypes: true, recursive: true })) { @@ -46,7 +46,7 @@ function hasNonIslandPages(dir: string): boolean { continue; } const name = entry.name; - if (name.startsWith('_') || name.startsWith('layout.')) { + if (name.startsWith('_') || name.startsWith('layout.') || name.includes('.server.')) { continue; } if (/\.(tsx|jsx)$/.test(name) && !name.includes('.island.')) { diff --git a/packages/react/test/unit/plugin-pages.test.ts b/packages/react/test/unit/plugin-pages.test.ts index 246d3cc9a..f02b979f3 100644 --- a/packages/react/test/unit/plugin-pages.test.ts +++ b/packages/react/test/unit/plugin-pages.test.ts @@ -1,6 +1,7 @@ import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vite-plus/test'; +import { islandsPlugin } from '../../src/plugin-islands.ts'; import { pagesPlugin } from '../../src/plugin-pages.ts'; const TMP = join(import.meta.dirname, '__tmp_pages_plugin__'); @@ -63,6 +64,65 @@ function setupWatcher(plugin: PagesPluginHooks, logError = vi.fn(), invalidateMo return handlers; } +describe('pagesPlugin build config', () => { + afterEach(() => { + rmSync(TMP, { recursive: true, force: true }); + }); + + it.each(['tsx', 'jsx'])( + 'omits pages-client for island-only pages with .server.%s files', + async (extension) => { + writePage( + 'index.island.tsx', + `import Counter from '../Counter' with { island: 'load' }; +export default function Home() { return ; }`, + ); + writePage(`index.server.${extension}`, 'export const loader = () => ({ count: 0 });'); + writeFileSync( + join(TMP, 'Counter.tsx'), + 'export default function Counter() { return ; }', + ); + + const plugin = pagesPlugin(PAGES_DIR) as PagesPluginHooks; + expect(getHookHandler(plugin.config)({}, { command: 'build' })).toBeUndefined(); + + const islandPlugin = islandsPlugin(PAGES_DIR)[0] as PagesPluginHooks; + const islandConfig = await getHookHandler(islandPlugin.config)({}, { command: 'build' }); + expect(islandConfig).toMatchObject({ + environments: { + client: { + build: { + rollupOptions: { input: { 'islands-client': 'virtual:void-islands-client' } }, + }, + }, + }, + }); + await getHookHandler(islandPlugin.buildStart).call({}); + const client = await getHookHandler(islandPlugin.load)('\0virtual:void-islands-client'); + expect(client).toContain('"Counter"'); + expect(client).toContain('initIslands(islands,'); + }, + ); + + it.each(['about.tsx', 'about.jsx', 'blog/about.tsx', 'blog/about.jsx'])( + 'keeps pages-client for an ordinary %s page alongside island pages', + (page) => { + writePage('index.island.tsx', 'export default function Home() { return

Home

; }'); + writePage('index.server.tsx', 'export const loader = () => ({});'); + writePage('index.server.jsx', 'export const loader = () => ({});'); + writePage(page, 'export default function About() { return

About

; }'); + + const plugin = pagesPlugin(PAGES_DIR) as PagesPluginHooks; + expect(getHookHandler(plugin.config)({}, { command: 'build' })).toMatchObject({ + build: { + manifest: true, + rollupOptions: { input: { 'pages-client': 'virtual:void-pages-client' } }, + }, + }); + }, + ); +}); + describe('pagesPlugin dev config', () => { afterEach(() => { rmSync(TMP, { recursive: true, force: true }); diff --git a/packages/solid/src/plugin-pages.ts b/packages/solid/src/plugin-pages.ts index 1c4472b97..e2fe48a7a 100644 --- a/packages/solid/src/plugin-pages.ts +++ b/packages/solid/src/plugin-pages.ts @@ -22,7 +22,7 @@ type PagesPluginOptions = { prefetch?: PrefetchConfig; }; -/** Check if the pages directory has any non-island page files (.tsx/.jsx without .island.). */ +/** Check if the pages directory has any page components that need the full client runtime. */ function hasNonIslandPages(dir: string): boolean { try { for (const entry of readdirSync(dir, { withFileTypes: true, recursive: true })) { @@ -30,7 +30,7 @@ function hasNonIslandPages(dir: string): boolean { continue; } const name = entry.name; - if (name.startsWith('_') || name.startsWith('layout.')) { + if (name.startsWith('_') || name.startsWith('layout.') || name.includes('.server.')) { continue; } if (/\.(tsx|jsx)$/.test(name) && !name.includes('.island.')) { diff --git a/packages/solid/test/unit/plugin-pages.test.ts b/packages/solid/test/unit/plugin-pages.test.ts index b31feba83..e89fbe8ea 100644 --- a/packages/solid/test/unit/plugin-pages.test.ts +++ b/packages/solid/test/unit/plugin-pages.test.ts @@ -1,6 +1,7 @@ import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vite-plus/test'; +import { islandsPlugin } from '../../src/plugin-islands.ts'; import { pagesPlugin } from '../../src/plugin-pages.ts'; const TMP = join(import.meta.dirname, '__tmp_pages_plugin__'); @@ -54,6 +55,65 @@ function setupWatcher(plugin: PagesPluginHooks, logError = vi.fn(), invalidateMo return handlers; } +describe('pagesPlugin build config', () => { + afterEach(() => { + rmSync(TMP, { recursive: true, force: true }); + }); + + it.each(['tsx', 'jsx'])( + 'omits pages-client for island-only pages with .server.%s files', + async (extension) => { + writePage( + 'index.island.tsx', + `import Counter from '../Counter' with { island: 'load' }; +export default function Home() { return ; }`, + ); + writePage(`index.server.${extension}`, 'export const loader = () => ({ count: 0 });'); + writeFileSync( + join(TMP, 'Counter.tsx'), + 'export default function Counter() { return ; }', + ); + + const plugin = pagesPlugin(PAGES_DIR) as PagesPluginHooks; + expect(getHookHandler(plugin.config)({}, { command: 'build' })).toBeUndefined(); + + const islandPlugin = islandsPlugin(PAGES_DIR)[0] as PagesPluginHooks; + const islandConfig = await getHookHandler(islandPlugin.config)({}, { command: 'build' }); + expect(islandConfig).toMatchObject({ + environments: { + client: { + build: { + rollupOptions: { input: { 'islands-client': 'virtual:void-islands-client' } }, + }, + }, + }, + }); + await getHookHandler(islandPlugin.buildStart).call({}); + const client = await getHookHandler(islandPlugin.load)('\0virtual:void-islands-client'); + expect(client).toContain('"Counter"'); + expect(client).toContain('initIslands(islands,'); + }, + ); + + it.each(['about.tsx', 'about.jsx', 'blog/about.tsx', 'blog/about.jsx'])( + 'keeps pages-client for an ordinary %s page alongside island pages', + (page) => { + writePage('index.island.tsx', 'export default function Home() { return

Home

; }'); + writePage('index.server.tsx', 'export const loader = () => ({});'); + writePage('index.server.jsx', 'export const loader = () => ({});'); + writePage(page, 'export default function About() { return

About

; }'); + + const plugin = pagesPlugin(PAGES_DIR) as PagesPluginHooks; + expect(getHookHandler(plugin.config)({}, { command: 'build' })).toMatchObject({ + build: { + manifest: true, + rollupOptions: { input: { 'pages-client': 'virtual:void-pages-client' } }, + }, + }); + }, + ); +}); + describe('pagesPlugin dev HMR', () => { afterEach(() => { rmSync(TMP, { recursive: true, force: true });