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 });