From 33f3054d0f78c655af09930df62a95d4f49de3bd Mon Sep 17 00:00:00 2001 From: Yangshun Date: Fri, 23 Oct 2026 13:17:00 +0800 Subject: [PATCH 1/2] fix(pages): skip server companions in React and Solid client detection --- packages/react/src/plugin-pages.ts | 4 +- packages/react/test/unit/plugin-pages.test.ts | 60 +++++++++++++++++++ packages/solid/src/plugin-pages.ts | 4 +- packages/solid/test/unit/plugin-pages.test.ts | 60 +++++++++++++++++++ 4 files changed, 124 insertions(+), 4 deletions(-) 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..45bb81b56 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 companions', + 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..b495a4f6e 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 companions', + 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 }); From 5a45467cbd79912e2d9179bd58df822de1f2630a Mon Sep 17 00:00:00 2001 From: Yangshun Date: Fri, 23 Oct 2026 13:23:25 +0800 Subject: [PATCH 2/2] test(pages): clarify JSX server filename coverage --- packages/react/test/unit/plugin-pages.test.ts | 2 +- packages/solid/test/unit/plugin-pages.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/test/unit/plugin-pages.test.ts b/packages/react/test/unit/plugin-pages.test.ts index 45bb81b56..f02b979f3 100644 --- a/packages/react/test/unit/plugin-pages.test.ts +++ b/packages/react/test/unit/plugin-pages.test.ts @@ -70,7 +70,7 @@ describe('pagesPlugin build config', () => { }); it.each(['tsx', 'jsx'])( - 'omits pages-client for island-only pages with .server.%s companions', + 'omits pages-client for island-only pages with .server.%s files', async (extension) => { writePage( 'index.island.tsx', diff --git a/packages/solid/test/unit/plugin-pages.test.ts b/packages/solid/test/unit/plugin-pages.test.ts index b495a4f6e..e89fbe8ea 100644 --- a/packages/solid/test/unit/plugin-pages.test.ts +++ b/packages/solid/test/unit/plugin-pages.test.ts @@ -61,7 +61,7 @@ describe('pagesPlugin build config', () => { }); it.each(['tsx', 'jsx'])( - 'omits pages-client for island-only pages with .server.%s companions', + 'omits pages-client for island-only pages with .server.%s files', async (extension) => { writePage( 'index.island.tsx',