Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions packages/react/src/plugin-pages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, relative, resolve } from 'pathslash';
import type { Plugin } from 'vite';
import {
Expand All @@ -22,7 +23,8 @@ const WORKER_ENVIRONMENT_NAME = 'void_worker';
const REACT_DEDUPE = ['react', 'react-dom'];
const REACT_OPTIMIZE_DEPS = ['react', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom'];
const CLIENT_OPTIMIZE_DEPS = [...REACT_OPTIMIZE_DEPS, 'react-dom/client'];
const WORKER_OPTIMIZE_DEPS = [...REACT_OPTIMIZE_DEPS, 'react-dom/server', 'void/live/client'];
const WORKER_OPTIMIZE_DEPS = [...REACT_OPTIMIZE_DEPS, 'react-dom/server'];
const NODE_MODULES_SEGMENT = /[\\/]node_modules[\\/]/;

// SSR HTML bypasses transformIndexHtml, so @vitejs/plugin-react cannot inject
// its Fast Refresh preamble. Without it, the dev client entry fails to load.
Expand Down Expand Up @@ -73,7 +75,10 @@ export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plu
// from Vite's dep optimizer. void/pages-client and
// void/pages-prefetch are reached only through virtual entries and
// the excluded @void/react, so the scanner cannot see them.
include: [...CLIENT_OPTIMIZE_DEPS, 'void/pages-client', 'void/pages-prefetch'],
include: [
...CLIENT_OPTIMIZE_DEPS,
...installedDependencies(['void/pages-client', 'void/pages-prefetch']),
],
},
ssr: {
optimizeDeps: {
Expand All @@ -89,7 +94,7 @@ export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plu
// SSR is already rendering, the program reload can mix React
// and react-dom/server from different optimized-dep hashes.
entries: [pagesOptimizeEntries],
include: WORKER_OPTIMIZE_DEPS,
include: [...WORKER_OPTIMIZE_DEPS, ...installedDependencies(['void/live/client'])],
},
},
},
Expand Down Expand Up @@ -117,6 +122,22 @@ export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plu
});
}

/**
* Keeps the specifiers that resolve into `node_modules`. Vite serves a linked
* package, such as `void` in a workspace, as source unless
* `optimizeDeps.include` lists it. A listed linked package is pre-bundled into
* a cache keyed by the lockfile and config rather than the package's files, so
* dev would keep serving that copy after the package is rebuilt. A client built
* against the new exports then fails to load and never hydrates.
*/
export function installedDependencies(
specifiers: ReadonlyArray<string>,
resolveFile: (specifier: string) => string = (specifier) =>
fileURLToPath(import.meta.resolve(specifier)),
): Array<string> {
return specifiers.filter((specifier) => NODE_MODULES_SEGMENT.test(resolveFile(specifier)));
}

function clientOptimizeEntries(
scan: PageScanResult | null,
pagesDir: string,
Expand Down
40 changes: 32 additions & 8 deletions packages/react/test/unit/plugin-pages.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +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 { pagesPlugin } from '../../src/plugin-pages.ts';
import { installedDependencies, pagesPlugin } from '../../src/plugin-pages.ts';

const TMP = join(import.meta.dirname, '__tmp_pages_plugin__');
const PAGES_DIR = join(TMP, 'pages');
Expand Down Expand Up @@ -197,6 +197,22 @@ describe('pagesPlugin build config', () => {
});
});

describe('installedDependencies', () => {
it('keeps entries installed in node_modules and drops linked workspace entries', () => {
const files: Record<string, string> = {
'void/pages-client':
'/app/node_modules/.pnpm/void@1.6.2/node_modules/void/dist/pages/client.mjs',
'void/pages-prefetch': 'C:\\app\\node_modules\\void\\dist\\pages\\prefetch.mjs',
'void/live/client': '/repo/packages/void/dist/runtime/live-client.mjs',
};

expect(installedDependencies(Object.keys(files), (specifier) => files[specifier]!)).toEqual([
'void/pages-client',
'void/pages-prefetch',
]);
});
});

describe('pagesPlugin dev config', () => {
afterEach(() => {
rmSync(TMP, { recursive: true, force: true });
Expand All @@ -209,19 +225,14 @@ describe('pagesPlugin dev config', () => {
expect(config).toMatchObject({
optimizeDeps: {
entries: ['pages/index.tsx'],
include: expect.arrayContaining(['void/pages-client', 'void/pages-prefetch']),
include: expect.arrayContaining(['react', 'react-dom/client']),
},
environments: {
void_worker: {
optimizeDeps: {
exclude: ['@void/react'],
entries: ['pages/**/*.{js,jsx,ts,tsx}'],
include: expect.arrayContaining([
'react',
'react-dom',
'react-dom/server',
'void/live/client',
]),
include: expect.arrayContaining(['react', 'react-dom', 'react-dom/server']),
},
},
},
Expand Down Expand Up @@ -287,6 +298,19 @@ describe('pagesPlugin dev config', () => {
expect((config.optimizeDeps as { entries: Array<string> }).entries).toHaveLength(2);
});

it('serves the workspace-linked void as source instead of pre-bundling it', async () => {
// This repository links void from packages/void, so a pre-bundled copy would
// outlive the next void build.
const config = (await loadServeConfig()) as {
optimizeDeps: { include: Array<string> };
environments: { void_worker: { optimizeDeps: { include: Array<string> } } };
};

expect(config.optimizeDeps.include).not.toContain('void/pages-client');
expect(config.optimizeDeps.include).not.toContain('void/pages-prefetch');
expect(config.environments.void_worker.optimizeDeps.include).not.toContain('void/live/client');
});

it('includes only the React entry points needed by each environment', async () => {
const config = (await loadServeConfig()) as {
optimizeDeps: { include: Array<string> };
Expand Down
Loading