Skip to content

Commit f36e7bd

Browse files
Brooooooklynclaude
andauthored
fix(pages): include .dev and .prod pages only in their environment (#595)
Fixes #558. Pages stripped `.dev` and `.prod` from the URL but kept the page in every environment. So `debug.dev.tsx` shipped to production, and a `home.dev.tsx` + `home.prod.tsx` pair failed as a duplicate route. Pages now follow the same rule as `routes/`. | Page file(s) | `vp dev` | build / deploy / preview | URL | |---|---|---|---| | `home.tsx` | ✓ | ✓ | `/home` | | `debug.dev.tsx` | ✓ | — | `/debug` | | `stats.prod.tsx` | — | ✓ | `/stats` | | `home.dev.tsx` + `home.prod.tsx` | dev file | prod file | `/home` | | `home.tsx` + `home.dev.tsx` | error (two pages for `/home`) | `home.tsx` | `/home` | | `home.tsx` + `home.prod.tsx` | `home.tsx` | error | `/home` | ``` environment when pages kept dev vp dev plain + .dev prod build, deploy, vite preview plain + .prod (none) void prepare (types only) all; clash check runs once as dev, once as prod ``` Rules: - The suffix goes before `.island`: `chart.dev.island.tsx`. - A companion repeats it: `home.dev.tsx` pairs with `home.dev.server.ts`. A dropped page's companion is never read. - Layouts take no suffix. - The page id keeps the suffix (`home.dev`, `home.prod`), so the two pages of a pair never share an id. ## Every way a `.dev` page could reach production, and what closes it ``` core route table, prerender, ISR, deploy, types → scanPages(environment), passed from the app analysis adapter renderer + client entry (React/Vue/Svelte/Solid) → pages plugin rescans with the current environment adapter island list and island client → islands plugin scans with the current environment islands client entry, dev island rescan → hasIslandPages scans 'prod'; the dev watcher scans 'dev' inferred bindings (D1, KV, R2, AI…) → page files and companions filtered like routes/ @void/md pages list and md client scripts → same filter; the listed path drops the suffix ``` Framework apps that own `pages/` (Nuxt and others) are unchanged. ## Generated types - For a `.dev` + `.prod` pair, `void prepare` writes one `PageActionMap` entry per URL as a union, `{ dev shape } | { prod shape }`. So each page's form body type-checks even when the two bodies differ. - `ResolveActionBody` and `ActionUrl` in `void/routes` (and their generated copies) now read each half of that union on its own. So `useForm` accepts each page's URLs and body, even when one page has `action` and the other has `actions`, or the two have different named actions. A page with only named actions no longer loosens the other page's bare-URL body. For a single page the result is the same as before. ## Tests - `pages-scan.test.ts`: `.dev` page; `.dev` + `.prod` pair; plain + `.dev` and plain + `.prod` clashes, per environment and unfiltered; shadow warnings per environment; `.dev.island`; a broken companion of a dropped page is never parsed; `index.dev`; layouts; a `pageFileEnvironment` table checked against the scanner. - `scan.test.ts`, `route-types.test.ts`, `plugin-routing.test.ts`, `pages-islands-rescan.test.ts`, `plugin-inference.test.ts` (including a Nuxt app, whose bindings are not filtered). - React, Vue, Svelte and Solid: pages and islands in build and in serve, including `pages/` created after the dev server starts. - `@void/md`: serve lists `debug.dev.md` as `/debug`; a build leaves it and its script out. - Integration: - `tsx-server.test.ts` runs a real `vp dev` and `vp build`. `/debug` gives 200 in dev and 404 in prod, `/greeting` shows each environment's page, and the production types and module graphs have no `.dev` page. - `page-action-types-typecheck.test.ts` runs `tsc` on generated pair types through the same `U extends ActionUrl` constraint the adapters' `useForm` uses. - Mutation checks: each guarded line was broken in turn (the scanner filter, the per-environment clash loop, every adapter's environment value and the `isDev` move, the islands helpers, the type join, `ResolveActionBody`, the inference filter, the md filter). Each break made a test fail. - `vp fmt`, `vp lint`, the void/react/vue/svelte/solid/md typechecks, `vp run test` and one full `vp test run` pass. A separate review built real apps and found no trace of `.dev` files in any production output: bundles, manifests, prerender output, `_headers`, wrangler bindings or types. It also checked `vp preview`, adding and removing pages while `vp dev` runs, and `void prepare`. ## Choices to check - **Types follow the running environment.** The dev server and builds write `.void/routes.d.ts` for their own environment, as `routes/` already does, and `void prepare` writes both. So while `vp dev` runs, a `.prod` page that uses `useForm` shows a type error in the editor (and the other way round after `vp build`). Fixing that would need a second page scan just for types, which would have to parse dev-only files during a production build. The docs say this plainly. - **`home.tsx` + `home.prod.tsx` errors only at build or `void prepare`**, not on the dev server, because only one page runs in dev. The same goes for `routes/`. - **Helper files shared by `.dev` pages** (`pages/_db.ts`, `src/db.ts`) still add their bindings to production. Binding inference reads each file on its own and doesn't follow imports, as with `routes/`. - **Wrong suffix order** (`chart.island.dev.tsx`) gets no new error. It is a dev-only regular page at `/chart.island`. The docs say the suffix goes first, as in `chat.dev.ws.ts` for WebSocket routes. - An app whose only island page or only md page with a `<script>` is `.dev` gets an empty client chunk in production. No page code ships. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent a71da90 commit f36e7bd

41 files changed

Lines changed: 1451 additions & 88 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/guide/pages-routing/markdown.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ import pages from '@void/md/pages';
241241
// [{ path: "/docs/getting-started", title: "Getting Started", frontmatter: {...}, headings: [...] }, ...]
242242
```
243243

244+
The list holds only the pages for the current [environment](./overview.md#environment-only-pages): `.dev` pages on the dev server, `.prod` pages in builds. The suffix is dropped from `path`: `debug.dev.md` is listed as `/debug`.
245+
244246
Each entry has this shape:
245247

246248
```ts

‎docs/guide/pages-routing/overview.md‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,29 @@ A companion can export:
109109
- [**Actions**](./actions-and-forms), which handle mutations from forms and programmatic calls. Export a single `action` or multiple [named actions](./actions-and-forms#named-actions) when a page has several mutations
110110
- `ssr = false` to opt a route out of server-rendered component HTML while keeping server loaders and client-side routing
111111

112-
File-based routing rules are the same as [server routing](../server-routing.md): `[param]` for dynamic segments, `[...param]` for catch-all, `(group)/` for route groups.
112+
File-based routing rules are the same as [server routing](../server-routing.md): `[param]` for dynamic segments, `[...param]` for catch-all, `(group)/` for route groups, and a `.dev` or `.prod` suffix for [environment-only pages](#environment-only-pages).
113+
114+
### Environment-only pages
115+
116+
Add a `.dev` or `.prod` suffix before the extension to include a page in only one environment. The suffix is not part of the URL:
117+
118+
```
119+
pages/
120+
about.tsx → /about, always
121+
debug.dev.tsx → /debug, development only
122+
stats.prod.tsx → /stats, production only
123+
home.dev.tsx → /home in development
124+
home.prod.tsx → /home in production
125+
```
126+
127+
`.dev` pages run only on the dev server; `.prod` pages run in builds, deployments, and production previews.
128+
129+
- A `.dev` page and a `.prod` page can share a URL. A plain page and a suffixed page for the same URL is an error wherever both run: `home.tsx` with `home.dev.tsx` fails on the dev server, and `home.tsx` with `home.prod.tsx` fails the build.
130+
- The suffix goes **before** `.island`: `chart.dev.island.tsx` is a development-only island page. `chart.island.dev.tsx` is not an island page.
131+
- A companion repeats the suffix: `home.dev.server.ts` belongs to `home.dev.tsx`.
132+
- Suffixes apply to page files only. Layouts take no suffix.
133+
- A resource used only by `.dev` pages and their companions is not provisioned for production.
134+
- The dev server and builds generate types for their own environment. `void prepare` generates types for both.
113135

114136
## How Navigation Works
115137

‎docs/guide/server-routing.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ routes/
2929

3030
`.dev` routes run only in development; `.prod` routes run in builds, deployments, and production previews. A resource used only by `.dev` routes is not provisioned for production.
3131

32-
Suffixes apply only in `routes/`, not in `pages/`, `middleware/`, `crons/`, or `queues/`. `void prepare` generates types for both environments.
32+
Suffixes apply in `routes/` and [`pages/`](./pages-routing/overview.md#environment-only-pages), not in `middleware/`, `crons/`, or `queues/`. `void prepare` generates types for both environments.
3333

3434
Each file exports named HTTP method constants to handle specific methods:
3535

‎docs/reference/structure.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ Full-stack server-rendered pages with co-located data loading. Available with Vu
8585
- **`.server.ts` (or `.tsx`, `.js`, `.jsx`, `.mts`, `.mjs`) files** export `loader` for `GET` data and `action` for mutations, paired with the page component of the same name
8686
- **Layouts** nest automatically and persist across navigations
8787
- **Markdown** pages are supported with frontmatter
88+
- **Environment suffix**: `debug.dev.tsx` is served in development only, `stats.prod.tsx` in production only
8889

8990
See [Pages Routing](/guide/pages-routing/overview) for the full guide.
9091

‎packages/md/src/plugin.ts‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs';
33
import { dirname, join, relative, resolve } from 'pathslash';
44
import { glob } from 'tinyglobby';
55
import { normalizePath, type Plugin, type ResolvedConfig, type ViteDevServer } from 'vite';
6-
import { resolvePagesDir } from 'void/pages';
6+
import { pageFileEnvironment, resolvePagesDir } from 'void/pages';
77
import type { CompileOptions } from './compile.ts';
88
import { emitReactComponent } from './emit-react.ts';
99
import { emitSolidComponent } from './emit-solid.ts';
@@ -44,6 +44,19 @@ function stripPageSuffix(file: string): string {
4444
.replace(/^index$/, '');
4545
}
4646

47+
// The listed path drops the .dev / .prod suffix, like page routes do:
48+
// debug.dev.md -> debug, index.dev.md -> ''.
49+
function mdRouteName(file: string, env: 'dev' | 'prod' | null): string {
50+
let name = file.slice(0, -'.md'.length);
51+
if (env) {
52+
name = name.slice(0, -('.' + env).length);
53+
}
54+
if (name === 'index') {
55+
return '';
56+
}
57+
return name.endsWith('/index') ? name.slice(0, -'/index'.length) : name;
58+
}
59+
4760
function hasMdFilesWithScript(dir: string): boolean {
4861
try {
4962
for (const entry of readdirSync(dir, { withFileTypes: true, recursive: true })) {
@@ -79,6 +92,8 @@ export function voidMarkdown(options?: MarkdownOptions): Array<Plugin> {
7992
let mdPages: ReadonlyArray<MdPage> = [];
8093
let root = '';
8194
let pagesDir = '';
95+
// 'dev' on the dev server, 'prod' for builds and preview.
96+
let mdEnvironment: 'dev' | 'prod' = 'prod';
8297
const svelteModules = new Map<string, string>();
8398
const solidModules = new Map<string, string>();
8499
const clientScripts = new Map<string, string>();
@@ -100,21 +115,26 @@ export function voidMarkdown(options?: MarkdownOptions): Array<Plugin> {
100115
}
101116

102117
const mdFiles = await glob('**/*.md', { cwd: pagesDir });
118+
// Skip pages for the other environment, so their scripts are not added either.
119+
const liveFiles = mdFiles.filter((file) => {
120+
const fileEnv = pageFileEnvironment(file);
121+
return !fileEnv || fileEnv === mdEnvironment;
122+
});
103123
const pages = await Promise.all(
104-
mdFiles.map(async (file) => {
124+
liveFiles.map(async (file) => {
105125
const content = readFileSync(join(pagesDir, file), 'utf-8');
106126
const { body, clientCode } = extractScript(content);
107127
const result = extractMetadata(body);
108128

109-
const routeName = stripPageSuffix(file);
110-
const componentId = routeName || 'index';
129+
// componentId keeps the suffix so it matches the page id from scanPages.
130+
const componentId = stripPageSuffix(file) || 'index';
111131

112132
if (clientCode) {
113133
clientScripts.set(componentId, clientCode);
114134
}
115135

116136
return {
117-
path: '/' + routeName,
137+
path: '/' + mdRouteName(file, pageFileEnvironment(file)),
118138
title: result.title,
119139
frontmatter: result.frontmatter,
120140
headings: result.headings,
@@ -137,6 +157,7 @@ export function voidMarkdown(options?: MarkdownOptions): Array<Plugin> {
137157
},
138158

139159
config(config, env) {
160+
mdEnvironment = env.command === 'serve' && !env.isPreview ? 'dev' : 'prod';
140161
if (env.command === 'serve') {
141162
return;
142163
}

‎packages/md/test/unit/plugin-source-dir.test.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,62 @@ describe('voidMarkdown sourceDir support', () => {
6565
expect(load('\0' + 'virtual:void-md-client')).toContain('virtual:void-md-script:posts/hello');
6666
});
6767

68+
async function scanForCommand(root: string, command: 'serve' | 'build') {
69+
const plugin = createPlugin();
70+
const config = getHookHandler(plugin.config) as (
71+
config: { root: string },
72+
env: { command: 'serve' | 'build'; isPreview?: boolean },
73+
) => unknown;
74+
const configResolved = getHookHandler(plugin.configResolved) as (
75+
config: import('vite').ResolvedConfig,
76+
) => void;
77+
const buildStart = getHookHandler(plugin.buildStart) as () => Promise<void>;
78+
const load = getHookHandler(plugin.load) as (id: string) => string | undefined;
79+
80+
config({ root }, { command });
81+
configResolved({
82+
root,
83+
plugins: [{ name: 'void-react:pages' }],
84+
} as unknown as import('vite').ResolvedConfig);
85+
await buildStart.call({} as never);
86+
87+
const source = load('\0void-md-pages')!;
88+
const pages = JSON.parse(source.slice('export default '.length, -1)) as Array<{
89+
path: string;
90+
}>;
91+
return {
92+
paths: pages.map((page) => page.path),
93+
clientEntry: load('\0' + 'virtual:void-md-client'),
94+
};
95+
}
96+
97+
it('lists .dev markdown pages without the suffix only on the dev server', async () => {
98+
const root = createTempRoot();
99+
const pagesDir = join(root, 'src', 'pages');
100+
writeFileSync(
101+
join(pagesDir, 'debug.dev.md'),
102+
'<script>\ndocument.body.dataset.md = "debug";\n</script>\n\n# Debug\n',
103+
);
104+
writeFileSync(join(pagesDir, 'index.dev.md'), '# Dev home\n');
105+
writeFileSync(join(pagesDir, 'posts', 'hello.md'), '# Hello\n');
106+
107+
const serve = await scanForCommand(root, 'serve');
108+
expect(serve.paths).toEqual(['/', '/debug', '/posts/hello']);
109+
expect(serve.clientEntry).toContain('virtual:void-md-script:debug.dev');
110+
111+
const build = await scanForCommand(root, 'build');
112+
expect(build.paths).toEqual(['/posts/hello']);
113+
expect(build.clientEntry).not.toContain('debug.dev');
114+
});
115+
116+
it('lists .prod markdown pages only in builds', async () => {
117+
const root = createTempRoot();
118+
writeFileSync(join(root, 'src', 'pages', 'posts', 'stats.prod.md'), '# Stats\n');
119+
120+
expect((await scanForCommand(root, 'serve')).paths).toEqual([]);
121+
expect((await scanForCommand(root, 'build')).paths).toEqual(['/posts/stats']);
122+
});
123+
68124
it('adds the markdown client entry for sourceDir pages with scripts and islands', () => {
69125
const root = createTempRoot();
70126
writeFileSync(

‎packages/react/src/plugin-islands.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ export function islandsPlugin(
7373

7474
async buildStart() {
7575
if (existsSync(pagesDir)) {
76-
pageScan = await scanPages(root, { pagesDir });
76+
pageScan = await scanPages(root, {
77+
pagesDir,
78+
environment: isDev ? 'dev' : 'prod',
79+
});
7780
// Pre-scan island pages to populate the manifest eagerly.
7881
// This is needed because the client build may load the virtual
7982
// islands-client entry before island page transforms run.

‎packages/react/src/plugin-pages.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plu
7474
const pagesOptimizeEntries = `${relative(root, pagesDir)}/**/*.{js,jsx,ts,tsx}`;
7575
let pendingRescan: Promise<void> | null = null;
7676
const rescanPages = async () => {
77-
pageScan = existsSync(pagesDir) ? await scanPages(root, { pagesDir }) : null;
77+
pageScan = existsSync(pagesDir)
78+
? await scanPages(root, { pagesDir, environment: isDev ? 'dev' : 'prod' })
79+
: null;
7880
};
7981
const runRescan = () => {
8082
pendingRescan ??= rescanPages().finally(() => {
@@ -174,6 +176,8 @@ export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plu
174176
},
175177

176178
config(_, env) {
179+
// Set before the early return: pages/ may be created after the server starts.
180+
isDev = env.command === 'serve';
177181
if (!existsSync(pagesDir)) {
178182
return;
179183
}
@@ -182,8 +186,7 @@ export function pagesPlugin(pagesDir: string, options?: PagesPluginOptions): Plu
182186
// virtual module and user components share the same React context objects.
183187
// Without this, Vite pre-bundles @void/react, creating separate
184188
// context instances that break useContext (React contexts use object identity).
185-
if (env.command === 'serve') {
186-
isDev = true;
189+
if (isDev) {
187190
return {
188191
resolve: {
189192
dedupe: REACT_DEDUPE,

‎packages/react/test/unit/plugin-islands.test.ts‎

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type HookWithHandler<T> = { handler: T };
99
type IslandsPluginHooks = {
1010
configResolved?: (config: {
1111
root: string;
12-
command: 'build';
12+
command: 'serve' | 'build';
1313
plugins: ReadonlyArray<unknown>;
1414
}) => void;
1515
buildStart?: (() => Promise<void>) | HookWithHandler<() => Promise<void>>;
@@ -25,11 +25,12 @@ function writeFile(relativePath: string, content: string): void {
2525
writeFileSync(full, content);
2626
}
2727

28-
function setupPlugin() {
28+
function setupPlugin(options?: { dev?: boolean }) {
2929
const plugins = islandsPlugin(PAGES_DIR);
3030
const plugin = plugins[0] as IslandsPluginHooks;
3131

32-
const config = { root: TMP, command: 'build' as const, plugins: [] };
32+
const command = options?.dev ? 'serve' : 'build';
33+
const config = { root: TMP, command: command as 'serve' | 'build', plugins: [] };
3334
plugin.configResolved?.(config);
3435

3536
return {
@@ -178,3 +179,71 @@ export default function Layout({ children }) { return <div>{children}</div>; }`,
178179
expect(clientEntry).not.toContain('Sidebar');
179180
});
180181
});
182+
183+
describe('islandsPlugin — environment suffixes', () => {
184+
beforeEach(() => {
185+
mkdirSync(PAGES_DIR, { recursive: true });
186+
});
187+
188+
afterEach(() => {
189+
rmSync(TMP, { recursive: true, force: true });
190+
});
191+
192+
it('registers no .dev island page in a build', async () => {
193+
writeFile(
194+
'x.dev.island.tsx',
195+
`import Toggle from "./Toggle" with { island: "load" }
196+
export default function X() { return <Toggle />; }`,
197+
);
198+
writeFile('Toggle.tsx', `export default function Toggle() { return <button>off</button>; }`);
199+
200+
const { buildStart, loadClientEntry } = setupPlugin();
201+
await buildStart();
202+
203+
expect(loadClientEntry()).not.toContain('pages/Toggle');
204+
});
205+
206+
it('registers the .prod island page of a .dev + .prod pair in a build', async () => {
207+
writeFile(
208+
'x.dev.island.tsx',
209+
`import Toggle from "./Toggle" with { island: "load" }
210+
export default function X() { return <Toggle />; }`,
211+
);
212+
writeFile(
213+
'x.prod.island.tsx',
214+
`import Counter from "./Counter" with { island: "load" }
215+
export default function X() { return <Counter />; }`,
216+
);
217+
writeFile('Toggle.tsx', `export default function Toggle() { return <button>off</button>; }`);
218+
writeFile('Counter.tsx', `export default function Counter() { return <button>0</button>; }`);
219+
220+
const { buildStart, loadClientEntry } = setupPlugin();
221+
await buildStart();
222+
const clientEntry = loadClientEntry();
223+
224+
expect(clientEntry).toContain('pages/Counter');
225+
expect(clientEntry).not.toContain('pages/Toggle');
226+
});
227+
228+
it('registers the .dev island page of a .dev + .prod pair in dev', async () => {
229+
writeFile(
230+
'x.dev.island.tsx',
231+
`import Toggle from "./Toggle" with { island: "load" }
232+
export default function X() { return <Toggle />; }`,
233+
);
234+
writeFile(
235+
'x.prod.island.tsx',
236+
`import Counter from "./Counter" with { island: "load" }
237+
export default function X() { return <Counter />; }`,
238+
);
239+
writeFile('Toggle.tsx', `export default function Toggle() { return <button>off</button>; }`);
240+
writeFile('Counter.tsx', `export default function Counter() { return <button>0</button>; }`);
241+
242+
const { buildStart, loadClientEntry } = setupPlugin({ dev: true });
243+
await buildStart();
244+
const clientEntry = loadClientEntry();
245+
246+
expect(clientEntry).toContain('pages/Toggle');
247+
expect(clientEntry).not.toContain('pages/Counter');
248+
});
249+
});

‎packages/react/test/unit/plugin-pages.test.ts‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,64 @@ describe('pagesPlugin dev config', () => {
203203
expect(recovered).toContain('"about"');
204204
});
205205
});
206+
207+
describe('pagesPlugin environment suffixes', () => {
208+
afterEach(() => {
209+
rmSync(TMP, { recursive: true, force: true });
210+
});
211+
212+
function writeSuffixPages(): void {
213+
writePage('index.tsx', 'export default function Index() { return <h1>Home</h1>; }\n');
214+
writePage('debug.dev.tsx', 'export default function Debug() { return <h1>Debug</h1>; }\n');
215+
writePage('home.dev.tsx', 'export default function Home() { return <h1>Dev</h1>; }\n');
216+
writePage('home.prod.tsx', 'export default function Home() { return <h1>Prod</h1>; }\n');
217+
}
218+
219+
async function loadModules(command: 'serve' | 'build') {
220+
const plugin = pagesPlugin(PAGES_DIR) as PagesPluginHooks;
221+
getHookHandler(plugin.config)({}, { command });
222+
await getHookHandler(plugin.buildStart).call({});
223+
const load = getHookHandler(plugin.load);
224+
return {
225+
renderer: await load('\0virtual:void-pages-renderer'),
226+
client: await load('\0virtual:void-pages-client'),
227+
};
228+
}
229+
230+
it('leaves .dev pages out of the build renderer and client entry', async () => {
231+
writeSuffixPages();
232+
233+
const { renderer, client } = await loadModules('build');
234+
235+
for (const code of [renderer, client]) {
236+
expect(code).not.toContain('debug.dev');
237+
expect(code).not.toContain('home.dev');
238+
expect(code).toContain('home.prod.tsx');
239+
expect(code).toContain('index.tsx');
240+
}
241+
});
242+
243+
it('keeps .dev pages and leaves out .prod pages in serve', async () => {
244+
writeSuffixPages();
245+
246+
const { renderer, client } = await loadModules('serve');
247+
248+
for (const code of [renderer, client]) {
249+
expect(code).toContain('debug.dev.tsx');
250+
expect(code).toContain('home.dev.tsx');
251+
expect(code).not.toContain('home.prod');
252+
}
253+
});
254+
255+
it('scans for dev when pages/ is created after the serve config', async () => {
256+
const plugin = pagesPlugin(PAGES_DIR) as PagesPluginHooks;
257+
expect(getHookHandler(plugin.config)({}, { command: 'serve' })).toBeUndefined();
258+
259+
writePage('debug.dev.tsx', 'export default function Debug() { return <h1>Debug</h1>; }\n');
260+
await getHookHandler(plugin.buildStart).call({});
261+
const renderer = await getHookHandler(plugin.load)('\0virtual:void-pages-renderer');
262+
263+
expect(renderer).toContain('debug.dev.tsx');
264+
expect(renderer).toContain('/@react-refresh');
265+
});
266+
});

0 commit comments

Comments
 (0)