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