diff --git a/packages/react/src/plugin-pages.ts b/packages/react/src/plugin-pages.ts index dcfd36b31..1e38ad0d6 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,13 @@ function hasNonIslandPages(dir: string): boolean { continue; } const name = entry.name; - if (name.startsWith('_') || name.startsWith('layout.')) { + // Server companions such as `index.server.tsx` are not pages. + if ( + name.startsWith('_') || + name.startsWith('layout.') || + name.endsWith('.server.tsx') || + name.endsWith('.server.jsx') + ) { 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..11d6de15a 100644 --- a/packages/react/test/unit/plugin-pages.test.ts +++ b/packages/react/test/unit/plugin-pages.test.ts @@ -63,6 +63,41 @@ 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', + (extension) => { + writePage('index.island.tsx', 'export default function Home() { return

Home

; }'); + writePage(`index.server.${extension}`, 'export const loader = () => ({ count: 0 });'); + + const plugin = pagesPlugin(PAGES_DIR) as PagesPluginHooks; + expect(getHookHandler(plugin.config)({}, { command: 'build' })).toBeUndefined(); + }, + ); + + it.each(['about.tsx', 'about.jsx', 'blog/about.tsx', 'blog/about.jsx', 'status.server.v2.tsx'])( + '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..6195176e1 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,13 @@ function hasNonIslandPages(dir: string): boolean { continue; } const name = entry.name; - if (name.startsWith('_') || name.startsWith('layout.')) { + // Server companions such as `index.server.tsx` are not pages. + if ( + name.startsWith('_') || + name.startsWith('layout.') || + name.endsWith('.server.tsx') || + name.endsWith('.server.jsx') + ) { 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..c39dbbf6e 100644 --- a/packages/solid/test/unit/plugin-pages.test.ts +++ b/packages/solid/test/unit/plugin-pages.test.ts @@ -54,6 +54,41 @@ 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', + (extension) => { + writePage('index.island.tsx', 'export default function Home() { return

Home

; }'); + writePage(`index.server.${extension}`, 'export const loader = () => ({ count: 0 });'); + + const plugin = pagesPlugin(PAGES_DIR) as PagesPluginHooks; + expect(getHookHandler(plugin.config)({}, { command: 'build' })).toBeUndefined(); + }, + ); + + it.each(['about.tsx', 'about.jsx', 'blog/about.tsx', 'blog/about.jsx', 'status.server.v2.tsx'])( + '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 });